From b0dcdaab249890b2de4e14b93ab8500218cadea5 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(wp-gate):=20=E6=96=B0=E5=A2=9E=20claim=20?= =?UTF-8?q?=E8=88=87=20owns=EF=BC=8C=E6=8A=8A=E5=B7=A5=E4=BD=9C=E5=8C=85?= =?UTF-8?q?=E6=AD=B8=E5=B1=AC=E5=AF=AB=E9=80=B2=E9=96=98=E9=96=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:`tools/wp-gate.sh` 新增 `claim {owner}/{repo} {wp-number} [--analyze {分析頁頁名}]` 登錄領取、`owns {owner}/{repo} {index} [--wp {wp-number}]` 比對某支 PR 是不是自己這一包的;`lock` 新增 `--wp` 把 PR 掛到工作包名下;`check` 判定已合併時一併交回領取紀錄。新增狀態 `claimed`、`owned`、`foreign`、`unowned`,`foreign` 回 `1`,`owned` 與 `unowned` 回 `0`。 Why:同一份分析常有好幾包平行進行,每包各自的 worktree 與 PR。動任何一支 PR 之前要先確認它是自己這一包的,靠的必須是狀態檔而不是記憶——記憶跨不了工作階段。 How:歸屬一律以分析頁的工作包代號為準,不看分支名、不看 worktree 目錄名,那兩個都可能被改,改了也沒有徵兆。`WP-03`、`WP-3`、`3` 比對前先正規化成 `WP-03`,不然同一包的兩種寫法會被當成兩包。狀態檔的寫入一律轉呼叫 `jsc-hooks` 的 `sdlc-gate.sh`,本檔只讀不寫:兩邊各拼各的檔案,格式一改就對不上。查無歸屬(沒有領取紀錄、工作包還沒掛上 PR)一律 `unowned` 並放行只提醒,這與「查不到就擋」不衝突——那條講的是查得到卻查失敗,這條講的是根本還沒有歸屬可查。擋人時要講得出對方是誰,所以 `foreign` 會指出那支 PR 掛在哪一包名下。合併後交回領取紀錄前先確認領取檔登記的正是這一包,中途改領別包時直接交回會把還在進行的那一包的歸屬清掉。 Who:`jsc-sdlc:implement` 領包、開 PR 與處理留言的每一步,以及跨工作階段接手同一份分析的人。 --- tools/wp-gate.sh | 206 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 200 insertions(+), 6 deletions(-) diff --git a/tools/wp-gate.sh b/tools/wp-gate.sh index 8a829cc..3dc14e7 100755 --- a/tools/wp-gate.sh +++ b/tools/wp-gate.sh @@ -21,6 +21,23 @@ # PR 欄(PR 有給就活查 Gitea 是否已合併),不吃 implement 技能自己讀到的任何快取或判斷。 # 只認同一張分析頁上的 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 已合併,鎖已解除,相依於它的工作包現在可以挑了 @@ -29,13 +46,18 @@ # 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) 1=未合併,或 check-deps 判定 blocked -# 2=用法錯誤 3=相依工具或 PR/分析頁查不到 +# 結束碼: 0=已合併或無阻擋(含 check-deps 的 ready、claim 的 claimed、owns 的 owned 與 unowned) +# 1=未合併,或 check-deps 判定 blocked,或 owns 判定 foreign +# 2=用法錯誤 3=相依工具或 PR/分析頁查不到,或歸屬登錄不了 # # 陷阱: # - 「state=closed 但 merged=false」最容易被當成完成:那是 PR 被關掉、程式碼沒進去, @@ -53,6 +75,10 @@ # 若寫的是另一份計畫或另一張分析頁的敘述(例如「節點建置」這種跨頁文字依賴),這支腳本 # 沒有能力去查那邊的狀態,只能原樣列出來要求人工確認,不會拿它當擋人的理由——結構上就 # 查不到的東西當成擋人的理由,跟前一條「查不到就擋」矛盾,會讓那個工作包永遠挑不到。 +# - 工作包代號補零與否兩種寫法都會出現(WP-8、WP-08),比對前一律先過 wp_norm 正規化, +# 不然同一包的兩種寫法會被當成兩包,歸屬比對永遠對不上。 +# - 領取檔一個存取庫一支,不是一個工作包一支。所以合併後交回領取紀錄之前要先確認 +# 領取檔登記的正是這一包:中途改領別包時直接交回,會把還在進行的那一包的歸屬清掉。 set -u script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) @@ -63,11 +89,67 @@ usage() { 用法: 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=已合併或無阻擋(含 check-deps 的 ready) 1=未合併,或 check-deps 判定 blocked 2=用法錯誤 3=相依工具或 PR/分析頁查不到 + 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 各有版本目錄,取排序最後的一份(通常即最新版) @@ -175,6 +257,18 @@ case "$sub" in 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 &2 + fi + fi exit 0 fi @@ -308,15 +402,110 @@ case "$sub" in [ -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 &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; } - [ "$#" -le 2 ] || { 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 呼叫。 - if ! out=$(sh "$gate" wp-lock "$repo" "$index" 2>&1 &1 &2 + fi exit 0 ;; *)