#!/bin/sh # gitea-bootstrap.sh — 新 repo 開通工作管理元件(狀態標籤)。冪等,重跑無害。 # # 為什麼是腳本不是一段叮嚀(leo 2026-08-09:「每到一個新的 repo 就要建立這些管理元件」): # 靠人/靠 AI 記得 = 必然漂移。今天一天就抓到三條「規則寫了沒人驗」。 # label 是 repo-scoped、沒有跨 repo 繼承 ⇒ 每個 repo 都要建一次 ⇒ 必須一行做完。 # # 用法(在該 repo 目錄下跑,或用 -r 指定): # scripts/gitea-bootstrap.sh # 用當前 repo 的 gitea remote # scripts/gitea-bootstrap.sh -r Leo/some-repo # 指定 repo(token 仍從當前 repo 取) # # milestone 不在這裡建——milestone = sprint = 一次交貨,每個 repo 的交貨內容不同, # 不能預設。建法見 /issue-handle skill。 set -eu REPO="" [ "${1:-}" = "-r" ] && { REPO="${2:?-r 後面要接 owner/repo}"; } REMOTE=$(git remote get-url gitea 2>/dev/null) || { echo "✗ 當前目錄沒有名為 gitea 的 remote。請 cd 到目標 repo,或先加 remote。" >&2; exit 1; } TOKEN=$(printf '%s' "$REMOTE" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') [ "$TOKEN" = "$REMOTE" ] && { echo "✗ gitea remote URL 裡沒有 token,無法取得認證。" >&2; exit 1; } HOST=$(printf '%s' "$REMOTE" | sed -E 's|.*@([^/]+)/.*|\1|') [ -n "$REPO" ] || REPO=$(printf '%s' "$REMOTE" | sed -E 's|.*://[^/]+/(.+)\.git$|\1|') echo "→ repo: $REPO host: $HOST" # 狀態機(互斥 scope label)。順序=出貨流程: # s/todo → s/doing → s/stage →(leo 蓋章 + arm + 推 prod)→ closed # 🔴 不建 s/done:closed 就是 done,多一個就是同一件事兩個真相。 # 🔴 s/triage 存在的理由:**「留白」查不出來**。撈得到 s/todo,卻撈不到 # 「所有還沒驗傷的」——沒有標籤不是一種狀態,是查詢的死角 # (leo 2026-08-09 建 Triage 看板時暴露的設計缺陷)。 # p/ 是另一個軸:s/ 答「走到哪」、p/ 答「多重要」—— # s/backlog 的東西也可以是 p/high,併成一組就表達不出來。 set -- \ 's/triage|d4c5f9|新進來的,還沒驗傷——還沒決定要不要做' \ 's/backlog|c2e0c6|驗過了、確定要做,但還沒排進任何 sprint(wishlist/功能需求/待規劃)' \ 's/todo|ededed|已排進 sprint,等開工' \ 's/doing|0e8a16|進行中——現在有人在做' \ 's/stage|5319e7|已推上 stage,等 leo 去 youlin 的 stage 環境驗收(出貨流程第⑤步)' \ 's/pending|fbca04|卡住——等外部/等人,不是沒人做' \ 'p/high|b60205|高——擋住交付或有時間壓力' \ 'p/low|bfd4f2|低——想做,但晚一點沒關係' # 🔴 先抓現有清單再建。**Gitea 允許同名 label、回 201 不是 422** # ⇒ 靠「重複會被擋」達成冪等是錯的。2026-08-09 實撞:本腳本第一版 # 在 arcrun-rag 造出每個標籤各兩份——那會直接弄壞互斥狀態機(同名兩個 id, # 貼哪一個都不會把另一個頂掉),事後手動刪掉四個重複 id 才救回來。 EXISTING=$(curl -s -H "Authorization: token $TOKEN" -H "Cache-Control: no-cache" \ "https://$HOST/api/v1/repos/$REPO/labels?limit=100" \ | python3 -c "import json,sys;print(' '.join(l['name'] for l in json.load(sys.stdin)))") created=0; existed=0; failed=0 for spec in "$@"; do name=${spec%%|*}; rest=${spec#*|}; color=${rest%%|*}; desc=${rest#*|} case " $EXISTING " in *" $name "*) echo " · 已存在 $name"; existed=$((existed+1)); continue ;; esac payload=$(NAME="$name" COLOR="$color" DESC="$desc" python3 -c ' import json,os print(json.dumps({"name":os.environ["NAME"],"color":"#"+os.environ["COLOR"], "description":os.environ["DESC"],"exclusive":True}))') code=$(printf '%s' "$payload" | curl -s -o /tmp/.bootstrap-out -w '%{http_code}' \ -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ --data-binary @- "https://$HOST/api/v1/repos/$REPO/labels") case "$code" in 201) echo " ✓ 建立 $name"; created=$((created+1)) ;; 422) echo " · 已存在 $name"; existed=$((existed+1)) ;; *) echo " ✗ $name → HTTP $code: $(head -c 120 /tmp/.bootstrap-out)"; failed=$((failed+1)) ;; esac done # 複驗:從 repo 端讀回來,不信自己送出的指令(2026-08-09 教訓:宣告 vs 證據) echo "→ 複驗(從 repo 讀回):" curl -s -H "Authorization: token $TOKEN" -H "Cache-Control: no-cache" \ "https://$HOST/api/v1/repos/$REPO/labels?limit=100" \ | python3 -c "import json,sys;print(' ',sorted(l['name'] for l in json.load(sys.stdin) if l['name'].startswith('s/')))" echo "→ 新建 $created/已存在 $existed/失敗 $failed" [ "$failed" -eq 0 ] || exit 1