Files
gitea/tools/pr-watch.sh
T
jiantw83 5471b97635 feat(pr-watch): 新增盯 PR 到合併的輪詢腳本
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 的技能。
2026-08-27 11:20:27 +08:00

129 lines
5.1 KiB
Bash
Executable File
Raw 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
# pr-watch.sh — 盯著一支 PR,直到它合併或關閉。
# 用法: pr-watch.sh <owner>/<repo> <pr-index> [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 的原格式
# 「{時間}<TAB>{作者}<TAB>{類型}<TAB>{內容}」,呼叫端可直接用 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 <owner>/<repo> <pr-index> [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