#!/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