Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1f399f8b9 | |||
| a446ad6d1d | |||
| 41c56acd32 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "isep",
|
"name": "isep",
|
||||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:43 支機械閘(53 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、27 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:43 支機械閘(53 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、27 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||||
"version": "0.3.4",
|
"version": "0.3.6",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"inkstone",
|
"inkstone",
|
||||||
"guardrails",
|
"guardrails",
|
||||||
|
|||||||
@@ -17,7 +17,17 @@ ROOT="${CLAUDE_PLUGIN_ROOT:-}"
|
|||||||
VER="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
VER="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
||||||
"$ROOT/.claude-plugin/plugin.json" 2>/dev/null | head -1)"
|
"$ROOT/.claude-plugin/plugin.json" 2>/dev/null | head -1)"
|
||||||
VER="${VER:-未知}"
|
VER="${VER:-未知}"
|
||||||
GATES="$(ls "$ROOT"/hooks/*.sh 2>/dev/null | wc -l | tr -d ' ')"
|
# 🔴 數「真的被註冊的」,不是數目錄裡有幾個 .sh(leo 的雲端驗收 2026-08-23 抓到):
|
||||||
|
# 舊寫法 `ls hooks/*.sh` 把 `pre-write-guard.template.sh`(樣板,不是閘)
|
||||||
|
# 與兩支沒掛註冊的輔助檔一起算進去 ⇒ 報 45,實際註冊 42。
|
||||||
|
# 這個數字是 leo 判斷「這個 session 有沒有閘」的唯一介面——**多報就是假綠**。
|
||||||
|
# (查過歷史:本檔自 daa1674 建立以來只有那一版,沒有別的分支修過這段。)
|
||||||
|
GATES="$(grep -oE 'hooks/[a-zA-Z0-9._-]+\.sh' "$ROOT/hooks/hooks.json" 2>/dev/null \
|
||||||
|
| sort -u | wc -l | tr -d ' ')"
|
||||||
|
case "$GATES" in
|
||||||
|
''|*[!0-9]*|0)
|
||||||
|
GATES="$(ls "$ROOT"/hooks/*.sh 2>/dev/null | grep -cv '\.template\.sh$' | tr -d ' ')" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
MSG="🟢 ISEP v${VER} 已載入(${GATES} 支閘在 ${ROOT})"
|
MSG="🟢 ISEP v${VER} 已載入(${GATES} 支閘在 ${ROOT})"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""hooks/lib/push_target_dir.py -- shared helper (inkstone/ISEP#40 S7-style
|
||||||
|
lib helper: source of truth for a small piece of logic, not a gate by
|
||||||
|
itself; nothing here decides allow/block on its own).
|
||||||
|
|
||||||
|
What it answers: given a shell command string that somewhere invokes
|
||||||
|
`git ... push ...`, which directory will that push actually execute in?
|
||||||
|
|
||||||
|
Why this exists (inkstone/ISEP#30 comment 3949, 脈絡見 InkStoneCo#57):
|
||||||
|
main-and-prod-push-guard.sh's stamp_ok() used to read `git rev-parse
|
||||||
|
--show-toplevel` from the *hook's own* cwd (= the session's real repo) as
|
||||||
|
"HERE", and compare it against the repo path the caller wrote into the
|
||||||
|
stamp ("WANT"). That works when the push target IS the repo the session is
|
||||||
|
standing in. It can never work otherwise: a `cd <other-repo> && git push
|
||||||
|
origin HEAD:main` changes the *push's* directory but not the hook's, so
|
||||||
|
HERE stays the real repo forever while WANT is (correctly) the other repo
|
||||||
|
-- the two can never match, no matter how carefully the caller follows the
|
||||||
|
gate's own instructions. This isn't a bad judgment call; that scenario
|
||||||
|
simply doesn't exist in the old model.
|
||||||
|
|
||||||
|
This module extracts the directory the push *actually* runs in, purely by
|
||||||
|
tokenizing -- it never executes anything. Two sources, first one found
|
||||||
|
wins (in the order a real shell would apply them):
|
||||||
|
- a `cd <path>` chain preceding the push, scoped correctly across
|
||||||
|
subshells: a `(` inherits the current directory from its parent at the
|
||||||
|
moment it opens, but whatever a subshell `cd`s to does NOT leak back
|
||||||
|
out to sibling commands after the matching `)` closes (this mirrors
|
||||||
|
real bash: a subshell's cwd change is local to that subshell). This
|
||||||
|
scoping is load-bearing, not cosmetic: without it, `(cd /repo-A &&
|
||||||
|
true); git push origin main` would misattribute the later push (which
|
||||||
|
really runs wherever the outer shell already was) to /repo-A, and a
|
||||||
|
stale/legitimate stamp for /repo-A could then wrongly wave through a
|
||||||
|
push into whatever the outer cwd actually is -- the exact "stamp
|
||||||
|
opened for repo A also opens the door for repo B" shape 2026-08-11
|
||||||
|
already burned us on once.
|
||||||
|
- a `-C <path>` flag on the git invocation itself, which further wins
|
||||||
|
over any `cd` chain (matches git's own precedence: `-C` sets the
|
||||||
|
directory for that invocation regardless of the shell's cwd).
|
||||||
|
|
||||||
|
Multiple relative `cd`/`-C` hops are combined by plain string join here
|
||||||
|
(no `..`/`~`/`$()` resolution) -- resolving the combined expression to a
|
||||||
|
real, canonical, absolute path is left to the caller, which does it with a
|
||||||
|
read-only `cd "<expr>" && pwd` in a throwaway subshell. That two-step split
|
||||||
|
matters: this module only ever *parses*, so it stays side-effect-free even
|
||||||
|
when fed a hostile or malformed command; only the caller's final `cd`
|
||||||
|
touches the filesystem, and `cd` cannot execute anything, it can only fail
|
||||||
|
to find a directory.
|
||||||
|
|
||||||
|
If no `cd`/`-C` applies (the push runs wherever the hook itself is, i.e.
|
||||||
|
today's behaviour), or the command doesn't parse, prints nothing -- the
|
||||||
|
caller falls back to its existing cwd-based resolution. That fallback
|
||||||
|
direction is deliberately the *safe* one: on any parse ambiguity we hand
|
||||||
|
back "unknown" rather than guess, and an unresolved HERE can only make the
|
||||||
|
gate keep blocking (fail toward blocking), never open a door it wouldn't
|
||||||
|
have opened before.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
printf '%s' "$CMD" | python3 push_target_dir.py
|
||||||
|
"""
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_SEPARATORS = {";", "&&", "||", "|", "&", "\n"}
|
||||||
|
|
||||||
|
|
||||||
|
def _join(base, path):
|
||||||
|
"""Combine a cwd-so-far (`base`, or None if unknown/hook-cwd) with a
|
||||||
|
`cd`/`-C` argument written in the command. Absolute paths and `~`
|
||||||
|
replace the base outright; anything else is appended textually --
|
||||||
|
normalizing `..`/`.` is intentionally left to the caller's real `cd`."""
|
||||||
|
if not path:
|
||||||
|
return base
|
||||||
|
if path == "-" or path.startswith("$"):
|
||||||
|
# `cd -` (previous dir) and `$VAR`/`$(...)` expansions can't be
|
||||||
|
# resolved by tokenizing alone -- treat as "unknown" rather than
|
||||||
|
# guess wrong, which keeps the caller on its safe fallback path.
|
||||||
|
return None
|
||||||
|
if path.startswith("/") or path.startswith("~"):
|
||||||
|
return path
|
||||||
|
if base is None:
|
||||||
|
return path
|
||||||
|
return base.rstrip("/") + "/" + path
|
||||||
|
|
||||||
|
|
||||||
|
def _classify(tokens):
|
||||||
|
if not tokens:
|
||||||
|
return ("other", None)
|
||||||
|
if tokens[0] == "cd" and len(tokens) > 1:
|
||||||
|
return ("cd", tokens[1])
|
||||||
|
if tokens[0] == "git" and "push" in tokens[1:]:
|
||||||
|
return ("push", tokens)
|
||||||
|
return ("other", None)
|
||||||
|
|
||||||
|
|
||||||
|
def _events(cmd):
|
||||||
|
"""Tokenize cmd into (kind, value) events in source order: 'enter'/
|
||||||
|
'exit' for parens (subshell boundaries), 'cd'/'push'/'other' for
|
||||||
|
statements split on the usual shell separators. Returns [] on any
|
||||||
|
quoting error -- caller then falls back to cwd-based resolution."""
|
||||||
|
try:
|
||||||
|
lexer = shlex.shlex(cmd, posix=True, punctuation_chars=True)
|
||||||
|
lexer.whitespace_split = True
|
||||||
|
toks = list(lexer)
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
events = []
|
||||||
|
seg = []
|
||||||
|
|
||||||
|
def flush():
|
||||||
|
if seg:
|
||||||
|
# _classify's "push" branch returns `tokens` by reference; copy
|
||||||
|
# before clear() below, or the event's tuple would observe the
|
||||||
|
# list emptied out from under it (aliasing, not a value copy).
|
||||||
|
events.append(_classify(seg[:]))
|
||||||
|
seg.clear()
|
||||||
|
|
||||||
|
for tok in toks:
|
||||||
|
if tok == "(":
|
||||||
|
flush()
|
||||||
|
events.append(("enter", None))
|
||||||
|
elif tok == ")":
|
||||||
|
flush()
|
||||||
|
events.append(("exit", None))
|
||||||
|
elif tok in _SEPARATORS:
|
||||||
|
flush()
|
||||||
|
else:
|
||||||
|
seg.append(tok)
|
||||||
|
flush()
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def find_push_target(cmd):
|
||||||
|
events = _events(cmd)
|
||||||
|
if not events:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
stack = [None] # cwd-so-far per paren depth; None = "same as hook cwd"
|
||||||
|
result = None
|
||||||
|
saw_push = False
|
||||||
|
|
||||||
|
for kind, val in events:
|
||||||
|
if kind == "enter":
|
||||||
|
stack.append(stack[-1]) # child subshell inherits current dir
|
||||||
|
elif kind == "exit":
|
||||||
|
if len(stack) > 1:
|
||||||
|
stack.pop() # subshell's own cd's don't leak out
|
||||||
|
elif kind == "cd":
|
||||||
|
stack[-1] = _join(stack[-1], val)
|
||||||
|
elif kind == "push":
|
||||||
|
saw_push = True
|
||||||
|
c_path = None
|
||||||
|
toks = val
|
||||||
|
for j, t in enumerate(toks):
|
||||||
|
if t == "-C" and j + 1 < len(toks):
|
||||||
|
c_path = toks[j + 1]
|
||||||
|
break
|
||||||
|
if t.startswith("-C") and len(t) > 2:
|
||||||
|
c_path = t[2:]
|
||||||
|
break
|
||||||
|
result = _join(stack[-1], c_path) if c_path else stack[-1]
|
||||||
|
|
||||||
|
if not saw_push:
|
||||||
|
return ""
|
||||||
|
return result or ""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cmd = sys.stdin.read()
|
||||||
|
sys.stdout.write(find_push_target(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -98,7 +98,32 @@ stamp_ok() {
|
|||||||
# 等於一把萬用鑰匙——正是 08-11 那次穿透的形狀(替 A repo 開的門 B repo 也走得過)。
|
# 等於一把萬用鑰匙——正是 08-11 那次穿透的形狀(替 A repo 開的門 B repo 也走得過)。
|
||||||
# 而且 `.claude/settings.local.json` 裡真的放行過 `touch /tmp/.main-push-ok`。
|
# 而且 `.claude/settings.local.json` 裡真的放行過 `touch /tmp/.main-push-ok`。
|
||||||
# ⇒ 現在**空內容一律不算數**:要嘛寫得出 repo 路徑且對得上,要嘛不放行。
|
# ⇒ 現在**空內容一律不算數**:要嘛寫得出 repo 路徑且對得上,要嘛不放行。
|
||||||
HERE=$(git rev-parse --show-toplevel 2>/dev/null || printf '')
|
#
|
||||||
|
# 🔴 2026-08-23(inkstone/ISEP#30 comment 3949,脈絡 InkStoneCo#57):
|
||||||
|
# `HERE` 原本一律讀 hook 自己的 cwd(=session 站著的那個 repo)。
|
||||||
|
# 只要要推的 repo **不是**「session 站著的那個 repo」——例如指令自己
|
||||||
|
# `cd <別的 repo> && git push` 或 `git -C <別的 repo> push`——HERE 永遠是
|
||||||
|
# 總管的真身,而總管替目標 repo 開的 WANT 永遠對不上,這道閘就**永遠沒辦法
|
||||||
|
# 合法通過**。不是判斷錯,是這個情境在舊模型裡根本不存在(照閘的指示做
|
||||||
|
# 戳記,戳記內容天生就贏不了)。
|
||||||
|
# 改法:先看指令本身有沒有把 push 的執行目錄改掉
|
||||||
|
# (lib/push_target_dir.py——純 tokenize,不執行任何指令,
|
||||||
|
# 對 `cd A && cd B && git push` 這種多層鏈與 `(cd A && …); git push` 這種
|
||||||
|
# 子殼會不會外洩都做了範圍化,理由見該檔檔頭);解得出來就 `cd` 進那個
|
||||||
|
# 目錄(唯讀操作,`cd` 本身不會執行任何東西)問 git 那裡的 toplevel 是誰;
|
||||||
|
# 解不出來(沒有 cd/-C,或指令太怪解析失敗)才退回舊行為=hook 自己的 cwd。
|
||||||
|
# 🔴 綁 repo+單次用完即丟兩條性質完全沒有鬆動:這裡只是把「現在人在哪個
|
||||||
|
# repo」問得更準,比對邏輯(下面兩行)一個字沒動。
|
||||||
|
_push_target_dir="$(dirname "$0")/lib/push_target_dir.py"
|
||||||
|
_target_expr=""
|
||||||
|
if [ -f "$_push_target_dir" ]; then
|
||||||
|
_target_expr=$(printf '%s' "$CMD" | python3 "$_push_target_dir" 2>/dev/null || printf '')
|
||||||
|
fi
|
||||||
|
if [ -n "$_target_expr" ]; then
|
||||||
|
HERE=$(cd "$_target_expr" 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null || printf '')
|
||||||
|
else
|
||||||
|
HERE=$(git rev-parse --show-toplevel 2>/dev/null || printf '')
|
||||||
|
fi
|
||||||
WANT=$(head -1 "$STAMP" 2>/dev/null || printf '')
|
WANT=$(head -1 "$STAMP" 2>/dev/null || printf '')
|
||||||
[ -n "$WANT" ] || return 1
|
[ -n "$WANT" ] || return 1
|
||||||
[ -n "$HERE" ] || return 1
|
[ -n "$HERE" ] || return 1
|
||||||
@@ -132,7 +157,16 @@ if printf '%s' "$CMD" | grep -qE '(^|[;&|(`]|&&|\|\|)[[:space:]]*git([[:space:]]
|
|||||||
# git checkout origin/main --detach; git push origin refs/tags/v0.3.3
|
# git checkout origin/main --detach; git push origin refs/tags/v0.3.3
|
||||||
# ⇒ **紅線寫得越細,命中關鍵字的機率越高**(leo 2026-08-17 的觀察,
|
# ⇒ **紅線寫得越細,命中關鍵字的機率越高**(leo 2026-08-17 的觀察,
|
||||||
# 文字層封路必敗)。這裡改成判動作的目標,不是判字面。
|
# 文字層封路必敗)。這裡改成判動作的目標,不是判字面。
|
||||||
_push_seg=$(printf '%s' "$CMD" | sed -E 's/.*git[[:space:]]+(-[^[:space:]]+[[:space:]]+)*push//' | sed -E 's/[;&|].*//')
|
# 2026-08-23(順著 inkstone/ISEP#30 comment 3949 補測時自己抓到的洞,不在原票範圍
|
||||||
|
# 但屬於同一支閘、同一段邏輯,且直接讓下面「反向不准鬆」的驗證跑不過,所以一併修):
|
||||||
|
# `(git push origin HEAD:main)`——單純用括號包住整條指令——舊版會整段放行,
|
||||||
|
# 跟 HERE/戳記完全無關,**連目的地判斷本身都沒觸發**。
|
||||||
|
# 成因:截斷 refspec 尾巴只切 `;`/`&`/`|` 三種字元,沒算到 `)`——
|
||||||
|
# 於是「HEAD:main)」被當成一個 token,`${_tok##*:}` 剝完冒號還剩「main)」,
|
||||||
|
# 跟 `^main$` 對不上 ⇒ 判定成「看不出目標」⇒ 整段放行。加 `)` 進截斷字元。
|
||||||
|
# git 的 refspec/分支名語法本來就不允許出現 `)`,所以在這裡截斷永遠安全,
|
||||||
|
# 不會誤傷任何合法的推送目標。
|
||||||
|
_push_seg=$(printf '%s' "$CMD" | sed -E 's/.*git[[:space:]]+(-[^[:space:]]+[[:space:]]+)*push//' | sed -E 's/[;&|)].*//')
|
||||||
_dest=""
|
_dest=""
|
||||||
_seen_remote=0
|
_seen_remote=0
|
||||||
_saw_refspec=0
|
_saw_refspec=0
|
||||||
|
|||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 跨 repo 戳記實測(inkstone/ISEP#30 comment 3949,脈絡 inkstone/InkStoneCo#57)
|
||||||
|
#
|
||||||
|
# 補的是什麼:hooks/tests/main-and-prod-push-guard.test.sh 那八向都只在單一 repo
|
||||||
|
# (測試腳本自己所在的 repo)裡驗證,從沒測過「站在 A、要推 B 的 main」這個形狀
|
||||||
|
# ——而這正是 2026-08-21 真的撞到、讓戳記永遠對不上的那個情境。這支專門補這塊。
|
||||||
|
#
|
||||||
|
# 用法:main-and-prod-push-guard-cross-repo.test.sh <要測的 hook 絕對路徑>
|
||||||
|
# 路徑務必給絕對路徑——測試會 cd 進臨時建立的 A/B repo 再呼叫它,相對路徑
|
||||||
|
# 到那時就對不上了(自己撞過一次:exit=127 command not found)。
|
||||||
|
set -u
|
||||||
|
G="$1"
|
||||||
|
STAMP=/tmp/.main-push-ok
|
||||||
|
WORK=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORK"; rm -f "$STAMP"' EXIT
|
||||||
|
|
||||||
|
for d in A B; do
|
||||||
|
git init -q -b main "$WORK/$d"
|
||||||
|
git -C "$WORK/$d" config user.email t@t.com
|
||||||
|
git -C "$WORK/$d" config user.name t
|
||||||
|
echo x > "$WORK/$d/f.txt"
|
||||||
|
git -C "$WORK/$d" add f.txt
|
||||||
|
git -C "$WORK/$d" commit -q -m init
|
||||||
|
done
|
||||||
|
A="$WORK/A"; B="$WORK/B"
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
t() { # t <說明> <cwd> <指令> <期望 exit>
|
||||||
|
local desc="$1" cwd="$2" cmd="$3" want="$4"
|
||||||
|
local rc
|
||||||
|
rc=$(cd "$cwd" && CLAUDE_CODE_CHILD_SESSION=1 python3 -c '
|
||||||
|
import json, subprocess, sys
|
||||||
|
p = subprocess.run(["bash", sys.argv[2]],
|
||||||
|
input=json.dumps({"tool_name": "Bash",
|
||||||
|
"tool_input": {"command": sys.argv[1]}}),
|
||||||
|
capture_output=True, text=True)
|
||||||
|
print(p.returncode)
|
||||||
|
' "$cmd" "$G")
|
||||||
|
if [ "$rc" = "$want" ]; then printf ' ✅ %-58s exit=%s\n' "$desc" "$rc"; pass=$((pass+1))
|
||||||
|
else printf ' ❌ %-58s exit=%s(期望 %s)\n' "$desc" "$rc" "$want"; fail=$((fail+1)); fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "── 2026-08-21 實撞的原形狀:站在 A,要推 B 的 main ──"
|
||||||
|
rm -f "$STAMP"
|
||||||
|
t "沒戳記 → 擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||||||
|
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "替 B 開的戳記 → 推 B 的 main 該放行(舊版在此情境永遠擋,這是本票要修的洞)" \
|
||||||
|
"$A" "cd $B && git push origin HEAD:main" 0
|
||||||
|
|
||||||
|
echo "── 反向不准鬆:替 A 開的戳記,不能拿去放行推 B(08-11 那次穿透的形狀)──"
|
||||||
|
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "替 A 開的戳記 → 拿去推 B 的 main 必須仍被擋" \
|
||||||
|
"$A" "cd $B && git push origin HEAD:main" 2
|
||||||
|
rm -f "$STAMP"
|
||||||
|
|
||||||
|
echo "── git -C 語法要吃到同一套判斷 ──"
|
||||||
|
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "替 B 開戳記,用 git -C B push" "$A" "git -C $B push origin main" 0
|
||||||
|
rm -f "$STAMP"
|
||||||
|
|
||||||
|
echo "── 08-11 原始穿透的形狀:子殼裡的 cd 不能外洩到殼外 ──"
|
||||||
|
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "子殼裡 cd 去 B 但沒在殼內推;殼外站著 A 真的推 → 符合 A 的戳記,放行" \
|
||||||
|
"$A" "(cd $B && true); git push origin HEAD:main" 0
|
||||||
|
rm -f "$STAMP"
|
||||||
|
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "子殼裡 cd 去 B 且在殼內真的推 → 目標是 B,戳記是 A,必須擋" \
|
||||||
|
"$A" "(cd $B && git push origin HEAD:main)" 2
|
||||||
|
rm -f "$STAMP"
|
||||||
|
|
||||||
|
echo "── 順手抓到、一併修的洞:純括號包住整條指令,不准繞過目的地判斷 ──"
|
||||||
|
t "(git push origin HEAD:main) 沒有任何戳記 → 必須擋(舊版在此整段放行)" \
|
||||||
|
"$A" "(git push origin HEAD:main)" 2
|
||||||
|
|
||||||
|
echo "── 同 repo(session 站著的那個)舊行為原封不動 ──"
|
||||||
|
rm -f "$STAMP"
|
||||||
|
t "站在 A 推 A 自己的 main,沒戳記 → 擋" "$A" "git push origin HEAD:main" 2
|
||||||
|
git -C "$A" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "站在 A 推 A 自己的 main,替 A 開戳記 → 放行" "$A" "git push origin HEAD:main" 0
|
||||||
|
rm -f "$STAMP"
|
||||||
|
|
||||||
|
echo "── 舊有行為一條都不能壞 ──"
|
||||||
|
t "推 feature branch 放行" "$A" "git push origin feat/xyz" 0
|
||||||
|
t "推 tag 放行" "$A" "git push origin refs/tags/v1.0.0" 0
|
||||||
|
t "只是提到 main 的 gh pr create,放行" "$A" "gh pr create --base main --title t" 0
|
||||||
|
|
||||||
|
echo "── subagent 沒戳記,即使 cd 去別的 repo 也照擋 ──"
|
||||||
|
rm -f "$STAMP"
|
||||||
|
t "subagent 站在 A、cd 去 B 推 main,沒戳記仍擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||||||
|
|
||||||
|
echo "── 單次用完即丟、900 秒逾時:換到跨 repo 場景一樣要成立 ──"
|
||||||
|
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
t "第一次:替 B 開戳記推 B → 放行" "$A" "cd $B && git push origin HEAD:main" 0
|
||||||
|
t "第二次:同一枚戳記(已用掉)再推一次 → 應該擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||||||
|
rm -f "$STAMP"; touch "$STAMP"
|
||||||
|
t "touch 出的空戳記 → 推 B 的 main 仍應擋(08-12 補的洞不能被本次改動重開)" \
|
||||||
|
"$A" "cd $B && git push origin HEAD:main" 2
|
||||||
|
rm -f "$STAMP"
|
||||||
|
git -C "$B" rev-parse --show-toplevel > "$STAMP"
|
||||||
|
touch -t "$(date -v-16M +%Y%m%d%H%M.%S 2>/dev/null || date -d '-16 minutes' +%Y%m%d%H%M.%S)" "$STAMP" 2>/dev/null
|
||||||
|
t "16 分鐘前開的戳記 → 已過期,推 B 應擋" "$A" "cd $B && git push origin HEAD:main" 2
|
||||||
|
rm -f "$STAMP"
|
||||||
|
|
||||||
|
echo "────── 通過 $pass / 失敗 $fail"
|
||||||
|
[ "$fail" = 0 ]
|
||||||
Reference in New Issue
Block a user