226 lines
11 KiB
Bash
Executable File
226 lines
11 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# wp-gate.sh — 工作包 PR 閘門:一個工作包的 PR 沒合併,就不准開下一包(供 jsc-sdlc:implement 呼叫)。
|
||
#
|
||
# 為什麼要有這支腳本:這條規則原本只寫在技能內文裡,靠模型自律遵守。內文靠不住——換一個
|
||
# 工作階段、換一個模型,或只是上下文被截掉,規則就跟著消失,而且沒有任何徵兆看得出來。
|
||
# 所以判定搬到程式層:未結清的狀態由 jsc-hooks 的 sdlc-gate.sh 記在 $JSC_HOME/wp/ 下(刻意
|
||
# 不綁 session,開新對話照樣擋),PR 的真實合併狀態則由本檔向 Gitea 查,查到合併才結清。
|
||
#
|
||
# 分工:hook 那半(wp-check)只注入提醒並擋掉別的階段技能,它不打網路;本檔是唯一會去問
|
||
# Gitea 的一方,也是唯一有權解鎖的一方。兩邊都能改狀態的話,就沒有人說得清鎖為什麼不見了。
|
||
#
|
||
# 用法:
|
||
# wp-gate.sh check {owner}/{repo} {index} [--since {ISO 時間}]
|
||
# 查一支 PR。已合併就解鎖並放行;沒合併就把留言全部印出來並擋住。
|
||
# --since 只印比該時間更新的留言,值用上一輪印出的 latest=(UTC,形如 2026-08-25T10:19:59Z)。
|
||
# wp-gate.sh lock {owner}/{repo} {index}
|
||
# PR 開好之後上鎖,讓閘門跨工作階段有效。轉呼叫 sdlc-gate.sh wp-lock。
|
||
#
|
||
# 輸出: 第一行固定為 `status=...`(供程式判讀),其後為人類可讀的繁中說明。
|
||
# status=merged 已合併,鎖已解除,可以挑下一個工作包
|
||
# status=open PR 還開著,沒有合併
|
||
# status=closed-unmerged PR 被關掉但沒有合併——這不算完成
|
||
# status=locked 已記下這筆未結清的 PR
|
||
# status=usage 用法錯誤
|
||
# status=missing-dep 相依腳本找不到,或這支 PR 查不到
|
||
# check 未合併時,最後一行固定為 `latest={最新一筆留言的時間戳}`(一筆留言都沒有就是 `latest=`),
|
||
# 供呼叫端寫回分析頁,下一輪拿它當 --since,已處理過的留言就不會再處理一遍。
|
||
#
|
||
# 結束碼: 0=已合併或無阻擋 1=未合併(擋住,呼叫端必須停下來逐筆修留言)
|
||
# 2=用法錯誤 3=相依工具或 PR 查不到
|
||
#
|
||
# 陷阱:
|
||
# - 「state=closed 但 merged=false」最容易被當成完成:那是 PR 被關掉、程式碼沒進去,
|
||
# 所以單獨回報 closed-unmerged,結束碼一樣是 1。只看 state 會放行一包沒交出去的工作。
|
||
# - 查不到就擋,絕不安靜放行。相依腳本缺一支、或 PR 查不到,一律 exit 3 並講明缺哪一支:
|
||
# 閘門查不到卻放行,等於沒有閘門,而且比沒有更糟——大家以為有人在看。
|
||
# - 留言讀不到(例如網路中斷)時仍然擋(exit 1),不改判成通過。
|
||
# - --since 是字串比較,不做時區換算。Gitea 印的是 UTC 的 ...Z,字典順序等於時間順序;
|
||
# 餵進帶 +08:00 這類偏移的值會比錯,所以只餵上一輪的 latest=。
|
||
# - latest= 取的是「全部留言」裡最新的一筆,不是過濾後那幾筆。取過濾後的會在沒有新留言時
|
||
# 倒退回舊時間戳,下一輪又把處理過的留言全部翻出來。
|
||
# - 已合併時會呼叫 sdlc-gate.sh wp-unlock 解鎖;解鎖失敗照樣回報 merged 並 exit 0,但會多印
|
||
# 一行警示與手動指令——鎖沒清掉,hook 會一直提醒下去。
|
||
set -u
|
||
|
||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||
plugin_root="${CLAUDE_PLUGIN_ROOT:-$script_dir/..}"
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
用法:
|
||
wp-gate.sh check {owner}/{repo} {index} [--since {ISO 時間}] 查 PR:合併就解鎖放行,沒合併就印出留言並擋住
|
||
wp-gate.sh lock {owner}/{repo} {index} 開完 PR 後上鎖,讓閘門跨工作階段有效
|
||
結束碼: 0=已合併或無阻擋 1=未合併(擋住) 2=用法錯誤 3=相依工具或 PR 查不到
|
||
EOF
|
||
}
|
||
|
||
# 找相依腳本。兩種版面都要顧到,否則腳本只在其中一種版面下會動:
|
||
# 並排存取庫(開發用):{workspace}/sdlc 旁邊就是 {workspace}/gitea、{workspace}/hooks
|
||
# 已安裝 plugin:每個 plugin 各有版本目錄,取排序最後的一份(通常即最新版)
|
||
# 找不到不回傳路徑,由呼叫端 exit 3;不預設一個猜的路徑,猜錯會變成安靜放行。
|
||
resolve_dep() { # $1=並排目錄名(gitea、hooks) $2=plugin 內的相對路徑(tools/gitea.sh)
|
||
_name="$1"; _rel="$2"
|
||
for _c in "$plugin_root/../$_name/$_rel" "$plugin_root/../jsc-$_name/$_rel"; do
|
||
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
done
|
||
_c=$(ls -d "$plugin_root"/../../"jsc-$_name"/*/"$_rel" \
|
||
"$plugin_root"/../../"$_name"/*/"$_rel" \
|
||
"$HOME"/.claude/plugins/cache/*/"jsc-$_name"/*/"$_rel" 2>/dev/null \
|
||
| sort | tail -n1)
|
||
[ -n "$_c" ] && [ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
_c=$(command -v "$(basename "$_rel")" 2>/dev/null || true)
|
||
[ -n "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
||
return 1
|
||
}
|
||
|
||
gitea_sh() {
|
||
if [ -n "${JSC_GITEA_TOOLS:-}" ] && [ -f "$JSC_GITEA_TOOLS/gitea.sh" ]; then
|
||
printf '%s\n' "$JSC_GITEA_TOOLS/gitea.sh"; return 0
|
||
fi
|
||
resolve_dep gitea tools/gitea.sh
|
||
}
|
||
|
||
sdlc_gate_sh() {
|
||
if [ -n "${JSC_HOOKS_DIR:-}" ] && [ -f "$JSC_HOOKS_DIR/sdlc-gate.sh" ]; then
|
||
printf '%s\n' "$JSC_HOOKS_DIR/sdlc-gate.sh"; return 0
|
||
fi
|
||
resolve_dep hooks hooks/sdlc-gate.sh
|
||
}
|
||
|
||
missing_dep() { # $1=缺哪一支的說明
|
||
echo 'status=missing-dep'
|
||
{
|
||
echo "[jsc][工作包閘門][ERR]:$1"
|
||
echo '本次不判定,也不放行。閘門查不到卻放行等於沒有閘門,請先修好相依關係再重跑。'
|
||
} >&2
|
||
exit 3
|
||
}
|
||
|
||
# {owner}/{repo} 格式檢查:剛好一層斜線,前後都不得為空。
|
||
valid_repo() { # $1=參數
|
||
case "${1:-}" in
|
||
*/*/*|/*|*/) return 1 ;;
|
||
*/*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
valid_index() { # $1=參數
|
||
case "${1:-}" in
|
||
''|*[!0-9]*) return 1 ;;
|
||
*) return 0 ;;
|
||
esac
|
||
}
|
||
|
||
sub="${1:-}"
|
||
[ -n "$sub" ] || { echo 'status=usage'; usage; exit 2; }
|
||
shift
|
||
|
||
case "$sub" in
|
||
check)
|
||
repo="${1:-}"; index="${2:-}"
|
||
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }
|
||
valid_index "$index" || { echo 'status=usage'; usage; exit 2; }
|
||
shift 2
|
||
since=''
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--since)
|
||
since="${2:-}"
|
||
[ -n "$since" ] || { echo 'status=usage'; usage; exit 2; }
|
||
shift 2 ;;
|
||
--since=*) since=${1#--since=}
|
||
[ -n "$since" ] || { echo 'status=usage'; usage; exit 2; }
|
||
shift ;;
|
||
*) echo 'status=usage'; usage; exit 2 ;;
|
||
esac
|
||
done
|
||
|
||
gitea=$(gitea_sh) || missing_dep "找不到 jsc-gitea 的 tools/gitea.sh。並排版面請確認 {workspace}/gitea 存在,已安裝版面請確認 jsc-gitea plugin 已安裝,或設定 JSC_GITEA_TOOLS 指向它的 tools 目錄。"
|
||
|
||
if ! st=$(sh "$gitea" pr-status "$repo" "$index" 2>/dev/null) || [ -z "$st" ]; then
|
||
missing_dep "查不到 PR $repo 第 $index 號的狀態。請確認存取庫、PR 編號、GITEA_HOST 與 GITEA_TOKEN 都對。"
|
||
fi
|
||
state=$(printf '%s\n' "$st" | awk '{print $1}')
|
||
merged=$(printf '%s\n' "$st" | awk '{print $2}')
|
||
# PR 不存在時 gitea.sh 印的是「? none none」並照樣 exit 0。認不得的欄位一律當成查不到,
|
||
# 不當成未合併也不當成合併:把打錯的 PR 編號當成「未合併」會讓呼叫端一直修不存在的留言。
|
||
case "$state:$merged" in
|
||
open:true|open:false|closed:true|closed:false) ;;
|
||
*) missing_dep "查不到 PR $repo 第 $index 號(pr-status 回「$st」)。請確認存取庫、PR 編號、GITEA_HOST 與 GITEA_TOKEN 都對。" ;;
|
||
esac
|
||
|
||
if [ "$merged" = 'true' ]; then
|
||
echo 'status=merged'
|
||
echo "$repo 第 $index 號 PR 已合併,這一包結清了,可以挑下一個工作包。"
|
||
gate=$(sdlc_gate_sh) || missing_dep "找不到 jsc-hooks 的 hooks/sdlc-gate.sh,鎖解不掉。並排版面請確認 {workspace}/hooks 存在,或設定 JSC_HOOKS_DIR 指向它的 hooks 目錄。"
|
||
# stdin 一定要關掉:sdlc-gate.sh 的 hook 模式會讀標準輸入,管線沒人關閉時整支卡死。
|
||
if sh "$gate" wp-unlock "$repo" >/dev/null 2>&1 </dev/null; then
|
||
echo "鎖已解除($repo)。"
|
||
else
|
||
echo "[jsc][工作包閘門][WARN]:鎖解不掉($repo)。PR 確實已合併,但狀態檔還在,hook 會一直提醒。請手動執行:sdlc-gate.sh wp-unlock $repo" >&2
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
if [ "$state" = 'closed' ]; then
|
||
echo 'status=closed-unmerged'
|
||
echo "$repo 第 $index 號 PR 被關掉了,但沒有合併——程式碼沒進到來源分支,這不算完成。"
|
||
echo '要嘛重開這支 PR 並把留言修完,要嘛請使用者裁決;在那之前不得挑下一個工作包。'
|
||
else
|
||
echo 'status=open'
|
||
echo "$repo 第 $index 號 PR 還開著,沒有合併。這一包還沒結清,不得挑下一個工作包。"
|
||
fi
|
||
echo '下列每一筆留言都要有結果(已修、不需修、修不動)。修不動或純討論、讚美的留言直接忽略:'
|
||
echo '不在 PR 上回覆、也不因此停下流程,但要在最終回報裡逐筆列出被忽略的留言與理由。'
|
||
if [ -n "$since" ]; then
|
||
echo "留言(只列比 $since 更新的;更早的已經處理過):"
|
||
else
|
||
echo '留言(全部,沒有給 --since):'
|
||
fi
|
||
|
||
tmp=$(mktemp) || missing_dep '建不出暫存檔,無法讀留言。'
|
||
if ! sh "$gitea" pr-comments "$repo" "$index" >"$tmp" 2>/dev/null; then
|
||
rm -f "$tmp"
|
||
echo "[jsc][工作包閘門][WARN]:留言讀不到($repo 第 $index 號)。PR 沒合併這件事不變,照樣擋住;請自行到 PR 頁面確認留言。" >&2
|
||
echo 'latest='
|
||
exit 1
|
||
fi
|
||
awk -F'\t' -v since="$since" '
|
||
NF == 0 { next }
|
||
{ if ($1 > latest) latest = $1 }
|
||
since == "" || $1 > since { print }
|
||
END { print "latest=" latest }
|
||
' "$tmp"
|
||
rm -f "$tmp"
|
||
exit 1 ;;
|
||
|
||
lock)
|
||
repo="${1:-}"; index="${2:-}"
|
||
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }
|
||
valid_index "$index" || { echo 'status=usage'; usage; exit 2; }
|
||
[ "$#" -le 2 ] || { echo 'status=usage'; usage; exit 2; }
|
||
|
||
gate=$(sdlc_gate_sh) || missing_dep "找不到 jsc-hooks 的 hooks/sdlc-gate.sh,鎖上不了。並排版面請確認 {workspace}/hooks 存在,已安裝版面請確認 jsc-hooks plugin 已安裝,或設定 JSC_HOOKS_DIR 指向它的 hooks 目錄。"
|
||
# stdin 一定要關掉,理由同 check 分支的 wp-unlock 呼叫。
|
||
if ! out=$(sh "$gate" wp-lock "$repo" "$index" 2>&1 </dev/null); then
|
||
echo 'status=missing-dep'
|
||
{
|
||
echo "[jsc][工作包閘門][ERR]:鎖上不了($repo 第 $index 號)。"
|
||
[ -n "$out" ] && printf '%s\n' "$out"
|
||
echo '沒記下等於沒鎖,閘門在下一個工作階段就會放行。請修好之後重跑。'
|
||
} >&2
|
||
exit 3
|
||
fi
|
||
echo 'status=locked'
|
||
echo "$repo 第 $index 號 PR 已記為未結清。合併之後跑 wp-gate.sh check 才會解鎖。"
|
||
exit 0 ;;
|
||
|
||
*)
|
||
echo 'status=usage'
|
||
echo "[jsc][工作包閘門][ERR]:不認得子命令「$sub」。" >&2
|
||
usage
|
||
exit 2 ;;
|
||
esac
|