出貨鏈三站上 main:release-ship 前置①改看 tag 的樹、release-check 可指定版本、信標開場報「打了 tag 沒 release」(inkstone/ISEP#67)

- hooks/lib/release_chain.py:問 Gitea 的判準一份三處用(匿名讀,D20)
- scripts/release-ship:前置①比「要建的那個 tag 的樹」裡的 plugin.json,不比工作樹/最新 tag
  (總管 09-07 在 v0.23.0 被擋的情境現在放行);note 從 Gitea compare 生,從 plugin 快取跑也行;
  覆核改跑 release-check <那個 tag>
- scripts/release-check [vX.Y.Z]:指定版本;v0.19/0.20/0.21 依裁決標「不補」不再黃字;補法印正本絕對路徑
- hooks/lib/beacon_report.py ④:最新 tag 沒 release/main 定版沒打 tag ⇒ 一行缺站+補法;都在安靜;
  只快取「都在」6 小時
- 測試:test-release-ship 14、test-release-check 14、信標 26→33;docs:TESTING A37/A38、盤點表、README 60 支腳本、
  白名單加 release-ship/release-check 形狀、mistakes 一條
- 沒改 plugin.json 版本號(待總管定版)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
This commit is contained in:
claude-code
2026-09-07 05:52:41 +00:00
parent 1fa45cd539
commit fef35ed03a
12 changed files with 717 additions and 332 deletions
+78 -114
View File
@@ -6,6 +6,8 @@ leo 2026-08-27 的病(票上原文):
「版本只到 ISEP v0.3.9,你有新版為什麼不推?」
v0.4.0v0.5.0 的 tag 早就推了,但 Gitea Releases 頁停在 v0.3.9
⇒ 對 leo 等於這兩版不存在;claude plugin list 也還顯示舊版。
2026-09-07 又一次:v0.23.0v0.24.0 打了 tagReleases 頁停在 v0.22.0——
交棒給 leo 的「測試位置 releases/tag/v0.24.0」打開是一頁空 tag。
ISEP 的運送鏈有這幾段,**每一段都要有人回頭問「到了嗎」**:
@@ -15,18 +17,25 @@ ISEP 的運送鏈有這幾段,**每一段都要有人回頭問「到了嗎」*
🔴 這支的全部價值在一句話:**它不聽上一站說「我送了」,它自己去問收貨的那一端。**
tag 在不在 → 問 Gitea 的 tagsrelease 在不在 → 問 Gitea 的 releasesleo 會看的那頁);
載入到了沒 → 看這個 session 真的掛著的那一份 plugin.json。
送的人(release-ship)說「成功」不算數——這支拿收貨端的事實覆核。
問的方式全部匿名(D20:讀不帶憑證),判準與 release-shipSessionStart 信標同一份
hooks/lib/release_chain.py)。
用法:
release-check 現行版本 這棵樹 plugin.json 宣稱的那一版
release-check v0.23.0 現行版本 你指定的那一版(release-ship 送完拿來覆核**那一版**
不管工作樹現在 checkout 在哪)
判準是**結構**,不是關鍵字(leo 2026-08-17 已證偽關鍵字黑名單:8 次誤攔、0 次正確):
· 哪些版本「應該」被送到 = main 上有 `定版 vX.Y.Z` commit 的那些(git 說了算,不是猜)
· 那個版本「送到了沒」= 去 Gitea 問它的 tag 與 release 在不在(在=到了)
離開碼:
0 現行版本plugin.json 宣稱的那一版)**tag 與 release 都在** ⇒ 送貨端完成
0 現行版本 **tag 與 release 都在** ⇒ 送貨端完成
2 現行版本缺 tag 或缺 release ⇒ **送貨端斷了**stderr 講出缺哪一站+怎麼補
(這正是工單驗收 4:「故意漏一步 → 管線要擋下來並說出缺哪一站」)
說明:比現行版本更舊的 `定版` 版本若缺 tag/release,只**黃字提醒**、不改離開碼——
它們多半已被新版取代,硬擋等於「懲罰謹慎」(over-block 比 under-block 嚴重)。
v0.190.200.21 就是總管 09-07 裁定「不補」的那三個。
載入端落後(這個 session 跑的比 main 舊)也只提醒:那要 update+重啟,
是收貨端的動作,不是送貨端能修的,且 SessionStart 信標本來就一直在報。
@@ -37,13 +46,18 @@ import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
HOST = os.environ.get("RELEASE_CHECK_HOST", "https://git.uncle6.me")
REPO = os.environ.get("RELEASE_CHECK_REPO", "inkstone/ISEP")
FIXTURE = os.environ.get("RELEASE_CHECK_FIXTURE", "")
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "hooks", "lib"))
try:
import release_chain as rc
except Exception as e: # pragma: no cover
print("🔴 讀不到 hooks/lib/release_chain.py%s" % e, file=sys.stderr)
sys.exit(2)
HOST = os.environ.get("RELEASE_CHECK_HOST", rc.DEFAULT_HOST)
REPO = os.environ.get("RELEASE_CHECK_REPO", rc.DEFAULT_REPO)
FIXTURE = os.environ.get("RELEASE_CHECK_FIXTURE", "")
def die(msg, code=2):
@@ -59,13 +73,9 @@ def proj_root():
return os.path.dirname(HERE)
def vtuple(v):
nums = re.findall(r"\d+", v or "")
return tuple(int(x) for x in nums[:3]) if nums else (0,)
# ══ 收貨端事實:全部從「別人」那裡拿,不聽送的人說 ═══════════════════════
def _fixture():
def load_fixture():
if not FIXTURE:
return None
try:
with open(FIXTURE) as f:
return json.load(f)
@@ -73,69 +83,22 @@ def _fixture():
die("🔴 讀不到 RELEASE_CHECK_FIXTURE%s" % FIXTURE)
def token():
for env in ("GITEA_TOKEN_CLAUDE_CODE", "GITEA_TOKEN"):
if os.environ.get(env):
return os.environ[env]
root = os.environ.get("CLAUDE_PROJECT_DIR") or proj_root()
host = 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 host in line:
m = re.search(r"//[^:/]+:([^@]+)@", line)
if m:
return m.group(1)
return ""
FX = load_fixture()
CHAIN = rc.Chain(HOST, REPO, fixture=FX)
def _api_names(path, key):
"""回一個 setGitea 上那個 collection 裡所有的名字(tag_namename)。"""
tk = token()
if not tk:
die("🔴 拿不到 %s 的 tokenremote 沒帶憑證,也沒有 GITEA_TOKEN_CLAUDE_CODE" % HOST)
names, page = set(), 1
while True:
url = "%s/api/v1/repos/%s/%s?limit=50&page=%d" % (HOST, REPO, path, page)
req = urllib.request.Request(url, headers={"Authorization": "token " + tk})
try:
rows = json.load(urllib.request.urlopen(req, timeout=25))
except urllib.error.HTTPError as e:
die("🔴 Gitea %s%s" % (e.code, e.read().decode()[:200]))
except Exception as e:
die("🔴 連不上 %s%s" % (HOST, e))
if not rows:
break
for r in rows:
n = r.get(key)
if n:
names.add(n)
if len(rows) < 50:
break
page += 1
return names
def gitea_tags():
if FIXTURE:
return set(_fixture().get("tags") or [])
return _api_names("tags", "name")
def gitea_releases():
if FIXTURE:
return set(_fixture().get("releases") or [])
return _api_names("releases", "tag_name")
# 🔴 裁定不補的歷史版本(唯一識別碼往放行方向比對,不是關鍵字):
# 總管 2026-09-07inkstone/ISEP#67 → comment 6574):「v0.190.200.21 不補——
# 沒 tagv0.22.0 的 note 已列到那三個定版。」這三個定版 commit 永遠在 main 上,
# 不列在這裡它們就會每次都黃字——天天念的東西會被學會忽略,等於不存在。
WAIVED = {"0.19.0": "inkstone/ISEP#67 → 6574", "0.20.0": "inkstone/ISEP#67 → 6574",
"0.21.0": "inkstone/ISEP#67 → 6574"}
# ══ 送貨端事實:main 上「定版」了哪些、現在宣稱哪一版、這個 session 載到哪一版 ══
def dingban_versions():
"""main 上有 `定版 vX.Y.Z` commit 的版本 —— 「應該被送到」的權威清單。"""
if FIXTURE:
return list(_fixture().get("dingban") or [])
if FX is not None:
return list(FX.get("dingban") or [])
root = proj_root()
try:
out = subprocess.run(
@@ -154,8 +117,8 @@ def dingban_versions():
def current_version():
"""plugin.json 現在宣稱的那一版(= main 要送出去的那一版)。"""
if FIXTURE:
return _fixture().get("current") or ""
if FX is not None:
return FX.get("current") or ""
try:
with open(os.path.join(proj_root(), ".claude-plugin", "plugin.json")) as f:
return json.load(f).get("version") or ""
@@ -168,8 +131,8 @@ def loaded_version():
拿不到就回 None —— 不猜、不謊稱到了(house style:不知道就說不知道)。
"""
if FIXTURE:
return _fixture().get("loaded")
if FX is not None:
return FX.get("loaded")
root = os.environ.get("CLAUDE_PLUGIN_ROOT")
if not root:
return None
@@ -181,8 +144,8 @@ def loaded_version():
# ══ 覆核:逐版問「到了嗎」 ════════════════════════════════════════════════
def audit():
cur = current_version()
def audit(focus=None):
cur = focus or current_version()
if not cur:
die("🔴 讀不到 .claude-plugin/plugin.json 的 version —— 這不是 ISEP repo,或檔案壞了。")
dingban = dingban_versions()
@@ -190,39 +153,42 @@ def audit():
# 現行版本連 `定版` commit 都沒有 ⇒ plugin.json 宣稱了一個沒被定版的號碼。
# 這是 check-version-consistency 那條線的病,不是這支的守備範圍——但要講出來。
dingban = [cur] + dingban
tags = gitea_tags()
rels = gitea_releases()
def tagname(v):
return "v" + v
try:
tags = CHAIN.tags()
rels = CHAIN.releases()
except rc.ChainError as e:
die("🔴 問不到收貨端,這一支不猜:%s" % e)
rows = []
for v in sorted(set(dingban), key=vtuple, reverse=True):
for v in sorted(set(dingban), key=rc.vtuple, reverse=True):
rows.append({
"ver": v,
"dingban": True, # 這份清單本來就是從 定版 commit 來的
"tag": tagname(v) in tags,
"release": tagname(v) in rels,
"tag": rc.tagname(v) in tags,
"release": rc.tagname(v) in rels,
"current": v == cur,
"gaps": rc.gaps(v, tags, rels),
"waived": WAIVED.get(v) if v != cur else None,
})
return cur, rows
STATIONS = [
("dingban", "定版 commit", "在 main 上 commit 一顆 `定版 v<版本>`(= plugin.json 也改成那一版)"),
("tag", "tagGitea", "打並推 tag`git tag v<版本> && git push origin v<版本>`"),
("release", "releaseGitea", "建 release`scripts/release-ship v<版本>`(這一站就是最常斷的那站)"),
]
def ship_cmd():
"""補 release 的那一行要能直接貼著跑:印正本自己的絕對路徑(mistakes.md 2026-08-31)。"""
return "python3 %s" % os.path.join(HERE, "release-ship")
def report():
cur, rows = audit()
def report(focus=None):
cur, rows = audit(focus)
loaded = loaded_version()
icon = {True: "✅", False: "❌"}
lines = ["ISEP 運送鏈覆核(收貨端事實,不聽送的人說) repo=%s" % REPO, ""]
lines.append(" 版本 定版 tag release (← 現行 plugin.json 宣稱的版本")
lines.append(" 版本 定版 tag release (← %s"
% ("你指定的版本" if focus else "現行 plugin.json 宣稱的版本"))
for r in rows:
mark = " ←" if r["current"] else ""
if r["waived"] and r["gaps"]:
mark = " ⏭ 裁定不補(%s" % r["waived"]
lines.append(" %-10s %s %s %s%s" % (
"v" + r["ver"], icon[r["dingban"]], icon[r["tag"]], icon[r["release"]], mark))
@@ -231,7 +197,7 @@ def report():
if loaded is None:
lines.append(" 載入端:這個 context 讀不到 CLAUDE_PLUGIN_ROOT ⇒ 說不出「這個 session 載到哪一版」"
"(不是 session 裡跑的,或沒裝 plugin)。")
elif vtuple(loaded) < vtuple(cur):
elif rc.vtuple(loaded) < rc.vtuple(cur):
lines.append(" 🟡 載入端落後:這個 session 掛的是 v%smain 已到 v%s。" % (loaded, cur))
lines.append(" 修:本機 `claude plugin update isep@inkstone` **並重啟 session**"
"(升版與生效是兩件事);雲端去動一下 Environment 的 setup script 逼它重拍快照。")
@@ -241,19 +207,15 @@ def report():
# ── 判決 ────────────────────────────────────────────────────────────
currow = next((r for r in rows if r["current"]), None)
hard = []
if currow:
for key, label, fix in STATIONS:
if not currow[key]:
hard.append((label, fix))
older_gaps = [r for r in rows if not r["current"] and (not r["tag"] or not r["release"])]
hard = currow["gaps"] if currow else []
older_gaps = [r for r in rows if not r["current"] and r["gaps"] and not r["waived"]]
lines.append("")
if hard:
lines.append("🔴 **現行版本 v%s 的送貨端斷了** —— 對 leo 等於這一版不存在:" % cur)
for label, fix in hard:
for _key, label, fix in hard:
lines.append(" 缺這一站:%s" % label)
lines.append(" 怎麼補:%s" % fix)
lines.append(" 怎麼補:%s" % fix.format(v=cur, ship=ship_cmd()))
if older_gaps:
lines.append(" (另外還有更舊的版本也缺,見下方黃字,但那些不擋。)")
else:
@@ -263,14 +225,10 @@ def report():
lines.append("")
lines.append("🟡 更舊的定版版本也缺件(多半已被新版取代,只提醒、不擋):")
for r in older_gaps:
miss = []
if not r["tag"]:
miss.append("tag")
if not r["release"]:
miss.append("release")
lines.append(" v%s 缺:%s" % (r["ver"], "".join(miss)))
lines.append(" 要補歷史版本 ⇒ 逐版 `scripts/release-ship v<版本>`"
"確定不補 ⇒ 這是 leo/總管的決定,記一句在 inkstone/ISEP#67。")
lines.append(" v%s 缺:%s" % (r["ver"], "".join(label for _k, label, _f in r["gaps"])))
lines.append(" 要補歷史版本 ⇒ 逐版 `%s v<版本>`"
"確定不補 ⇒ 這是 leo/總管的決定,記一句在 inkstone/ISEP#67"
"v0.190.200.21 已於 2026-09-07 裁定不補)。" % ship_cmd())
out = "\n".join(lines)
if hard:
@@ -281,10 +239,16 @@ def report():
def main(argv):
if argv and argv[0] in ("-h", "--help"):
print(__doc__)
return 0
return report()
focus = None
for a in argv:
if a in ("-h", "--help"):
print(__doc__)
return 0
m = re.match(r"^v?(\d+\.\d+\.\d+)$", a.strip())
if not m:
die("用法:release-check [vX.Y.Z]--help 看說明);不認得:%s" % a)
focus = m.group(1)
return report(focus)
if __name__ == "__main__":