fix(implement): 工作包 PR 閘門改為只檢查候選工作包自身相依,並修正平行工作包共用 worktree 路徑衝突

This commit is contained in:
2026-08-26 17:59:08 +08:00
parent 41bcbf0f1c
commit 87d73034c5
5 changed files with 150 additions and 30 deletions
+126 -14
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
# wp-gate.sh — 工作包 PR 閘門:一個工作包的 PR 沒合併,就不准開下一包(供 jsc-sdlc:implement 呼叫)。
# wp-gate.sh — 工作包 PR 閘門:一個工作包的 PR 沒合併,擋的是相依於它的工作包,不是整份分析
# (供 jsc-sdlc:implement 呼叫;check-deps 判斷某個候選包能不能挑,check/lock 管單一 PR 本身)。
#
# 為什麼要有這支腳本:這條規則原本只寫在技能內文裡,靠模型自律遵守。內文靠不住——換一個
# 工作階段、換一個模型,或只是上下文被截掉,規則就跟著消失,而且沒有任何徵兆看得出來。
@@ -15,19 +16,26 @@
# --since 只印比該時間更新的留言,值用上一輪印出的 latest=(UTC,形如 2026-08-25T10:19:59Z)。
# wp-gate.sh lock {owner}/{repo} {index}
# PR 開好之後上鎖,讓閘門跨工作階段有效。轉呼叫 sdlc-gate.sh wp-lock。
# wp-gate.sh check-deps {owner}/{repo} {wp-number}
# 候選工作包能不能挑:活抓分析頁 WBS 表的相依欄,逐一核對每個相依工作包的狀態欄與
# PR 欄(PR 有給就活查 Gitea 是否已合併),不吃 implement 技能自己讀到的任何快取或判斷。
# 只認同一張分析頁上的 WP-NN 編號;相依欄裡指到別份計畫或別頁分析的文字項目查不了,
# 照樣放行但會在說明裡列出來,要求人工確認。
#
# 輸出: 第一行固定為 `status=...`(供程式判讀),其後為人類可讀的繁中說明。
# status=merged 已合併,鎖已解除,可以挑下一個工作包
# status=merged 已合併,鎖已解除,相依於它的工作包現在可以挑了
# status=open PR 還開著,沒有合併
# status=closed-unmerged PR 被關掉但沒有合併——這不算完成
# status=locked 已記下這筆未結清的 PR
# status=ready check-deps 專用:相依的工作包都已結清,可以挑
# status=blocked check-deps 專用:至少一個相依工作包還沒結清,不得挑
# status=usage 用法錯誤
# status=missing-dep 相依腳本找不到,或這支 PR 查不到
# status=missing-dep 相依腳本找不到、PR 查不到,或分析頁/WBS 列查不到
# check 未合併時,最後一行固定為 `latest={最新一筆留言的時間戳}`(一筆留言都沒有就是 `latest=`),
# 供呼叫端寫回分析頁,下一輪拿它當 --since,已處理過的留言就不會再處理一遍。
#
# 結束碼: 0=已合併或無阻擋 1=未合併(擋住,呼叫端必須停下來逐筆修留言)
# 2=用法錯誤 3=相依工具或 PR 查不到
# 結束碼: 0=已合併或無阻擋(含 check-deps 的 ready) 1=未合併,或 check-deps 判定 blocked
# 2=用法錯誤 3=相依工具或 PR/分析頁查不到
#
# 陷阱:
# - 「state=closed 但 merged=false」最容易被當成完成:那是 PR 被關掉、程式碼沒進去,
@@ -39,8 +47,12 @@
# 餵進帶 +08:00 這類偏移的值會比錯,所以只餵上一輪的 latest=。
# - latest= 取的是「全部留言」裡最新的一筆,不是過濾後那幾筆。取過濾後的會在沒有新留言時
# 倒退回舊時間戳,下一輪又把處理過的留言全部翻出來。
# - 已合併時會呼叫 sdlc-gate.sh wp-unlock 解鎖;解鎖失敗照樣回報 merged 並 exit 0,但會多印
# 一行警示與手動指令——鎖沒清掉,hook 會一直提醒下去。
# - 已合併時會呼叫 sdlc-gate.sh wp-unlock {owner}/{repo} {index} 解鎖這一包;解鎖失敗照樣
# 回報 merged 並 exit 0,但會多印一行警示與手動指令——鎖沒清掉,hook 會一直提醒下去。
# - check-deps 只核對「同一張分析頁上的 WP-NN 編號」,不是「相依欄裡的每一個字」:相依欄
# 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本
# 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就
# 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。
set -u
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
@@ -50,8 +62,9 @@ usage() {
cat >&2 <<'EOF'
用法:
wp-gate.sh check {owner}/{repo} {index} [--since {ISO 時間}] 查 PR:合併就解鎖放行,沒合併就印出留言並擋住
wp-gate.sh check-deps {owner}/{repo} {wp-number} 候選工作包能不能挑:核對它在分析頁上的相依工作包是否都已結清
wp-gate.sh lock {owner}/{repo} {index} 開完 PR 後上鎖,讓閘門跨工作階段有效
結束碼: 0=已合併或無阻擋 1=未合併(擋住) 2=用法錯誤 3=相依工具或 PR 查不到
結束碼: 0=已合併或無阻擋(含 check-deps 的 ready) 1=未合併,或 check-deps 判定 blocked 2=用法錯誤 3=相依工具或 PR/分析頁查不到
EOF
}
@@ -153,13 +166,14 @@ case "$sub" in
if [ "$merged" = 'true' ]; then
echo 'status=merged'
echo "$repo 第 $index 號 PR 已合併,這一包結清了,可以挑下一個工作包。"
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)。"
# 鎖檔一個工作包一支,解鎖要帶 index,只解掉這一包;其餘平行進行的工作包不受影響。
if sh "$gate" wp-unlock "$repo" "$index" >/dev/null 2>&1 </dev/null; then
echo "鎖已解除($repo 第 $index 號)。"
else
echo "[jsc][工作包閘門][WARN]:鎖解不掉($repo)。PR 確實已合併,但狀態檔還在,hook 會一直提醒。請手動執行:sdlc-gate.sh wp-unlock $repo" >&2
echo "[jsc][工作包閘門][WARN]:鎖解不掉($repo 第 $index 號)。PR 確實已合併,但狀態檔還在,hook 會一直提醒。請手動執行:sdlc-gate.sh wp-unlock $repo $index" >&2
fi
exit 0
fi
@@ -167,11 +181,12 @@ case "$sub" in
if [ "$state" = 'closed' ]; then
echo 'status=closed-unmerged'
echo "$repo 第 $index 號 PR 被關掉了,但沒有合併——程式碼沒進到來源分支,這不算完成。"
echo '要嘛重開這支 PR 並把留言修完,要嘛請使用者裁決;在那之前不得挑下一個工作包。'
echo '要嘛重開這支 PR 並把留言修完,要嘛請使用者裁決;在那之前這一包不算結清。'
else
echo 'status=open'
echo "$repo 第 $index 號 PR 還開著,沒有合併。這一包還沒結清,不得挑下一個工作包。"
echo "$repo 第 $index 號 PR 還開著,沒有合併。這一包還沒結清。"
fi
echo '這一包沒結清,只表示相依於它的工作包不能開始;其餘互不相依的工作包能不能挑,見 wp-gate.sh check-deps。'
echo '下列每一筆留言都要有結果(已修、不需修、修不動)。修不動或純討論、讚美的留言直接忽略:'
echo '不在 PR 上回覆、也不因此停下流程,但要在最終回報裡逐筆列出被忽略的留言與理由。'
if [ -n "$since" ]; then
@@ -196,6 +211,103 @@ case "$sub" in
rm -f "$tmp"
exit 1 ;;
check-deps)
repo="${1:-}"; wp_number="${2:-}"
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }
valid_index "$wp_number" || { echo 'status=usage'; usage; exit 2; }
[ "$#" -le 2 ] || { echo 'status=usage'; usage; exit 2; }
gitea=$(gitea_sh) || missing_dep "找不到 jsc-gitea 的 tools/gitea.sh。並排版面請確認 {workspace}/gitea 存在,已安裝版面請確認 jsc-gitea plugin 已安裝,或設定 JSC_GITEA_TOOLS 指向它的 tools 目錄。"
hash=$(sh "$gitea" hash-id "$repo" 2>/dev/null)
[ -n "$hash" ] || missing_dep "算不出 $repo 的 wiki HASH。"
wiki_repo=$(sh "$gitea" wiki-repo ANALYZE 2>/dev/null)
[ -n "$wiki_repo" ] || missing_dep "解析不出 ANALYZE 頁的 wiki 存取庫(JSC_WIKI_REPO_ANALYZE 與 JSC_WIKI_REPO 都未設定)。"
page="ANALYZE_${hash}"
content=$(sh "$gitea" wiki-get "$wiki_repo" "$page" 2>/dev/null)
[ -n "$content" ] || missing_dep "讀不到分析頁 $wiki_repo 的 $page。"
# WBS 表欄位(以 | 分隔,$1 是第一個 | 之前的空字串):
# $2=編號 $6=相依 $11=PR $12=狀態。
# 找列一律比數值,不比字串:範本固定兩位數補零(WP-08),但呼叫端或相依欄裡的寫法
# 不一定補零(WP-8);比字串會讓兩種寫法各自找不到對方那一列,誤判成「查不到」。
find_wp_row() { # $1=WP 編號(可帶或不帶前導零) -> 印出該列,找不到印空字串
_n=$(printf '%s' "$1" | sed 's/^0*//'); [ -n "$_n" ] || _n=0
printf '%s\n' "$content" | awk -F'|' -v n="$_n" '
{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w)
if (w ~ /^WP-[0-9]+$/) {
wn = w; sub(/^WP-0*/, "", wn); if (wn == "") wn = "0"
if (wn + 0 == n + 0) { print; exit }
}
}'
}
row=$(find_wp_row "$wp_number")
[ -n "$row" ] || missing_dep "分析頁 $page 上找不到 WP-$wp_number 這一列,核對不了相依。"
self_wp=$(printf '%s' "$row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }')
deps=$(printf '%s' "$row" | awk -F'|' '{ d = $6; gsub(/^[ \t]+|[ \t]+$/, "", d); print d }')
case "$deps" in
''|-|無)
echo 'status=ready'
echo "$self_wp 沒有相依,可以挑。"
exit 0 ;;
esac
unmet=''
unknown=''
# 用 sed 把分隔符號換成換行、靠 IFS 只切換行:頓號是多位元組字元,tr 逐位元組處理
# 這種字元集合會把緊鄰的中文字元切壞(例如「節點建置」被腰斬成亂碼)。
_old_ifs=$IFS
IFS='
'
for dep in $(printf '%s\n' "$deps" | sed 's/、/\
/g; s/,/\
/g; s/,/\
/g'); do
IFS=$_old_ifs
dep_idx=$(printf '%s' "$dep" | sed -n 's/.*WP-\([0-9][0-9]*\).*/\1/p')
if [ -z "$dep_idx" ]; then
# 不是本頁的 WP-NN 編號(例如指到別份計畫、別張分析頁的文字敘述):查不了,
# 但不能因為查不了就一律擋住——那會讓這種工作包永遠挑不到。列出來要求人工確認。
[ -n "$dep" ] && unknown="${unknown}${unknown:+、}$dep"
continue
fi
dep_row=$(find_wp_row "$dep_idx")
if [ -z "$dep_row" ]; then
unmet="${unmet}${unmet:+、}WP-$dep_idx(分析頁上查不到這一列)"
continue
fi
dep_wp=$(printf '%s' "$dep_row" | awk -F'|' '{ w = $2; gsub(/^[ \t]+|[ \t]+$/, "", w); print w }')
dep_status=$(printf '%s' "$dep_row" | awk -F'|' '{ s = $12; gsub(/^[ \t]+|[ \t]+$/, "", s); print s }')
if [ "$dep_status" = '已完成' ]; then
continue
fi
dep_pr=$(printf '%s' "$dep_row" | awk -F'|' '{ print $11 }')
pr_index=$(printf '%s' "$dep_pr" | sed -n 's#.*/pulls/\([0-9][0-9]*\).*#\1#p' | head -n1)
if [ -z "$pr_index" ]; then
unmet="${unmet}${unmet:+、}${dep_wp}(狀態「${dep_status:-未完成}」,還沒開 PR)"
continue
fi
st=$(sh "$gitea" pr-status "$repo" "$pr_index" 2>/dev/null) || st=''
merged=$(printf '%s' "$st" | awk '{print $2}')
if [ "$merged" != 'true' ]; then
unmet="${unmet}${unmet:+、}${dep_wp}(第 $pr_index 號 PR 未合併)"
fi
done
if [ -n "$unmet" ]; then
echo 'status=blocked'
echo "$self_wp 相依 $unmet 尚未結清,不得挑選。"
[ -n "$unknown" ] && echo "另有查不了的相依項目(非本頁工作包編號,需人工確認):$unknown"
exit 1
fi
echo 'status=ready'
echo "$self_wp 在本頁上的相依工作包全部已結清,可以挑。"
[ -n "$unknown" ] && echo "有查不了的相依項目(非本頁工作包編號,需人工確認是否已完成):$unknown"
exit 0 ;;
lock)
repo="${1:-}"; index="${2:-}"
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }