ISEP#47: 補齊 8 支 Python 也從 env/.env 取 token,不再從 remote URL 抽
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
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
#!/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
|
||||
+14
-14
@@ -25,9 +25,16 @@ import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
try:
|
||||
from gitea_token import gitea_token as _gitea_token # 唯一 token 來源(ISEP#47):env→.env,不碰 remote URL
|
||||
except Exception: # 旁邊沒有 gitea_token.py(部分部署)也要能載入——讀 tag/release 一律匿名,不靠 token
|
||||
_gitea_token = None
|
||||
|
||||
DEFAULT_HOST = "https://git.uncle6.me"
|
||||
DEFAULT_REPO = "inkstone/ISEP"
|
||||
MANIFEST_PATH = ".claude-plugin/plugin.json"
|
||||
@@ -58,23 +65,16 @@ def tagname(v):
|
||||
|
||||
|
||||
def token_from_env_or_remote(host, root=None):
|
||||
"""建 release 才用得到。環境變數優先;沒有就從 remote URL 裡撈(bootstrap 會帶)。"""
|
||||
"""建 release(POST)才用得到。來源=env/.env(ISEP#47),**不再從 remote URL 抽**——
|
||||
那會逼 token 明文嵌在 URL 裡,任何印出 remote 的訊息都會洩一次。解析順序見
|
||||
hooks/lib/gitea_token.py(先 env、再 $CLAUDE_PROJECT_DIR/.env、最後 git 根的 .env)。
|
||||
`host`/`root` 參數保留給呼叫端相容,現已不參與解析。拿不到回 ""(呼叫者自己判空)。"""
|
||||
if _gitea_token is not None:
|
||||
return _gitea_token() or ""
|
||||
# 退路:gitea_token.py 不在旁邊——只讀 env(仍不碰 remote URL,不重現 #47 的洩漏源)
|
||||
for env in ("GITEA_TOKEN_CLAUDE_CODE", "GITEA_TOKEN"):
|
||||
if os.environ.get(env):
|
||||
return os.environ[env]
|
||||
if not root:
|
||||
return ""
|
||||
h = host.split("//")[-1].rstrip("/")
|
||||
try:
|
||||
out = subprocess.run(["git", "-C", root, "remote", "-v"],
|
||||
capture_output=True, text=True, timeout=20).stdout
|
||||
except Exception:
|
||||
out = ""
|
||||
for line in out.splitlines():
|
||||
if h in line:
|
||||
m = re.search(r"//[^:/]+:([^@]+)@", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user