From daa84b583feb5777cf3b89fcfc034fafc161e7ba Mon Sep 17 00:00:00 2001 From: cloud-worker Date: Fri, 18 Sep 2026 12:36:30 +0000 Subject: [PATCH] ISEP#47: source Gitea token from env/.env, not remote URL (9 scripts + credential helper) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01Kxf68FcYGqiuLAPiKKU2U3 --- hooks/baton-handback-guard.sh | 16 ++++++---------- hooks/factory-idle-guard.sh | 5 ++++- hooks/issue-status-autoflip.sh | 17 ++++++----------- hooks/no-ticket-no-dispatch.sh | 10 ++++++---- hooks/pr-verdict-guard.sh | 13 ++++--------- hooks/ticket-api-bypass-guard.sh | 8 ++++---- scripts/gitea-bootstrap.sh | 25 ++++++++++++++++++++----- scripts/gitea-labels-sync.sh | 11 +++++------ scripts/wiki-panorama.sh | 22 ++++++++++++++-------- 9 files changed, 69 insertions(+), 58 deletions(-) diff --git a/hooks/baton-handback-guard.sh b/hooks/baton-handback-guard.sh index 36a0731..dd5ff3b 100755 --- a/hooks/baton-handback-guard.sh +++ b/hooks/baton-handback-guard.sh @@ -149,16 +149,12 @@ check_one(){ # $1 owner $2 repo $3 num J=$(cat "$BATON_GUARD_FIXTURE/issue.json") C=$(cat "$BATON_GUARD_FIXTURE/comments.json") else - # token:掃已知的 checkout 找第一個帶憑證、指向本站的 remote(同 scripts/ticket 的做法) - local ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" URL="" U dir TOKEN - for dir in "$ROOT" "$ROOT/products/$REPO" "$ROOT/matrix/$REPO" "$ROOT/polaris/$REPO"; do - [ -e "$dir/.git" ] || continue - U=$(git -C "$dir" remote -v 2>/dev/null | grep -m1 'git\.uncle6\.me' | grep '@' | awk '{print $2}') || true - [ -n "${U:-}" ] && { URL="$U"; break; } - done - [ -n "$URL" ] || return 0 - TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') - [ "$TOKEN" != "$URL" ] || return 0 + # token:來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽(那會逼 token 明文嵌在 URL 裡) + local TOKEN _GT_LIB + _GT_LIB="$(dirname -- "$0")/../scripts/lib/gitea-token.sh"; [ -f "$_GT_LIB" ] && . "$_GT_LIB" + command -v gitea_token >/dev/null 2>&1 || return 0 + TOKEN=$(gitea_token) || return 0 + [ -n "$TOKEN" ] || return 0 J=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \ "https://git.uncle6.me/api/v1/repos/$OWNER/$REPO/issues/$NUM" 2>/dev/null) || return 0 C=$(curl -s --max-time 12 -H "Authorization: token $TOKEN" \ diff --git a/hooks/factory-idle-guard.sh b/hooks/factory-idle-guard.sh index 952e3b2..5cbe964 100755 --- a/hooks/factory-idle-guard.sh +++ b/hooks/factory-idle-guard.sh @@ -294,7 +294,10 @@ for d in /private/tmp/claude-501/*/*/tasks; do done [ "$RUNNING" -gt 0 ] && exit 0 -TOKEN=$(git -C "$PROJ" remote get-url gitea 2>/dev/null | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') || TOKEN="" +# token 來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽(那會逼 token 明文嵌在 URL 裡) +_GT_LIB="$(dirname -- "$0")/../scripts/lib/gitea-token.sh"; [ -f "$_GT_LIB" ] && . "$_GT_LIB" +command -v gitea_token >/dev/null 2>&1 || exit 0 +TOKEN=$(gitea_token) || TOKEN="" [ -z "$TOKEN" ] && exit 0 # 🔴 org 是 inkstone,不是 Leo(2026-08-13 搬遷;舊值讓本閘靜默失效到 08-16) diff --git a/hooks/issue-status-autoflip.sh b/hooks/issue-status-autoflip.sh index 7b6fe52..a206724 100755 --- a/hooks/issue-status-autoflip.sh +++ b/hooks/issue-status-autoflip.sh @@ -43,17 +43,12 @@ for line in p.splitlines(): set -- $SPEC OWNER="$1"; REPO="$2"; NUM="$3" -# token 從該 repo 的 gitea remote 取;取不到就安靜結束(不影響派工) -for d in "$CLAUDE_PROJECT_DIR/products/$REPO" "$CLAUDE_PROJECT_DIR/matrix/$REPO" "$CLAUDE_PROJECT_DIR"; do - [ -d "$d/.git" ] || continue - URL=$(git -C "$d" remote get-url gitea 2>/dev/null) || continue - case "$URL" in *@*) break ;; esac -done -[ -n "${URL:-}" ] || exit 0 - -TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') -HOST=$(printf '%s' "$URL" | sed -E 's|.*@([^/]+)/.*|\1|') -[ "$TOKEN" != "$URL" ] || exit 0 +# token 來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽;取不到就安靜結束(不影響派工) +_GT_LIB="$(dirname -- "$0")/../scripts/lib/gitea-token.sh"; [ -f "$_GT_LIB" ] && . "$_GT_LIB" +command -v gitea_token >/dev/null 2>&1 || exit 0 +TOKEN=$(gitea_token) || exit 0 +[ -n "$TOKEN" ] || exit 0 +HOST="git.uncle6.me" curl -s -o /dev/null --max-time 8 \ -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ diff --git a/hooks/no-ticket-no-dispatch.sh b/hooks/no-ticket-no-dispatch.sh index e9ac1e5..d95c3ca 100755 --- a/hooks/no-ticket-no-dispatch.sh +++ b/hooks/no-ticket-no-dispatch.sh @@ -79,10 +79,12 @@ set -- $REF OWNER="$1"; REPO="$2"; NUM="$3" # 拿 token(拿不到就放行——這是本地環境問題,不是派工者的錯) -URL=$(git -C "${CLAUDE_PROJECT_DIR:-.}" remote get-url gitea 2>/dev/null || true) -case "${URL:-}" in *@*) ;; *) exit 0 ;; esac -TOKEN=$(printf '%s' "$URL" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') -HOST=$(printf '%s' "$URL" | sed -E 's|.*@([^/]+)/.*|\1|') +# token 來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽(那會逼 token 明文嵌在 URL 裡) +_GT_LIB="$(dirname -- "$0")/../scripts/lib/gitea-token.sh"; [ -f "$_GT_LIB" ] && . "$_GT_LIB" +command -v gitea_token >/dev/null 2>&1 || exit 0 +TOKEN=$(gitea_token) || exit 0 +[ -n "$TOKEN" ] || exit 0 +HOST="git.uncle6.me" RESP=$(curl -s --max-time 15 -H "Authorization: token $TOKEN" \ "https://$HOST/api/v1/repos/$OWNER/$REPO/issues/$NUM" 2>/dev/null || true) diff --git a/hooks/pr-verdict-guard.sh b/hooks/pr-verdict-guard.sh index 574fba8..89f85ce 100644 --- a/hooks/pr-verdict-guard.sh +++ b/hooks/pr-verdict-guard.sh @@ -62,17 +62,12 @@ set -u PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}" PAYLOAD=$(cat 2>/dev/null || echo '{}') -# ── token:同 scripts/ticket 的做法(掃 remote 找帶憑證的,再退到環境變數)── +# ── token:來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽 ── +# (舊法掃各 repo 的 remote 找帶憑證的網址,代表 token 得明文嵌在 URL 裡 → 會洩進 log) TOKEN="" if [ -z "${PR_VERDICT_FIXTURE:-}" ]; then - for d in "$PROJ" "$PROJ/products"/* "$PROJ/matrix"/* "$PROJ/polaris"/*; do - [ -e "$d/.git" ] || continue - U=$(git -C "$d" remote -v 2>/dev/null | grep -m1 'git\.uncle6\.me' | grep '@' | awk '{print $2}') || true - if [ -n "${U:-}" ]; then - T=$(printf '%s' "$U" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') - [ "$T" != "$U" ] && { TOKEN="$T"; break; } - fi - done + _GT_LIB="$(dirname -- "$0")/../scripts/lib/gitea-token.sh"; [ -f "$_GT_LIB" ] && . "$_GT_LIB" + command -v gitea_token >/dev/null 2>&1 && TOKEN=$(gitea_token) || TOKEN="" [ -n "$TOKEN" ] || TOKEN="${GITEA_TOKEN_CLAUDE_CODE:-${GITEA_TOKEN:-}}" fi diff --git a/hooks/ticket-api-bypass-guard.sh b/hooks/ticket-api-bypass-guard.sh index b5cf7e2..05ed380 100755 --- a/hooks/ticket-api-bypass-guard.sh +++ b/hooks/ticket-api-bypass-guard.sh @@ -116,10 +116,10 @@ fi # 不影響「擋下來」這個判斷本身。 HITS="" if [ "$RESOURCE" = "issues" ]; then - REMOTES=$(git -C "${CLAUDE_PROJECT_DIR:-.}" remote -v 2>/dev/null || true) - CRED_LINE=$(printf '%s' "$REMOTES" | grep -m1 'git\.uncle6\.me' | grep -m1 '@' || true) - if [ -n "$CRED_LINE" ]; then - TOKEN=$(printf '%s' "$CRED_LINE" | sed -E 's|.*//[^:]+:([^@]+)@.*|\1|') + # token 來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽(那會逼 token 明文嵌在 URL 裡) + _GT_LIB="$(dirname -- "$0")/../scripts/lib/gitea-token.sh"; [ -f "$_GT_LIB" ] && . "$_GT_LIB" + TOKEN=""; command -v gitea_token >/dev/null 2>&1 && TOKEN=$(gitea_token) || TOKEN="" + if [ -n "$TOKEN" ]; then # 只認雙引號的 "title": "..."(curl -d/json.dumps 輸出的實際格式,也是 # 2026-08-27 那 13 張票真實用的寫法,見 scripts/test-ticket-api-bypass-guard.sh # 既有的模擬指令)。刻意不支援單引號版本——這段 python 要塞進 bash 單引號 diff --git a/scripts/gitea-bootstrap.sh b/scripts/gitea-bootstrap.sh index 487e832..2b67fe5 100755 --- a/scripts/gitea-bootstrap.sh +++ b/scripts/gitea-bootstrap.sh @@ -16,14 +16,29 @@ 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; } +# 取當前 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=$(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|') +# 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)。順序=出貨流程: diff --git a/scripts/gitea-labels-sync.sh b/scripts/gitea-labels-sync.sh index 985e259..e4665ed 100755 --- a/scripts/gitea-labels-sync.sh +++ b/scripts/gitea-labels-sync.sh @@ -30,16 +30,15 @@ if [[ ! -f "$LABELS_YAML" ]]; then exit 1 fi -# --- token:跟 InkStoneCo 頂層 gitea remote 拿同一把(不落地檔案、不印出來) --- +# --- token:來源=env/.env(inkstone/ISEP#47),不再從 remote URL 抽(那會逼 token 明文嵌在 URL 裡) --- 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 + _GT_LIB="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/lib/gitea-token.sh" + [[ -f "$_GT_LIB" ]] && . "$_GT_LIB" + command -v gitea_token >/dev/null 2>&1 && TOKEN="$(gitea_token || true)" fi if [[ -z "$TOKEN" ]]; then - echo "拿不到 Gitea token——設 GITEA_TOKEN 環境變數,或確認 $INKSTONE_TOP 的 gitea remote 存在" >&2 + echo "拿不到 Gitea token——設 GITEA_TOKEN_CLAUDE_CODE 環境變數,或在 .env/頂層 .env 放 GITEA_TOKEN_CLAUDE_CODE=" >&2 exit 1 fi diff --git a/scripts/wiki-panorama.sh b/scripts/wiki-panorama.sh index 3d9a85c..f7fef1a 100644 --- a/scripts/wiki-panorama.sh +++ b/scripts/wiki-panorama.sh @@ -61,15 +61,21 @@ done [ -f "$ROSTER" ] || { echo "找不到 roster:$ROSTER" >&2; exit 1; } -# ── Gitea base(含憑證,**絕不可印出來**)──────────────────────────────── -REMOTE=$(git -C "$ROOT" remote get-url gitea 2>/dev/null || true) -[ -n "$REMOTE" ] || { echo "本 repo 沒有 gitea remote,無法取 repo。" >&2; exit 1; } +# ── Gitea base(乾淨網址;token 來源=env/.env,不從 URL 抽——inkstone/ISEP#47)────────── +_GT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +[ -f "$_GT_DIR/lib/gitea-token.sh" ] && . "$_GT_DIR/lib/gitea-token.sh" +REMOTE=$(git -C "$ROOT" remote get-url gitea 2>/dev/null || git -C "$ROOT" remote get-url origin 2>/dev/null || true) +[ -n "$REMOTE" ] || { echo "本 repo 沒有 gitea/origin remote,無法取 repo。" >&2; exit 1; } +# 去掉網址裡可能還嵌著的舊憑證(遷移期相容)→ 乾淨 base,clone/fetch 靠 credential helper 認證 +REMOTE=$(printf '%s' "$REMOTE" | sed -E 's#//[^/@]*@#//#') BASE=${REMOTE%/InkStoneCo.git} BASE=${BASE%/InkStoneCo} SELF=$(basename "$REMOTE" .git) -SECRET=$(printf '%s' "$REMOTE" | sed -nE 's|.*//[^:]+:([^@]+)@.*|\1|p') -# 任何要外流的字串都先過這關(錯誤訊息可能夾帶 clone URL) +# scrub 用的 SECRET:token 從 env/.env 取(不從 URL 抽),用來遮掉萬一夾帶在錯誤訊息裡的 token +SECRET=""; command -v gitea_token >/dev/null 2>&1 && SECRET=$(gitea_token || true) scrub() { if [ -n "$SECRET" ]; then sed "s|$SECRET|***|g"; else cat; fi; } +# clone/fetch 的認證:乾淨 URL + credential helper(token 不進 URL、不洩進 log) +GIT_CRED=""; [ -f "$_GT_DIR/git-credential-gitea.sh" ] && GIT_CRED="-c credential.helper=$_GT_DIR/git-credential-gitea.sh" mkdir -p "$CACHE" # 自己這個 repo 一律讀工作副本 ⇒ 快取裡若留著一份舊的自己,是誤導來源(會被人拿去讀) @@ -95,7 +101,7 @@ for repo in $REPOS; do if [ -d "$dst/.git" ]; then if [ "$DO_FETCH" = 1 ]; then br=$(git -C "$dst" symbolic-ref --short HEAD 2>/dev/null || echo main) - git -C "$dst" fetch --quiet origin "$br" 2>&1 | scrub >&2 || true + git $GIT_CRED -C "$dst" fetch --quiet origin "$br" 2>&1 | scrub >&2 || true git -C "$dst" reset --quiet --hard FETCH_HEAD 2>/dev/null || true fi else @@ -104,7 +110,7 @@ for repo in $REPOS; do fi # blobless + sparse:只下載 wiki 目錄的內容,但保留完整 commit 歷史 #(要歷史才算得出「每個檔最後更新是哪天」——淺 clone 會讓所有檔同一天) - if ! err=$(git clone --quiet --filter=blob:none --sparse "$BASE/$repo.git" "$dst" 2>&1 | scrub); then + if ! err=$(git $GIT_CRED clone --quiet --filter=blob:none --sparse "$BASE/$repo.git" "$dst" 2>&1 | scrub); then printf '%s\tclone-failed\t%s\n' "$repo" "$(printf '%s' "$err" | tr '\n' ' ')" >> "$STATUS_TSV" rm -rf "$dst"; continue fi @@ -312,7 +318,7 @@ if gap > 0: f"Gitea `Leo/*` 在 {ASOF} 實查是 **{TOTAL} 個**,名單 {len(repos)} 個 ⇒ **還有 {gap} 個沒被點名**。") w(" 補法(人發起,一次呼叫,不排程)——拿到完整清單、把缺的名字加進名單再重跑:") w(" ```") - w(" TOKEN=$(git remote get-url gitea | sed -E 's|.*//[^:]+:([^@]+)@.*|\\1|')") + w(" TOKEN=$GITEA_TOKEN_CLAUDE_CODE # 來源=env/.env,不從 remote URL 抽(inkstone/ISEP#47)") w(" curl -s -H \"Authorization: token $TOKEN\" \\") w(" 'https://git.uncle6.me/api/v1/orgs/Leo/repos?limit=100' | python3 -c \\") w(" 'import sys,json;[print(r[\"name\"]) for r in json.load(sys.stdin)]'")