Files
sdlc/tools/wp-gate.sh
jiantw83andClaude Sonnet 5 59c198fb06 fix(sdlc): 修正 wp-gate check-deps 相依欄位解析錯位問題
原本 awk -F'|' 固定用第 6 欄當相依欄,但表格每列開頭多一個空欄,實際相依欄是第 7 欄,
導致每個候選工作包都被誤判成無相依可直接挑選,相依關卡形同虛設。同時 PR/狀態欄改用
從結尾往回數,避免層 0 多一欄「工作證」時與層 1 到 4 的表格欄位對不齊;並在切欄前先
處理儲存格內跳脫的字面 |,避免該列後續欄位整批錯位。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 10:07:33 +08:00

556 lines
30 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# wp-gate.sh — 工作包 PR 閘門:一個工作包的 PR 沒合併,擋的是相依於它的工作包,不是整份分析
# (供 jsc-sdlc:implement 呼叫;check-deps 判斷某個候選包能不能挑,check/lock 管單一 PR 本身)。
#
# 為什麼要有這支腳本:這條規則原本只寫在技能內文裡,靠模型自律遵守。內文靠不住——換一個
# 工作階段、換一個模型,或只是上下文被截掉,規則就跟著消失,而且沒有任何徵兆看得出來。
# 所以判定搬到程式層:未結清的狀態由 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} [--wp {wp-number}]
# PR 開好之後上鎖,讓閘門跨工作階段有效。轉呼叫 sdlc-gate.sh wp-lock。
# --wp 把這支 PR 掛到該工作包名下;不帶就沒有歸屬,owns 對誰都放行。
# wp-gate.sh check-deps {owner}/{repo} {wp-number} --analyze {分析頁頁名}
# 候選工作包能不能挑:活抓分析頁 WBS 表的相依欄,逐一核對每個相依工作包的狀態欄與
# PR 欄(PR 有給就活查 Gitea 是否已合併),不吃 implement 技能自己讀到的任何快取或判斷。
# 分析頁由呼叫端明確指定;沒帶 --analyze 直接拒絕,不用存取庫 hash 猜頁名。
# 只認同一張分析頁上的 WP-NN 編號;相依欄裡指到別份計畫或別頁分析的文字項目查不了,
# 照樣放行但會在說明裡列出來,要求人工確認。
# wp-gate.sh claim {owner}/{repo} {wp-number} [--analyze {分析頁頁名}]
# 領工作包當下登錄歸屬,轉呼叫 sdlc-gate.sh wp-claim(領包時還沒有 PR,PR 欄留空)。
# wp-gate.sh owns {owner}/{repo} {index} [--wp {wp-number}]
# 這支 PR 是不是自己這一包的:拿領取紀錄與鎖檔比對,不是就擋。
#
# 工作包歸屬狀態檔由 jsc-hooks 的 sdlc-gate.sh 產生,本檔只讀不寫:
# 領取檔 $JSC_HOME/wp/{owner}-{repo}.claim 欄位 repo=、wp=、pr=、analyze=、claimed=
# 鎖檔 $JSC_HOME/wp/{owner}-{repo}-{index}.pr 欄位 repo=、index=、wp=、locked=
# 都是純文字 key=value,一行一欄。工作包代號 WP-03、WP-3、3 視為同一包,比對前先正規化。
# 寫入一律走子命令(wp-claim、wp-lock 的第四個參數、wp-unclaim、wp-unlock),本檔不自己拼檔案:
# 兩邊各拼各的,格式一改就對不上,而且誰寫壞了看不出來。
# 歸屬一律以分析頁的工作包代號為準,不以分支名、不以目錄名——那兩個都可能被改。
#
# 查無歸屬就放行:
# 沒有分析頁、沒有領取紀錄、工作包還沒掛上 PR,都印 status=unowned 並 exit 0,只多印一行提醒。
# 理由跟「查不到就擋」不衝突:那條講的是閘門查得到卻查失敗,這條講的是根本還沒有歸屬可查。
# 拿不存在的歸屬去擋人,會讓沒登錄過的工作包全部動不了。
#
# 輸出: 第一行固定為 `status=...`(供程式判讀),其後為人類可讀的繁中說明。
# status=merged 已合併,鎖已解除,相依於它的工作包現在可以挑了
# status=open PR 還開著,沒有合併
# status=closed-unmerged PR 被關掉但沒有合併——這不算完成
# status=locked 已記下這筆未結清的 PR
# status=ready check-deps 專用:相依的工作包都已結清,可以挑
# status=blocked check-deps 專用:至少一個相依工作包還沒結清,不得挑
# status=claimed claim 專用:已記下這一包的歸屬
# status=owned owns 專用:這支 PR 屬於自己這一包,放行
# status=foreign owns 專用:這支 PR 屬於別的工作包,擋住
# status=unowned owns 專用:查無歸屬,放行只提醒
# status=usage 用法錯誤
# status=missing-dep 相依腳本找不到、PR 查不到,或分析頁/WBS 列查不到
# check 未合併時,最後一行固定為 `latest={最新一筆留言的時間戳}`(一筆留言都沒有就是 `latest=`),
# 供呼叫端寫回分析頁,下一輪拿它當 --since,已處理過的留言就不會再處理一遍。
#
# 結束碼: 0=已合併或無阻擋(含 check-deps 的 ready、claim 的 claimed、owns 的 owned 與 unowned)
# 1=未合併,或 check-deps 判定 blocked,或 owns 判定 foreign
# 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 {owner}/{repo} {index} 解鎖這一包;解鎖失敗照樣
# 回報 merged 並 exit 0,但會多印一行警示與手動指令——鎖沒清掉,hook 會一直提醒下去。
# - check-deps 只核對「同一張分析頁上的 WP-NN 編號」,不是「相依欄裡的每一個字」:相依欄
# 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本
# 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就
# 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。
# - --analyze 的頁名原樣送查,本檔不驗 hash 長度:完整 40 碼與還沒搬的舊 8 碼都收得下。
# 驗長度只會在頁名規則變動時把讀得到的頁擋成 missing-dep,判長度的正本在 jsc-gitea。
# - 工作包代號補零與否兩種寫法都會出現(WP-8、WP-08),比對前一律先過 wp_norm 正規化,
# 不然同一包的兩種寫法會被當成兩包,歸屬比對永遠對不上。
# - 領取檔一個存取庫一支,不是一個工作包一支。所以合併後交回領取紀錄之前要先確認
# 領取檔登記的正是這一包:中途改領別包時直接交回,會把還在進行的那一包的歸屬清掉。
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 check-deps {owner}/{repo} {wp-number} --analyze {頁名} 候選工作包能不能挑:核對它在分析頁上的相依工作包是否都已結清
wp-gate.sh claim {owner}/{repo} {wp-number} [--analyze {頁名}] 領包當下登錄歸屬
wp-gate.sh lock {owner}/{repo} {index} [--wp {wp-number}] 開完 PR 後上鎖並把 PR 掛到工作包名下,讓閘門跨工作階段有效
wp-gate.sh owns {owner}/{repo} {index} [--wp {wp-number}] 這支 PR 是不是自己這一包的:不是就擋,查無歸屬就放行
結束碼: 0=已合併或無阻擋(含 check-deps 的 ready、claim 的 claimed、owns 的 owned 與 unowned) 1=未合併、check-deps 判定 blocked,或 owns 判定 foreign 2=用法錯誤 3=相依工具或 PR/分析頁查不到,或歸屬登錄不了
EOF
}
# --- 工作包歸屬:狀態檔由 jsc-hooks 的 sdlc-gate.sh 產生,本檔只讀不寫(格式見檔頭)---
wp_dir() { printf '%s/wp' "${JSC_HOME:-$HOME/.jsc}"; }
# WP-3、wp-03、3 是同一包。比對前一律先正規化成 WP-03;不合格回 1,由呼叫端印用法。
wp_norm() { # $1=工作包代號
_v="${1:-}"
case "$_v" in
[Ww][Pp]-*) _v=${_v#[Ww][Pp]-} ;;
[Ww][Pp]*) _v=${_v#[Ww][Pp]} ;;
esac
case "$_v" in
''|*[!0-9]*) return 1 ;;
esac
_v=$(printf '%s' "$_v" | sed 's/^0*//'); [ -n "$_v" ] || _v=0
printf 'WP-%02d' "$_v"
}
claim_file() { # $1={owner}/{repo}
printf '%s/%s.claim' "$(wp_dir)" "$(printf '%s' "$1" | tr '/' '-')"
}
lock_file() { # $1={owner}/{repo} $2=PR 編號
printf '%s/%s-%s.pr' "$(wp_dir)" "$(printf '%s' "$1" | tr '/' '-')" "$2"
}
wp_field() { # $1=狀態檔 $2=欄位名 -> 印出值,檔案或欄位不存在就印空字串
[ -f "$1" ] || return 0
sed -n "s/^$2=//p" "$1" 2>/dev/null | head -n1
}
# 某支 PR 登記在哪一個工作包名下;查不到印空字串。擋人時要講得出對方是誰,
# 只說「不是你的」,使用者分不出是自己領錯包,還是狀態檔留了上一輪的殘骸。
wp_of_pr() { # $1={owner}/{repo} $2=PR 編號
_w=$(wp_field "$(lock_file "$1" "$2")" wp)
[ -n "$_w" ] || { printf ''; return 0; }
wp_norm "$_w" || printf '%s' "$_w"
}
# 領取檔登記的工作包目前掛著哪一支 PR:領取檔的 pr 欄優先,空的就掃鎖檔補。
pr_of_wp() { # $1={owner}/{repo} $2=正規化後的 WP-NN
_p=$(wp_field "$(claim_file "$1")" pr)
if [ -n "$_p" ]; then printf '%s' "$_p"; return 0; fi
for _f in "$(wp_dir)/$(printf '%s' "$1" | tr '/' '-')"-*.pr; do
[ -f "$_f" ] || continue
_w=$(wp_field "$_f" wp)
[ -n "$_w" ] || continue
_w=$(wp_norm "$_w") || continue
[ "$_w" = "$2" ] || continue
wp_field "$_f" index
return 0
done
printf ''
}
# 找相依腳本。兩種版面都要顧到,否則腳本只在其中一種版面下會動:
# 並排存取庫(開發用):{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 模式會讀標準輸入,管線沒人關閉時整支卡死。
# 鎖檔一個工作包一支,解鎖要帶 index,只解掉這一包;其餘平行進行的工作包不受影響。
if sh "$gate" wp-unlock "$repo" "$index" >/dev/null 2>&1 </dev/null; then
echo "鎖已解除($repo 第 $index 號)。"
else
echo "[jsc][工作包閘門][WARN]:鎖解不掉($repo 第 $index 號)。PR 確實已合併,但狀態檔還在,hook 會一直提醒。請手動執行:sdlc-gate.sh wp-unlock $repo $index" >&2
fi
# 領取紀錄一併交回:這一包已經結清,留著會讓下一輪的 owns 拿舊 PR 編號比對。
# 只有領取檔登記的正是這一包才交回——同一個存取庫可能已經改領別的包了。
claimed_wp=$(wp_field "$(claim_file "$repo")" wp)
[ -n "$claimed_wp" ] && claimed_wp=$(wp_norm "$claimed_wp")
merged_wp=$(wp_of_pr "$repo" "$index")
if [ -n "$claimed_wp" ] && { [ -z "$merged_wp" ] || [ "$claimed_wp" = "$merged_wp" ]; }; then
if sh "$gate" wp-unclaim "$repo" >/dev/null 2>&1 </dev/null; then
echo "領取紀錄已交回($repo 的 $claimed_wp)。"
else
echo "[jsc][工作包閘門][WARN]:領取紀錄交不回($repo 的 $claimed_wp)。這一包確實結清了,但紀錄還在,下一輪的 owns 會拿舊 PR 比對。請手動執行:sdlc-gate.sh wp-unclaim $repo" >&2
fi
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 '這一包沒結清,只表示相依於它的工作包不能開始;其餘互不相依的工作包能不能挑,見 wp-gate.sh check-deps。'
echo '下列每一筆留言都要有結果(已修、不需修、修不動)。已處理的留言要用 gitea.sh comment-reply 回覆:'
echo '純討論或讚美可以不回覆、也不因此停下流程,但要在最終回報裡逐筆列出被忽略的留言與理由。'
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 ;;
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; }
shift 2
analyze=''
while [ "$#" -gt 0 ]; do
case "$1" in
--analyze) analyze="${2:-}"; [ -n "$analyze" ] || { echo 'status=usage'; usage; exit 2; }; shift 2 ;;
--analyze=*) analyze=${1#--analyze=}; [ -n "$analyze" ] || { echo 'status=usage'; usage; exit 2; }; shift ;;
*) echo 'status=usage'; usage; exit 2 ;;
esac
done
[ -n "$analyze" ] || {
echo 'status=usage'
echo '[jsc][工作包閘門][ERR]:缺少 --analyze ANALYZE_{HASH},check-deps 不會用存取庫 hash 猜分析頁。' >&2
usage
exit 2
}
gitea=$(gitea_sh) || missing_dep "找不到 jsc-gitea 的 tools/gitea.sh。並排版面請確認 {workspace}/gitea 存在,已安裝版面請確認 jsc-gitea plugin 已安裝,或設定 JSC_GITEA_TOOLS 指向它的 tools 目錄。"
# 分析頁是內容頁,住在 ANALYZE 型別的存取庫,不是目錄頁那個 CONTENTS 專用存取庫。
# 兩者分家之後這裡最容易被順手改成 CONTENTS:改了就再也讀不到 WBS 表,
# 每個候選工作包都會判成 missing-dep,等於閘門整個停擺。
wiki_repo=$(sh "$gitea" wiki-repo ANALYZE 2>/dev/null)
[ -n "$wiki_repo" ] || missing_dep "解析不出 ANALYZE 內容頁的 wiki 存取庫(JSC_WIKI_REPO_ANALYZE 與 JSC_WIKI_REPO 都未設定)。目錄頁的 JSC_WIKI_REPO_CONTENTS 不供這裡退讓。"
page="$analyze"
content=$(sh "$gitea" wiki-get "$wiki_repo" "$page" 2>/dev/null)
[ -n "$content" ] || missing_dep "讀不到分析頁 $wiki_repo 的 $page。"
# markdown 表格用 \| 跳脫儲存格裡的字面 |(例如描述某個分隔符本身就是 |);
# 下面逐欄用 awk -F'|' 切列,不先把跳脫符換掉,這一格會多出一欄,
# 害同一列後面每一欄全部錯位。先把 \| 換成控制字元,切完欄位再也用不到它,不必換回來。
content=$(printf '%s' "$content" | sed $'s/\\\\|/\x01/g')
# WBS 表欄位(以 | 分隔,$1 是第一個 | 之前的空字串,$NF 是結尾 | 之後的空字串):
# $2=編號 $7=相依。層 0 比層 1 到 4 多一欄「工作證」,PR 與狀態的絕對欄號因此不同,
# 一律從結尾往回數:倒數第一格是狀態、倒數第二格是 PR,兩種欄位數的表格都對得上。
# 找列一律比數值,不比字串:範本固定把編號補零到兩位數,但呼叫端或相依欄裡的寫法
# 不一定補零;比字串會讓兩種寫法各自找不到對方那一列,誤判成「查不到」。
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 = $7; 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 = $(NF-1); gsub(/^[ \t]+|[ \t]+$/, "", s); print s }')
if [ "$dep_status" = '已完成' ]; then
continue
fi
dep_pr=$(printf '%s' "$dep_row" | awk -F'|' '{ print $(NF-2) }')
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 ;;
claim)
repo="${1:-}"; wp_arg="${2:-}"
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }
wp=$(wp_norm "$wp_arg") || { echo 'status=usage'; usage; exit 2; }
shift 2
analyze=''
while [ "$#" -gt 0 ]; do
case "$1" in
--analyze) analyze="${2:-}"; [ -n "$analyze" ] || { echo 'status=usage'; usage; exit 2; }; shift 2 ;;
--analyze=*) analyze=${1#--analyze=}; [ -n "$analyze" ] || { echo 'status=usage'; usage; exit 2; }; shift ;;
*) echo 'status=usage'; usage; exit 2 ;;
esac
done
gate=$(sdlc_gate_sh) || missing_dep "找不到 jsc-hooks 的 hooks/sdlc-gate.sh,歸屬登錄不了。並排版面請確認 {workspace}/hooks 存在,已安裝版面請確認 jsc-hooks plugin 已安裝,或設定 JSC_HOOKS_DIR 指向它的 hooks 目錄。"
# 領包當下還沒有 PR,PR 編號位置給空字串佔位,分析頁頁名才排得到第四個參數。
# stdin 一定要關掉,理由同 check 分支的 wp-unlock 呼叫。
if ! out=$(sh "$gate" wp-claim "$repo" "$wp" '' "$analyze" 2>&1 </dev/null); then
echo 'status=missing-dep'
{
echo "[jsc][工作包閘門][ERR]:登錄不了 $repo 的 $wp。"
[ -n "$out" ] && printf '%s\n' "$out"
echo '沒登錄等於查無歸屬,之後每一支 PR 都會放行,隔離形同不存在。請修好之後重跑。'
} >&2
exit 3
fi
echo 'status=claimed'
echo "$repo 的 $wp 已登錄為本工作階段領取的工作包。開完 PR 再跑 wp-gate.sh lock $repo {index} --wp $wp 把 PR 掛上去。"
exit 0 ;;
owns)
repo="${1:-}"; index="${2:-}"
valid_repo "$repo" || { echo 'status=usage'; usage; exit 2; }
valid_index "$index" || { echo 'status=usage'; usage; exit 2; }
shift 2
wp=''
while [ "$#" -gt 0 ]; do
case "$1" in
--wp) wp=$(wp_norm "${2:-}") || { echo 'status=usage'; usage; exit 2; }; shift 2 ;;
--wp=*) wp=$(wp_norm "${1#--wp=}") || { echo 'status=usage'; usage; exit 2; }; shift ;;
*) echo 'status=usage'; usage; exit 2 ;;
esac
done
cf=$(claim_file "$repo")
claimed_wp=$(wp_field "$cf" wp)
[ -n "$claimed_wp" ] && claimed_wp=$(wp_norm "$claimed_wp")
if [ -z "$claimed_wp" ]; then
echo 'status=unowned'
echo "$repo 沒有領取紀錄($cf),判定不出第 $index 號 PR 的歸屬,本次放行。"
echo '領包時跑過 wp-gate.sh claim 才會有這筆紀錄,隔離也才會生效。'
exit 0
fi
# --wp 沒帶就用領取檔登記的工作包;帶了就要跟領取檔一致,不一致代表這個工作階段
# 根本沒領那一包,照樣不得處理它的 PR。
if [ -n "$wp" ] && [ "$wp" != "$claimed_wp" ]; then
echo 'status=foreign'
echo "$repo 目前領的是 $claimed_wp,不是 $wp,不得處理 $wp 的第 $index 號 PR。"
echo "要換包請先跑 sdlc-gate.sh wp-unclaim $repo 交回,再重新領取。"
exit 1
fi
[ -n "$wp" ] || wp="$claimed_wp"
own_pr=$(pr_of_wp "$repo" "$wp")
if [ -z "$own_pr" ]; then
echo 'status=unowned'
echo "$wp 還沒掛上任何 PR,比對不了第 $index 號 PR 的歸屬,本次放行。"
echo "開完 PR 跑 wp-gate.sh lock $repo {index} --wp $wp 之後才比對得了。"
exit 0
fi
if [ "$own_pr" = "$index" ]; then
echo 'status=owned'
echo "$repo 第 $index 號 PR 就是 $wp 這一包的,放行。"
exit 0
fi
other=$(wp_of_pr "$repo" "$index")
echo 'status=foreign'
echo "$repo 第 $index 號 PR 不屬於 $wp($wp 掛的是第 $own_pr 號),不得處理。"
if [ -n "$other" ]; then
echo "那支 PR 掛在 $other 名下,請由領該包的工作階段處理。"
else
echo '狀態檔裡沒有哪一包掛著這支 PR,請先確認分析頁的 PR 欄與工作包編號對不對得上。'
fi
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; }
shift 2
wp=''
while [ "$#" -gt 0 ]; do
case "$1" in
--wp) wp=$(wp_norm "${2:-}") || { echo 'status=usage'; usage; exit 2; }; shift 2 ;;
--wp=*) wp=$(wp_norm "${1#--wp=}") || { echo 'status=usage'; usage; exit 2; }; shift ;;
*) echo 'status=usage'; usage; exit 2 ;;
esac
done
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 呼叫。
# 工作包編號當第四個參數往下傳:hook 認得就把它寫進同一支狀態檔,不認得就原樣忽略,
# 兩種版本都不會壞。不傳的話 hook 只好另開一支以 PR 編號命名的檔,同一包生出兩份狀態。
if ! out=$(sh "$gate" wp-lock "$repo" "$index" "$wp" 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 才會解鎖。"
if [ -n "$wp" ]; then
echo "歸屬已記下:$wp ← 第 $index 號 PR。"
else
echo '[jsc][工作包閘門][WARN]:沒帶 --wp,這支 PR 掛不到任何工作包名下,owns 對它一律放行。分析頁上查得到工作包代號時請補帶 --wp。' >&2
fi
exit 0 ;;
*)
echo 'status=usage'
echo "[jsc][工作包閘門][ERR]:不認得子命令「$sub」。" >&2
usage
exit 2 ;;
esac