From 5471b9763560389156a1c943c4946bc907572f4b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 27 Aug 2026 11:20:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(pr-watch):=20=E6=96=B0=E5=A2=9E=E7=9B=AF?= =?UTF-8?q?=20PR=20=E5=88=B0=E5=90=88=E4=BD=B5=E7=9A=84=E8=BC=AA=E8=A9=A2?= =?UTF-8?q?=E8=85=B3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What:新增 `tools/pr-watch.sh`,盯一支 PR 直到它合併或關閉。退出碼 `0` 是已合併或已關閉、`10` 是有新留言要呼叫端接手、`2` 是參數錯誤或第一輪就連不上 Gitea、`3` 是查不到該 PR。輪詢間隔預設 60 秒,`JSC_PR_WATCH_INTERVAL` 可覆寫。 Why:等 PR 合併原本靠各技能自己寫輪詢迴圈,間隔與退場條件每支都不一樣,還常常自動逾時退場,PR 明明還開著就被當成收尾。改用一支共用腳本之後,等待行為只有一種,而且盯到 PR 收尾為止。 How:資料一律經同目錄的 `gitea.sh`(`pr-status`、`pr-comments`),不自行拼 API。已回報過的留言把最後一筆時間戳寫進狀態檔,預設 `$JSC_HOME/pr-watch/{owner}-{repo}-{編號}.seen`,同一則不重複回報;狀態檔還不存在時把既有留言整批回報一次,寧可重複也不漏掉開始盯之前的審查意見。PR 收尾那一輪若還有沒回報的留言,照樣印出來再退出,退出碼仍是 `0`。`pr-status` 對不存在的 PR 會印「? none none」並正常結束,所以 state 只認 `open` 與 `closed`、merged 只認 `true` 與 `false`,對不上就回 `3`,PR 編號打錯不會變成永遠輪詢一支不存在的 PR。 Who:`jsc-sdlc:implement` 開完工作包 PR 之後的等待合併,以及任何要盯 PR 的技能。 --- tools/pr-watch.sh | 128 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100755 tools/pr-watch.sh diff --git a/tools/pr-watch.sh b/tools/pr-watch.sh new file mode 100755 index 0000000..0bfdc52 --- /dev/null +++ b/tools/pr-watch.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env sh +# pr-watch.sh — 盯著一支 PR,直到它合併或關閉。 +# 用法: pr-watch.sh / [state-file] +# 規則: 資料一律經同目錄的 gitea.sh(pr-status、pr-comments),不自行拼 API。 +# 輪詢間隔預設 60 秒,JSC_PR_WATCH_INTERVAL 可覆寫,單位是秒的正整數。 +# 不自動逾時退場,盯到 PR 合併或關閉為止。 +# 已回報過的留言不重複回報:最後處理過的留言時間戳寫進狀態檔, +# 預設 $JSC_HOME/pr-watch/{owner}-{repo}-{index}.seen,JSC_HOME 未設定時用 ~/.jsc。 +# 狀態檔還不存在時,把既有留言整批當成新留言回報一次。寧可重複回報, +# 也不要漏掉開始盯之前就留下的審查意見。 +# 每輪先看 PR 狀態,再看留言。PR 收尾那一輪若還有沒回報的留言, +# 照樣印出來再退出,退出碼仍是 0:留言不會因為 PR 剛好合併就消失。 +# 輸出: 全部繁中,留言行維持 gitea.sh pr-comments 的原格式 +# 「{時間}{作者}{類型}{內容}」,呼叫端可直接用 cut -f 取欄位。 +# 退出碼: 0 = PR 已合併或關閉,印出最終狀態;10 = 有新留言待呼叫端跑決策樹; +# 2 = 參數錯誤或第一輪就連不上 Gitea;3 = 查不到該 PR。 +# 護欄: gitea.sh pr-status 對不存在的 PR 會印「? none none」並且 exit 0。 +# 所以 state 只認 open 與 closed、merged 只認 true 與 false,對不上就回 3。 +# 少了這道白名單,PR 編號打錯會變成永遠輪詢一支不存在的 PR。 +# 第一輪查詢失敗算環境沒接好,直接回 2;之後才失敗算連線抖動,警告一句繼續輪詢。 +set -u + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +GITEA="$script_dir/gitea.sh" + +usage() { + echo "用法: pr-watch.sh / [state-file]" >&2 + exit 2 +} + +[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage +REPO="$1"; INDEX="$2"; STATE="${3:-}" + +case "$REPO" in + */*) ;; + *) echo "錯誤: 第一個參數要寫成 {owner}/{repo},收到「$REPO」。" >&2; exit 2 ;; +esac +case "$INDEX" in + ''|*[!0-9]*) echo "錯誤: PR 編號要是數字,收到「$INDEX」。" >&2; exit 2 ;; +esac + +INTERVAL="${JSC_PR_WATCH_INTERVAL:-60}" +case "$INTERVAL" in + ''|*[!0-9]*) echo "錯誤: JSC_PR_WATCH_INTERVAL 要是正整數秒,收到「$INTERVAL」。" >&2; exit 2 ;; +esac +[ "$INTERVAL" -gt 0 ] || { echo "錯誤: JSC_PR_WATCH_INTERVAL 要大於 0。" >&2; exit 2; } + +if [ -z "$STATE" ]; then + jsc_home="${JSC_HOME:-$HOME/.jsc}" + owner=${REPO%%/*}; name=${REPO#*/} + STATE="$jsc_home/pr-watch/$owner-$name-$INDEX.seen" +fi + +TMP_NEW=$(mktemp) || { echo "錯誤: 無法建立暫存檔。" >&2; exit 2; } +trap 'rm -f "$TMP_NEW"' EXIT +trap 'rm -f "$TMP_NEW"; exit 130' INT TERM + +SEEN="" +[ -f "$STATE" ] && SEEN=$(head -n1 "$STATE" 2>/dev/null) + +save_seen() { # 時間戳 -> 寫回狀態檔 + ts="$1" + [ -n "$ts" ] || return 0 + dir=$(dirname -- "$STATE") + mkdir -p "$dir" 2>/dev/null || { echo "警告: 建不出狀態檔目錄 $dir,這批留言下次會重複回報。" >&2; return 0; } + printf '%s\n' "$ts" > "$STATE" 2>/dev/null \ + || echo "警告: 寫不進狀態檔 $STATE,這批留言下次會重複回報。" >&2 + SEEN="$ts" +} + +collect_new() { # 新留言寫進 $TMP_NEW;有東西才回 0 + # 時間戳一律由同一台 Gitea 產生,格式與時區都一致,所以字串比大小就夠用; + # pr-comments 自己也是照這個字串排序的。 + "$GITEA" pr-comments "$REPO" "$INDEX" 2>/dev/null \ + | awk -F '\t' -v seen="$SEEN" 'NF >= 4 && ($1 "") > (seen "")' > "$TMP_NEW" + [ -s "$TMP_NEW" ] +} + +flush_new() { # 印出新留言並記住最後一筆的時間戳 + cat "$TMP_NEW" + save_seen "$(awk -F '\t' 'END { print $1 }' "$TMP_NEW")" +} + +first_round=1 +while :; do + if ! status=$("$GITEA" pr-status "$REPO" "$INDEX" 2>/dev/null); then + if [ "$first_round" -eq 1 ]; then + echo "錯誤: 連不上 Gitea 或 gitea.sh 執行失敗。請確認 GITEA_HOST 與 GITEA_TOKEN,再重新盯。" >&2 + exit 2 + fi + echo "警告: 這一輪查不到 PR 狀態,$INTERVAL 秒後再試。" >&2 + sleep "$INTERVAL" + continue + fi + first_round=0 + + state=$(printf '%s\n' "$status" | awk '{ print $1 }') + merged=$(printf '%s\n' "$status" | awk '{ print $2 }') + case "$state" in + open|closed) ;; + *) echo "錯誤: 查不到 PR $REPO#$INDEX。請確認存取庫與 PR 編號。" >&2; exit 3 ;; + esac + case "$merged" in + true|false) ;; + *) echo "錯誤: 查不到 PR $REPO#$INDEX。請確認存取庫與 PR 編號。" >&2; exit 3 ;; + esac + + if [ "$state" = closed ]; then + if [ "$merged" = true ]; then + printf '最終狀態: PR %s#%s 已合併。\n' "$REPO" "$INDEX" + else + printf '最終狀態: PR %s#%s 已關閉,沒有合併。\n' "$REPO" "$INDEX" + fi + if collect_new; then + echo "收尾前還有沒回報的留言:" + flush_new + fi + exit 0 + fi + + if collect_new; then + printf '新留言 %s 則(%s#%s),請接手跑決策樹:\n' "$(wc -l < "$TMP_NEW" | tr -d ' ')" "$REPO" "$INDEX" + flush_new + exit 10 + fi + + sleep "$INTERVAL" +done