#!/usr/bin/env bash # review-poll.sh — Poll open PRs and review those with green CI # # Peek while running: cat /tmp/-review-status # Full log: tail -f /review/review.log set -euo pipefail # Load shared environment (with optional project TOML override) # Usage: review-poll.sh [projects/harb.toml] export PROJECT_TOML="${1:-}" source "$(dirname "$0")/../lib/env.sh" source "$(dirname "$0")/../lib/ci-helpers.sh" # shellcheck source=../lib/guard.sh source "$(dirname "$0")/../lib/guard.sh" check_active reviewer REPO_ROOT="${PROJECT_REPO_ROOT}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" API_BASE="${FORGE_API}" LOGFILE="$SCRIPT_DIR/review.log" MAX_REVIEWS=3 REVIEW_IDLE_TIMEOUT=14400 # 4h: kill review session if idle log() { printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE" } # Log rotation if [ -f "$LOGFILE" ]; then LOGSIZE=$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0) if [ "$LOGSIZE" -gt 102400 ]; then mv "$LOGFILE" "$LOGFILE.old" log "Log rotated" fi fi log "--- Poll start ---" # --- Clean up stale review sessions --- # Kill sessions for merged/closed PRs or idle > 4h REVIEW_SESSIONS=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^review-${PROJECT_NAME}-" || true) if [ -n "$REVIEW_SESSIONS" ]; then while IFS= read -r session; do pr_num="${session#review-"${PROJECT_NAME}"-}" phase_file="/tmp/review-session-${PROJECT_NAME}-${pr_num}.phase" # Check if PR is still open pr_state=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ "${API_BASE}/pulls/${pr_num}" | jq -r '.state // "unknown"' 2>/dev/null) || true if [ "$pr_state" != "open" ]; then log "cleanup: killing session ${session} (PR #${pr_num} state=${pr_state})" tmux kill-session -t "$session" 2>/dev/null || true rm -f "$phase_file" "/tmp/${PROJECT_NAME}-review-output-${pr_num}.json" \ "/tmp/review-injected-${PROJECT_NAME}-${pr_num}" # Prune thread-map entries for this PR sed -i "/\treview\t[^\t]*\t${pr_num}\t/d" "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true cd "$REPO_ROOT" git worktree remove "/tmp/${PROJECT_NAME}-review-${pr_num}" --force 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${pr_num}" 2>/dev/null || true continue fi # Check idle timeout (4h) phase_mtime=$(stat -c %Y "$phase_file" 2>/dev/null || echo 0) now=$(date +%s) if [ "$phase_mtime" -gt 0 ] && [ $(( now - phase_mtime )) -gt "$REVIEW_IDLE_TIMEOUT" ]; then log "cleanup: killing session ${session} (idle > 4h)" tmux kill-session -t "$session" 2>/dev/null || true rm -f "$phase_file" "/tmp/${PROJECT_NAME}-review-output-${pr_num}.json" \ "/tmp/review-injected-${PROJECT_NAME}-${pr_num}" # Prune thread-map entries for this PR sed -i "/\treview\t[^\t]*\t${pr_num}\t/d" "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true cd "$REPO_ROOT" git worktree remove "/tmp/${PROJECT_NAME}-review-${pr_num}" --force 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${pr_num}" 2>/dev/null || true continue fi # Safety net: clean up sessions in terminal phases (review already posted) current_phase=$(head -1 "$phase_file" 2>/dev/null | tr -d '[:space:]' || true) if [ "$current_phase" = "PHASE:review_complete" ]; then log "cleanup: killing session ${session} (terminal phase: review_complete)" tmux kill-session -t "$session" 2>/dev/null || true rm -f "$phase_file" "/tmp/${PROJECT_NAME}-review-output-${pr_num}.json" \ "/tmp/review-injected-${PROJECT_NAME}-${pr_num}" sed -i "/\treview\t[^\t]*\t${pr_num}\t/d" "${MATRIX_THREAD_MAP:-/tmp/matrix-thread-map}" 2>/dev/null || true cd "$REPO_ROOT" git worktree remove "/tmp/${PROJECT_NAME}-review-${pr_num}" --force 2>/dev/null || true rm -rf "/tmp/${PROJECT_NAME}-review-${pr_num}" 2>/dev/null || true continue fi done <<< "$REVIEW_SESSIONS" fi PRS=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ "${API_BASE}/pulls?state=open&limit=20" | \ jq -r --arg branch "${PRIMARY_BRANCH}" '.[] | select(.base.ref == $branch) | select(.draft != true) | select(.title | test("^\\[?WIP[\\]:]"; "i") | not) | "\(.number) \(.head.sha) \(.head.ref)"') if [ -z "$PRS" ]; then log "No open PRs targeting ${PRIMARY_BRANCH}" exit 0 fi TOTAL=$(echo "$PRS" | wc -l) log "Found ${TOTAL} open PRs" REVIEWED=0 SKIPPED=0 inject_review_into_dev_session() { local pr_num="$1" pr_sha="$2" pr_branch="$3" local issue_num issue_num=$(printf '%s' "$pr_branch" | grep -oP 'issue-\K[0-9]+' || true) [ -z "$issue_num" ] && return 0 local session="dev-${PROJECT_NAME}-${issue_num}" local phase_file="/tmp/dev-session-${PROJECT_NAME}-${issue_num}.phase" tmux has-session -t "${session}" 2>/dev/null || return 0 local current_phase current_phase=$(head -1 "${phase_file}" 2>/dev/null | tr -d '[:space:]' || true) [ "${current_phase}" = "PHASE:awaiting_review" ] || return 0 local review_comment review_comment=$(forge_api_all "/issues/${pr_num}/comments" | \ jq -r --arg sha "${pr_sha}" \ '[.[] | select(.body | contains("