說明跟腳本、技能的實際行為對不上,讀的人會照舊做法走。 - 工作包閘門腳本的檔頭補上上鎖的工作包參數。不帶就沒有歸屬,歸屬比對對誰都放行。 - 腳本用法數量改成正確的五個。 - 四段技能摘要改寫成現在的流程:閘門與停止條件先跑、互不相依的動作併發、寫入閘門只有 claude 擋得住、認可前的留言掃描是其餘四支 CLI 唯一來得及的一道。
546 lines
28 KiB
Bash
Executable File
546 lines
28 KiB
Bash
Executable File
#!/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 編號」,不是「相依欄裡的每一個字」:相依欄
|
||
# 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本
|
||
# 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就
|
||
# 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。
|
||
# - 工作包代號補零與否兩種寫法都會出現(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 目錄。"
|
||
|
||
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"
|
||
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 ;;
|
||
|
||
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
|