76adc749b0
c9128 複驗指出:雲端那次只改了 6 支 shell 的 sed 寫法,漏了 8 支 Python 的 re.search(r"//[^:/]+:([^@]+)@") 寫法。remote 清乾淨後這 8 支會靜默拿到 空字串 → 401,正是票上 2026-08-13「被讀成找不到/不存在」那種壞法;雲端有 環境變數所以測不出本機(只有 .env)的壞法。 - 新增 hooks/lib/gitea_token.py:Python 端唯一 token 來源,與 scripts/lib/gitea-token.sh 同一解析順序(env → $CLAUDE_PROJECT_DIR/.env → git 根的 .env),絕不從 remote URL 抽。 - 8 支改用它:scripts/ticket、debt-worklist、milestone-account、pr-verdict、 release-manifest、isep-nag、isep-notify、hooks/lib/release_chain.py。 release_chain 的 import 做成防禦式(旁邊沒有 gitea_token.py 也能載入, 讀 tag/release 一律匿名),token_from_env_or_remote 保留簽章、改走新來源。 - 測試:新增 scripts/test-gitea-token.sh(16 條,Python+shell 兩份實作、 解析順序、髒 remote 不准解出、8 支都走得過 token 步);docs/TESTING.md A44。 beacon 測試的 mkplugin 一併複製 gitea_token.py(release_chain 現在會 import 它)。 驗過(remote 乾淨+無環境變數+.env 有 token):ticket mine/isep-nag/ isep-notify/milestone-account/pr-verdict/debt-worklist 都拿得到 token、 走得過 token 步。回歸:test-gitea-token 16/16、milestone-account 23/23、 release-manifest 21/21、debt-worklist 47/47、release-check 14/14、 release-ship 14/14、ticket-pick 84/84、ticket-triage 80/80、 ticket-repo-arg 7/7、ticket-handoff-writeback 49/49、 ticket-where-seen 17/17、isep-notify-botapi 10/10、 beacon 33/33、pr-verdict-guard 52/52。 README scripts 數在本樹實數=68(main 66→branch 67 加 git-credential-gitea.sh →本次 68 加 test-gitea-token.sh;README 舊值 65 在 main 上就已 stale)。 hooks 仍 62,hooks-inventory.md 不動。 版本號待總管定(改到的是會被載入的 hooks/lib 與 scripts,需升版才會傳到安裝的那份)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwZx8MkV84Cz2Mf4sDU4if
68 lines
2.6 KiB
Python
68 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
||
"""gitea_token.py — 解出 Gitea 憑證(GITEA_TOKEN_CLAUDE_CODE)的唯一 Python 來源。
|
||
|
||
為什麼存在(inkstone/ISEP#47):舊法用 `git remote -v` +
|
||
`re.search(r"//[^:/]+:([^@]+)@")` 從 remote URL 抽 token ⇒ token 必須明文嵌在
|
||
URL 裡 ⇒ 任何印出 remote URL 的訊息(git-lfs 的 locksverify 那行)都會把它洩進
|
||
transcript/log。來源統一到這裡後,remote URL 才能改乾淨。
|
||
|
||
這是 `scripts/lib/gitea-token.sh` 的 Python 對照——**順序刻意一致**,讓 shell 與
|
||
Python 兩邊解出來的是同一把(兩份必然漂移,所以只留一條路:先 env、再 .env)。
|
||
|
||
解析順序(先環境、後檔案——雲端只有 env、本機有 .env,兩邊都涵蓋):
|
||
1) 環境變數 GITEA_TOKEN_CLAUDE_CODE,再退 GITEA_TOKEN(本機 source .env 後也在)
|
||
2) $CLAUDE_PROJECT_DIR/.env 的 GITEA_TOKEN_CLAUDE_CODE=
|
||
3) git 根目錄的 .env 的 GITEA_TOKEN_CLAUDE_CODE=
|
||
讀不到 → 回 None(呼叫者自己判空,不能把 None 當成功)。
|
||
|
||
🔴 只回值、不印別的——絕不把 token 印進 log。
|
||
🔴 不從 remote URL 抽 token——那正是 ISEP#47 要根除的洩漏源。
|
||
"""
|
||
import os
|
||
import subprocess
|
||
|
||
|
||
def _from_file(path):
|
||
"""從一個 .env 檔抓 GITEA_TOKEN_CLAUDE_CODE=,去掉外圍引號。抓不到回 None。"""
|
||
try:
|
||
with open(path, encoding="utf-8", errors="replace") as f:
|
||
for line in f:
|
||
if line.startswith("GITEA_TOKEN_CLAUDE_CODE="):
|
||
v = line.split("=", 1)[1].strip()
|
||
if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'):
|
||
v = v[1:-1]
|
||
return v or None
|
||
except OSError:
|
||
pass
|
||
return None
|
||
|
||
|
||
def _git_root():
|
||
"""git rev-parse --show-toplevel(跟 shell 版同一把尺)。拿不到回 None。"""
|
||
try:
|
||
out = subprocess.run(["git", "rev-parse", "--show-toplevel"],
|
||
capture_output=True, text=True, timeout=20)
|
||
root = (out.stdout or "").strip()
|
||
return root or None
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def gitea_token():
|
||
"""回 Gitea token 字串,或 None(拿不到)。順序見模組 docstring。"""
|
||
for env in ("GITEA_TOKEN_CLAUDE_CODE", "GITEA_TOKEN"):
|
||
v = os.environ.get(env)
|
||
if v:
|
||
return v
|
||
pd = os.environ.get("CLAUDE_PROJECT_DIR")
|
||
if pd:
|
||
v = _from_file(os.path.join(pd, ".env"))
|
||
if v:
|
||
return v
|
||
root = _git_root()
|
||
if root:
|
||
v = _from_file(os.path.join(root, ".env"))
|
||
if v:
|
||
return v
|
||
return None
|