#!/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