Files
ISEP/scripts/gitea-labels-sync.sh
T
Leo 54e479900a 新增標籤唯一真相源 + 冪等同步腳本,全 org 14 個 repo 對齊(inkstone/ISEP#4)
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),
按規約只告警不刪。
2026-08-20 13:03:12 +08:00

176 lines
6.6 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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