daa84b583f
- 6 hooks + gitea-labels-sync/wiki-panorama/gitea-bootstrap 全改用 gitea_token() - wiki-panorama clone/fetch 改乾淨 URL + credential helper - gitea-bootstrap 冪等清 remote URL + 設 credential.helper - 驗證:bootstrap/labels-sync API 認證通過、clone/push 經 helper 無 token 洩漏 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kxf68FcYGqiuLAPiKKU2U3
96 lines
5.9 KiB
Bash
Executable File
96 lines
5.9 KiB
Bash
Executable File
#!/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}"; }
|
||
|
||
# 取當前 repo 的 remote(雲端叫 origin、本機慣例叫 gitea)
|
||
REMOTE_NAME=$(git remote get-url gitea >/dev/null 2>&1 && echo gitea || (git remote get-url origin >/dev/null 2>&1 && echo origin))
|
||
[ -n "$REMOTE_NAME" ] || { echo "✗ 當前目錄沒有 gitea/origin remote。請 cd 到目標 repo,或先加 remote。" >&2; exit 1; }
|
||
REMOTE=$(git remote get-url "$REMOTE_NAME" 2>/dev/null)
|
||
# 去掉網址裡可能嵌著的憑證 → 乾淨 URL(inkstone/ISEP#47:token 不進 remote URL)
|
||
REMOTE=$(printf '%s' "$REMOTE" | sed -E 's#//[^/@]*@#//#')
|
||
|
||
# token 來源=env/.env(不從 remote URL 抽——那會逼 token 明文嵌在 URL 裡 → 洩進 log)
|
||
_GT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||
[ -f "$_GT_DIR/lib/gitea-token.sh" ] && . "$_GT_DIR/lib/gitea-token.sh"
|
||
TOKEN=""; command -v gitea_token >/dev/null 2>&1 && TOKEN=$(gitea_token || true)
|
||
[ -n "$TOKEN" ] || { echo "✗ 取不到 Gitea token(設 GITEA_TOKEN_CLAUDE_CODE,或在 .env 放 GITEA_TOKEN_CLAUDE_CODE=)。" >&2; exit 1; }
|
||
HOST=$(printf '%s' "$REMOTE" | sed -E 's|.*://([^/]+)/.*|\1|')
|
||
[ -n "$REPO" ] || REPO=$(printf '%s' "$REMOTE" | sed -E 's|.*://[^/]+/(.+)\.git$|\1|')
|
||
|
||
# 設定乾淨 remote URL + credential helper(冪等):URL 不含 token,認證改由 helper 從 env/.env 供
|
||
# ⇒ git 把 remote URL 印出來(git-lfs 的 locksverify 那行)也不會洩 token(inkstone/ISEP#47)。
|
||
if [ -f "$_GT_DIR/git-credential-gitea.sh" ]; then
|
||
CUR=$(git remote get-url "$REMOTE_NAME" 2>/dev/null || true)
|
||
case "$CUR" in *@*) git remote set-url "$REMOTE_NAME" "$REMOTE" && echo " ✓ remote $REMOTE_NAME 已清乾淨(移除嵌入的 token)" ;; esac
|
||
git config credential.helper "$_GT_DIR/git-credential-gitea.sh"
|
||
fi
|
||
|
||
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
|