54e479900a
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), 按規約只告警不刪。
176 lines
6.6 KiB
Bash
Executable File
176 lines
6.6 KiB
Bash
Executable File
#!/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
|