From e4e3d69acfc8fd4f6f2eaa710f15b6173c8ff888 Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 20 Aug 2026 13:02:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(release):=20=E7=89=88=E6=9C=AC=E5=8F=AA?= =?UTF-8?q?=E6=9C=89=20Gitea=20Releases=20=E7=AD=94=E5=BE=97=E5=87=BA?= =?UTF-8?q?=E4=BE=86=EF=BC=8C=E4=B8=8D=E5=86=8D=E9=9D=A0=20README=20?= =?UTF-8?q?=E8=87=AA=E5=A0=B1=EF=BC=88inkstone/ISEP#6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 現況:README.md 宣稱「狀態 0.1.0」,但 repo release_counter=0、一個 tag 都沒打。leo 當場指出這是違規,命中規範自己的 E12(宣稱交付但沒有 tag); leo 補充:「release 不是寫在 readme,要放在 release 裡」。 改法(結構性防漂移,不是靠人記得同步): - README.md 不再自行宣告版本號,改成指向 Gitea Releases 頁面 - .claude-plugin/plugin.json 的 version 改回哨兵值 0.0.0 (=誠實承認目前沒有一個經過驗證、掛在 Releases 上的版本; 真正打 tag 那天才跟 tag 一起同步成那個號碼) - 新增 scripts/check-version-consistency.sh:隨時可跑的一致性檢查 (plugin.json version 是否等於最新 tag/README 是否偷偷自報版本) - 新增 hooks/release-tag-guard.sh:PreToolUse Bash 閘,在真正打 git tag 的那一刻擋下與 plugin.json 不一致的版本號,註冊進 hooks.json 紅線:本次不打 tag、不建 release——那是總管驗過整個 milestone 之後的動作, 這裡交的是機制與草稿。 Co-Authored-By: Claude Sonnet 5 --- .claude-plugin/plugin.json | 4 +- README.md | 14 ++++- hooks/hooks.json | 4 ++ hooks/release-tag-guard.sh | 81 ++++++++++++++++++++++++++ scripts/check-version-consistency.sh | 87 ++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+), 5 deletions(-) create mode 100755 hooks/release-tag-guard.sh create mode 100755 scripts/check-version-consistency.sh diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a46c048..8441eb5 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "isep", - "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:41 支機械閘、7 支 slash command、2 支 skill、23 支腳本。本機與雲端裝同一份。", - "version": "0.1.0", + "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:42 支機械閘、7 支 slash command、2 支 skill、24 支腳本。本機與雲端裝同一份。", + "version": "0.0.0", "keywords": ["inkstone", "guardrails", "hooks", "gitea", "arcrun"] } diff --git a/README.md b/README.md index e389f13..b3d1be6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ | | 數量 | 是什麼 | |---|---|---| -| `hooks/` | 41 支 + `hooks.json` | 全部機械閘(PreToolUse/Stop/SubagentStop/SessionStart/PostToolUse 共 51 條註冊) | +| `hooks/` | 42 支 + `hooks.json` | 全部機械閘(PreToolUse/Stop/SubagentStop/SessionStart/PostToolUse 共 52 條註冊) | | `commands/` | 7 支 | `/wiki-recall` `/ship-check` `/cp-write` … | | `skills/` | 2 支 | | | `scripts/` | 23 支 | `ticket`/`github-arm.sh`/`gitea-bootstrap.sh` … | @@ -43,6 +43,14 @@ hook 一律用官方的 `${CLAUDE_PLUGIN_ROOT}`,**不准寫死絕對路徑、 🔴 **只改這裡,然後兩邊 `/plugin update`。** 不要再改 `InkStoneCo/.claude/hooks/`——那個目錄退場中。 -## 狀態 +## 版本 -- 0.1.0 — 從 `InkStoneCo/.claude/` 搬過來,51 條 hook 路徑全部改成 `${CLAUDE_PLUGIN_ROOT}` +**「ISEP 現在是哪一版」只有一個地方答得出來:[Gitea Releases](https://git.uncle6.me/inkstone/ISEP/releases)。** +每一版的內容、改了什麼、驗過什麼都寫在那裡的 release note,不寫在這份 README。 + +這份 README 本來寫死過「狀態:0.1.0」,但 repo 一個 tag 都沒打(`release_counter=0`)—— +leo 當場指出這是違規:宣稱交付卻沒有 tag 撐它(`inkstone/ISEP#6`)。 +現在改成結構性防漂移:`.claude-plugin/plugin.json` 的 `version` 欄位永遠跟最新 tag 一致, +`scripts/check-version-consistency.sh` 會擋下兩者對不上的狀態, +`hooks/release-tag-guard.sh` 則在打 tag 的當下直接擋住不一致的 tag—— +細節與判準都寫在那兩支腳本開頭的註解。 diff --git a/hooks/hooks.json b/hooks/hooks.json index 817ff7b..37e845c 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -32,6 +32,10 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/leo21c-write-guard.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/release-tag-guard.sh" } ] }, diff --git a/hooks/release-tag-guard.sh b/hooks/release-tag-guard.sh new file mode 100755 index 0000000..2b8b9a7 --- /dev/null +++ b/hooks/release-tag-guard.sh @@ -0,0 +1,81 @@ +#!/bin/sh +# release-tag-guard.sh — PreToolUse(Bash):打 tag 那一刻擋下版本不一致 +# +# 立這道閘的來由(inkstone/ISEP#6,2026-08-20): +# README.md 曾寫死「狀態:0.1.0」,但這個 repo `release_counter=0`、 +# 一個 tag 都沒打。leo 當場指出這是違規,且命中規範自己的 E12 +# (宣稱交付但沒有 tag);leo 補充:「release 不是寫在 readme,要放在 release 裡」。 +# +# 這支閘解的不是「README 寫錯字」,是**結構性防漂移**: +# 「ISEP 現在是哪一版」只有一個地方答得出來= Gitea Releases(git tag)。 +# `.claude-plugin/plugin.json` 的 `version` 欄位必須跟這個 tag 完全一致, +# 不然又回到「版本號在不同地方各說各話」的老路——只是這次換成 +# 「manifest 一個號碼、tag 又一個號碼」而不是「README 一個號碼、repo 裡沒 tag」。 +# 同一套判準也寫在 `scripts/check-version-consistency.sh`(可以隨時手動跑, +# 不必等到打 tag那一刻);這支 hook 是「結構性做不到」的那一半—— +# 在真正動手打 tag 的當下擋住,而不是靠事後補跑腳本才發現。 +# +# 🔴 紅線:**這支 hook 不打 tag、不建 release**——它只在別人(人類/總管)要打 tag 時 +# 檢查一致性。真正打哪個版本號的 tag,是總管驗過整個 milestone 之後的動作。 +# +# 設計紀律(沿用本 repo既有 guard 的兩條,見 main-and-prod-push-guard.sh 註解): +# • **先排除「談論/讀取/刪除/列出」**,只擋「真的要打一個新 tag」的那個命令形狀。 +# • **抽不出版本號就不擋**(fail-open on 解析失敗,不是 fail-open on 檢查結果)—— +# 避免因為指令格式特殊(例如帶簽名 `-s`、多行訊息)誤攔到人, +# 那正是「閘被誤攔多次會被繞過」的病根(README 段落引的 D95 同一款教訓)。 +set -eu + +INPUT="$(cat)" +CMD=$(printf '%s' "$INPUT" | python3 -c ' +import sys, json +try: print(json.load(sys.stdin).get("tool_input", {}).get("command", "") or "") +except Exception: print("") +' 2>/dev/null || printf '') + +[ -z "$CMD" ] && exit 0 + +# ── 先排除不是「打新 tag」的動作 ────────────────────────────────────── +case "$CMD" in + sed\ *|cat\ *|grep\ *|head\ *|tail\ *|wc\ *|less\ *|ls\ *|awk\ *|rg\ *|echo\ *) exit 0 ;; + *"git tag -d"*|*"git tag --delete"*|*"git tag -l"*|*"git tag --list"*|*"git tag -n"*) exit 0 ;; + *" --dry-run"*|*"--dry-run "*) exit 0 ;; +esac + +case "$CMD" in + *"git tag "*) ;; + *) exit 0 ;; +esac + +# ── 從命令裡萃取版本號(第一個 vX.Y.Z 或 X.Y.Z 樣式的 token)─────────── +TAGNAME=$(printf '%s' "$CMD" | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || printf '') +[ -n "$TAGNAME" ] || exit 0 # 抽不出版本號=不是本閘管的形狀,不擋(見上方設計紀律) + +VER=${TAGNAME#v} + +PROJ="${CLAUDE_PROJECT_DIR:-$(pwd)}" +PLUGIN_JSON="$PROJ/.claude-plugin/plugin.json" +[ -f "$PLUGIN_JSON" ] || exit 0 # 不在 ISEP repo 裡(沒有這個檔案)=不是本閘管的 repo + +PJVER=$(python3 -c "import json;print(json.load(open('$PLUGIN_JSON')).get('version',''))" 2>/dev/null || printf '') +[ -n "$PJVER" ] || exit 0 + +if [ "$PJVER" != "$VER" ]; then + cat >&2 <&2 + exit 1 +fi + +PJVER=$(python3 -c "import json;print(json.load(open('$PLUGIN_JSON')).get('version',''))" 2>/dev/null || printf '') +if [ -z "$PJVER" ]; then + echo "❌ .claude-plugin/plugin.json 沒有 version 欄位(或不是合法 JSON)" >&2 + exit 1 +fi + +# --- 2. 跟最新 tag 比對 --- +LATEST_TAG=$(cd "$PROJ" && git tag --list 'v*' --sort=-v:refname 2>/dev/null | head -1 || printf '') + +if [ -z "$LATEST_TAG" ]; then + # 沒有任何 tag:唯一合法的 plugin.json version 是哨兵值 0.0.0 + if [ "$PJVER" != "0.0.0" ]; then + echo "❌ 版本不一致:" >&2 + echo " repo 裡沒有任何 tag(release_counter=0,還沒有一個正式 release)" >&2 + echo " 但 .claude-plugin/plugin.json 的 version 卻宣稱「$PJVER」" >&2 + echo " → 這就是 inkstone/ISEP#6 抓到的違規本身:宣稱一個版本,卻沒有 tag 撐它。" >&2 + echo " → 修法:version 改回 0.0.0(=尚未發過正式版),等真正打 tag 那天再同步改成那個號碼。" >&2 + FAIL=1 + fi +else + TAGVER=${LATEST_TAG#v} + if [ "$PJVER" != "$TAGVER" ]; then + echo "❌ 版本不一致:" >&2 + echo " Gitea 上最新的 tag 是 $LATEST_TAG(版本 $TAGVER)" >&2 + echo " 但 .claude-plugin/plugin.json 的 version 是「$PJVER」" >&2 + echo " → 兩者必須完全一致,不然「ISEP 現在是哪一版」又變成各說各話。" >&2 + FAIL=1 + fi +fi + +# --- 3. README 不准自己宣告版本號 --- +HIT=$(grep -nE '^\s*-\s*v?[0-9]+\.[0-9]+\.[0-9]+\s*(—|-)' "$README" 2>/dev/null || printf '') +if [ -f "$README" ] && [ -n "$HIT" ]; then + echo "❌ README.md 還在自己宣告版本號(不准,release 只放在 Gitea Releases):" >&2 + echo "$HIT" >&2 + FAIL=1 +fi + +if [ "$FAIL" -eq 0 ]; then + if [ -n "$LATEST_TAG" ]; then + echo "✅ 版本一致:plugin.json=$PJVER,最新 tag=$LATEST_TAG,README 沒有自行宣告版本。" + else + echo "✅ 版本一致:plugin.json=$PJVER(哨兵值,尚無 tag 是合法狀態),README 沒有自行宣告版本。" + fi +fi + +exit "$FAIL" -- 2.52.0