From 54e479900afc18782b6ea4717bfa468a3114c86f Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 20 Aug 2026 13:03:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=A8=99=E7=B1=A4=E5=94=AF?= =?UTF-8?q?=E4=B8=80=E7=9C=9F=E7=9B=B8=E6=BA=90=20+=20=E5=86=AA=E7=AD=89?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E8=85=B3=E6=9C=AC=EF=BC=8C=E5=85=A8=20org=20?= =?UTF-8?q?14=20=E5=80=8B=20repo=20=E5=B0=8D=E9=BD=8A=EF=BC=88inkstone/ISE?= =?UTF-8?q?P#4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit labels.yaml:23 個標籤定義(名稱/顏色/描述/exclusive),已是 ISEP repo 現行實況。 scripts/gitea-labels-sync.sh:讀 labels.yaml 對齊任一/全部受治理 repo—— 缺的建、改的還原(僅 color/description/exclusive,不改名)、多出來的只告警不刪, 兩次連跑第二次必為 no-op(已實測 0 created/0 updated)。 實跑對帳:14 個 repo 首跑 253 created + 46 updated;重跑 0/0(冪等); InkStoneCo / Arcrun / content-pipeline 三個抽查點逐欄核對與 labels.yaml 完全一致; arcrun-rag 留有 6 個 Gitea 預設英文標籤(bug/enhancement/help wanted/invalid/question/wontfix), 按規約只告警不刪。 --- labels.yaml | 113 ++++++++++++++++++++++ scripts/gitea-labels-sync.sh | 175 +++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 labels.yaml create mode 100755 scripts/gitea-labels-sync.sh diff --git a/labels.yaml b/labels.yaml new file mode 100644 index 0000000..83f2897 --- /dev/null +++ b/labels.yaml @@ -0,0 +1,113 @@ +# ISEP labels.yaml — 全 inkstone org 的標籤唯一真相源 +# 規約:改標籤=改本檔 → PR → release → 跑 scripts/gitea-labels-sync.sh +# scope 帶「/」且 exclusive:true 者,同票至多一個(Gitea 平台保證,非法狀態不可表示) + +labels: + # ── 狀態機(exclusive,同票只能有一個)───────────────── + - name: "s/triage" + color: "d4c5f9" + exclusive: true + description: "新進來的,還沒驗傷——還沒決定要不要做" + - name: "s/backlog" + color: "c2e0c6" + exclusive: true + description: "驗過了、確定要做,但還沒排進任何 sprint" + - name: "s/todo" + color: "ededed" + exclusive: true + description: "已排進 milestone,等人領票" + - name: "s/doing" + color: "0e8a16" + exclusive: true + description: "進行中——已 self-assign,現在有人在做" + - name: "s/review" + color: "1d76db" + exclusive: true + description: "PR 已開(含 closes #n),等總管 review 並 merge" + - name: "s/stage" + color: "5319e7" + exclusive: true + description: "已部署 stage,等 leo 實際驗收" + - name: "s/pending" + color: "fbca04" + exclusive: true + description: "卡住——等外部/等依賴,不是沒人做" + + # ── 關閉分類(exclusive,關票必掛恰好一個)────────────── + - name: "close/merged" + color: "0e8a16" + exclusive: true + description: "PR merge 自動關(正常路徑)" + - name: "close/human-exec" + color: "b60205" + exclusive: true + description: "人執票完成,由人手動關" + - name: "close/duplicate" + color: "cfd3d7" + exclusive: true + description: "重複——關新的、留舊的,並指向舊票" + - name: "close/wontfix" + color: "6c757d" + exclusive: true + description: "討論後決定不做(只有 leo 可下此判斷)" + - name: "close/transferred" + color: "cfd3d7" + exclusive: true + description: "走錯棚,已在正確的 repo 重開" + - name: "close/split" + color: "cfd3d7" + exclusive: true + description: "太大,已拆成數張子票" + - name: "close/stale" + color: "cfd3d7" + exclusive: true + description: "逾期無資訊,自動關閉" + + # ── 人的介入(正交維度,可疊在任何 s/* 上,故不 exclusive)── + - name: "Human" + color: "b60205" + exclusive: false + description: "👤 這件事需要 leo 親手批示/動手(花錢・不可逆・品味方向・物理動作)。與 s/* 正交" + - name: "human/exec" + color: "e99695" + exclusive: false + description: "👤 這張票的『執行者』是人(GUI-only/實體權限/法定簽署),不是審核。交付物是環境狀態改變,不是 PR" + + # ── 優先序(exclusive)─────────────────────────────── + - name: "p/high" + color: "b60205" + exclusive: true + description: "高——擋住交付或有時間壓力" + - name: "p/low" + color: "bfd4f2" + exclusive: true + description: "低——想做,但晚一點沒關係" + + # ── 種類(exclusive)──────────────────────────────── + - name: "type/bug" + color: "d73a4a" + exclusive: true + description: "壞掉的東西" + - name: "type/feature" + color: "a2eeef" + exclusive: true + description: "新能力" + - name: "type/governance" + color: "5319e7" + exclusive: true + description: "規範、流程、閘本身" + - name: "type/chore" + color: "fef2c0" + exclusive: true + description: "雜務、整理、不改行為" + + # ── 結構(正交)────────────────────────────────── + - name: "hub" + color: "0052cc" + exclusive: false + description: "Tracking issue——聚合一批 leaf 的 scope 容器,本身不掛 milestone、不對應 PR" + +# 舊標籤:不刪(刪掉會從既有票上被摘除,歷史就斷了),改標記封存 +archive: + - name: "duplicate" + reason: "由 close/duplicate 取代" diff --git a/scripts/gitea-labels-sync.sh b/scripts/gitea-labels-sync.sh new file mode 100755 index 0000000..985e259 --- /dev/null +++ b/scripts/gitea-labels-sync.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# gitea-labels-sync.sh — 把 labels.yaml(唯一真相源)同步到 inkstone org 底下的受治理 repo。 +# +# 用法: +# scripts/gitea-labels-sync.sh # 同步 org 內全部 repo(自動列舉) +# scripts/gitea-labels-sync.sh InkStoneCo Arcrun # 只同步指定的 repo +# DRY_RUN=1 scripts/gitea-labels-sync.sh # 只印計畫,不寫入 +# +# 規則(見票 inkstone/ISEP#4): +# - 名稱在 labels.yaml 裡但 repo 沒有 → 建立 +# - 名稱兩邊都有但 color/description/exclusive 不同 → 更新(PATCH) +# - repo 有、labels.yaml(含 archive 段)都沒有的名稱 → 只印告警,絕不刪除 +# - labels.yaml 的 archive 段:已知的舊名,不視為「非規範」,但也不會被建立/更新 +# - 冪等:兩次連跑,第二次一定是 0 created / 0 updated +# +# 依賴:curl, jq, python3(+pyyaml) +# Token 來源:與 InkStoneCo 頂層同一顆 gitea remote 密碼(不落地、不進 commit) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +LABELS_YAML="${LABELS_YAML:-$REPO_ROOT/labels.yaml}" +API_BASE="https://git.uncle6.me/api/v1" +ORG="inkstone" +DRY_RUN="${DRY_RUN:-0}" + +if [[ ! -f "$LABELS_YAML" ]]; then + echo "找不到真相源:$LABELS_YAML" >&2 + exit 1 +fi + +# --- token:跟 InkStoneCo 頂層 gitea remote 拿同一把(不落地檔案、不印出來) --- +TOKEN="${GITEA_TOKEN:-}" +if [[ -z "$TOKEN" ]]; then + INKSTONE_TOP="${INKSTONE_TOP:-$HOME/Documents/tech_projects/InkStoneCo}" + if [[ -d "$INKSTONE_TOP/.git" ]]; then + TOKEN="$(git -C "$INKSTONE_TOP" remote get-url gitea 2>/dev/null | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|')" + fi +fi +if [[ -z "$TOKEN" ]]; then + echo "拿不到 Gitea token——設 GITEA_TOKEN 環境變數,或確認 $INKSTONE_TOP 的 gitea remote 存在" >&2 + exit 1 +fi + +# --- labels.yaml → JSON(一次轉換,供後面反覆查詢用) --- +LABELS_JSON="$(python3 -c " +import yaml, json, sys +with open('$LABELS_YAML') as f: + d = yaml.safe_load(f) +print(json.dumps(d)) +")" + +REPO_NAMES=("$@") +if [[ ${#REPO_NAMES[@]} -eq 0 ]]; then + # macOS 內建 bash 是 3.2,沒有 mapfile/readarray,改用相容寫法 + REPO_NAMES=() + while IFS= read -r line; do + [[ -n "$line" ]] && REPO_NAMES+=("$line") + done < <(curl -sf -H "Authorization: token $TOKEN" \ + "$API_BASE/orgs/$ORG/repos?limit=50" | python3 -c " +import json, sys +for r in json.load(sys.stdin): + print(r['name']) +") +fi + +echo "受治理 repo(${#REPO_NAMES[@]} 個):${REPO_NAMES[*]}" +echo "DRY_RUN=$DRY_RUN" +echo "================================================================" + +TOTAL_CREATED=0 +TOTAL_UPDATED=0 +declare -a SUMMARY_LINES=() + +for REPO in "${REPO_NAMES[@]}"; do + echo "" + echo "── $ORG/$REPO ──────────────────────────────────────────" + + EXISTING_JSON="$(curl -sf -H "Authorization: token $TOKEN" \ + "$API_BASE/repos/$ORG/$REPO/labels?limit=50")" + + # 用 python3 一次算出這個 repo 的 create/update/warn 計畫(純比對,不動網路) + # 用暫存檔傳資料給 python(避免把含 emoji/引號的 JSON 內嵌進 -c 字串源碼裡出錯) + LABELS_TMP="$(mktemp)" + EXISTING_TMP="$(mktemp)" + printf '%s' "$LABELS_JSON" > "$LABELS_TMP" + printf '%s' "$EXISTING_JSON" > "$EXISTING_TMP" + PLAN_JSON="$(python3 -c " +import json, sys + +with open('$LABELS_TMP', encoding='utf-8') as f: + labels_def = json.load(f) +with open('$EXISTING_TMP', encoding='utf-8') as f: + existing = json.load(f) + +canonical = labels_def.get('labels', []) +archived_names = {a['name'] for a in labels_def.get('archive', [])} +canonical_by_name = {l['name']: l for l in canonical} +existing_by_name = {l['name']: l for l in existing} + +to_create = [] +to_update = [] +for name, want in canonical_by_name.items(): + have = existing_by_name.get(name) + if have is None: + to_create.append(want) + continue + diff = {} + if have.get('color', '').lstrip('#').lower() != want['color'].lstrip('#').lower(): + diff['color'] = want['color'] + if (have.get('description') or '') != (want.get('description') or ''): + diff['description'] = want.get('description', '') + if bool(have.get('exclusive', False)) != bool(want.get('exclusive', False)): + diff['exclusive'] = bool(want.get('exclusive', False)) + if diff: + to_update.append({'id': have['id'], 'name': name, 'diff': diff}) + +extras = [] +for name in existing_by_name: + if name not in canonical_by_name and name not in archived_names: + extras.append(name) + +print(json.dumps({'create': to_create, 'update': to_update, 'extras': extras})) +")" + rm -f "$LABELS_TMP" "$EXISTING_TMP" + + N_CREATE="$(echo "$PLAN_JSON" | jq '.create | length')" + N_UPDATE="$(echo "$PLAN_JSON" | jq '.update | length')" + N_EXTRA="$(echo "$PLAN_JSON" | jq '.extras | length')" + + if [[ "$N_CREATE" -gt 0 ]]; then + echo "$PLAN_JSON" | jq -c '.create[]' | while read -r item; do + NAME="$(echo "$item" | jq -r '.name')" + echo " + 建立: $NAME" + if [[ "$DRY_RUN" != "1" ]]; then + BODY="$(echo "$item" | jq '{name, color, description: (.description // ""), exclusive: (.exclusive // false)}')" + curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ + -d "$BODY" "$API_BASE/repos/$ORG/$REPO/labels" > /dev/null + fi + done + fi + + if [[ "$N_UPDATE" -gt 0 ]]; then + echo "$PLAN_JSON" | jq -c '.update[]' | while read -r item; do + NAME="$(echo "$item" | jq -r '.name')" + ID="$(echo "$item" | jq -r '.id')" + DIFF="$(echo "$item" | jq -c '.diff')" + echo " ~ 更新: $NAME ($DIFF)" + if [[ "$DRY_RUN" != "1" ]]; then + curl -sf -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ + -d "$DIFF" "$API_BASE/repos/$ORG/$REPO/labels/$ID" > /dev/null + fi + done + fi + + if [[ "$N_EXTRA" -gt 0 ]]; then + echo " ⚠ 非規範標籤(不動手,只告警):$(echo "$PLAN_JSON" | jq -r '.extras | join(", ")')" + fi + + echo " 小計:+$N_CREATE created, ~$N_UPDATE updated, ⚠$N_EXTRA extra" + SUMMARY_LINES+=("$REPO|$N_CREATE|$N_UPDATE|$N_EXTRA") + TOTAL_CREATED=$((TOTAL_CREATED + N_CREATE)) + TOTAL_UPDATED=$((TOTAL_UPDATED + N_UPDATE)) +done + +echo "" +echo "================================================================" +echo "總計:${TOTAL_CREATED} created, ${TOTAL_UPDATED} updated(跨 ${#REPO_NAMES[@]} 個 repo)" +echo "" +printf "%-28s %8s %8s %8s\n" "repo" "created" "updated" "extras" +for line in "${SUMMARY_LINES[@]}"; do + IFS='|' read -r r c u e <<< "$line" + printf "%-28s %8s %8s %8s\n" "$r" "$c" "$u" "$e" +done -- 2.52.0