From e1907e6aec8444e5a759b1436b2e3c7bf2317dc8 Mon Sep 17 00:00:00 2001 From: Hardik Date: Wed, 24 Jun 2026 15:48:48 +0530 Subject: [PATCH] fix(automation): scan all Claude PRs for review comments; drop author-based bot filter Two issues surfaced on first live run: - The per-run cap truncated the PR list to the lowest-numbered PR, so a comment on a higher-numbered PR was never scanned. The cap now limits only how many PRs Claude RUNS on; comment-less PRs are skipped for free, so newer PRs are never crowded out. - The bot posts as the repo owner's account, so excluding "the bot's own comments" by author also excluded the owner's legitimate review comments (and required a read:user token scope, which 403'd). Replaced with a guard that excludes only comments carrying the HANDLED_TAG marker -- robust even when the bot and the reviewer are the same account. The /user call is gone. Co-Authored-By: Claude Opus 4.8 --- automation/claude-pr-review-watcher.sh | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/automation/claude-pr-review-watcher.sh b/automation/claude-pr-review-watcher.sh index fb31622..cfe11cf 100644 --- a/automation/claude-pr-review-watcher.sh +++ b/automation/claude-pr-review-watcher.sh @@ -94,21 +94,23 @@ if [ ! -d "$WORKDIR/.git" ]; then git -C "$WORKDIR" config user.email "claude-autofix@pelagiamarine.com" fi -# --- identity + authorization set --- -# The bot's own login (so its acknowledgements are never treated as instructions). -BOT_LOGIN=$(api GET "/user" | jq -r '.login // ""') +# --- authorization set --- # Collaborators = users with write access. The repo owner is always allowed. +# (The bot may post as the owner's account, so we never filter by author to spot +# the bot's own comments -- its acknowledgements are excluded by the HANDLED_TAG +# marker instead, and human acks lack the claude-review: marker anyway.) COLLAB=$(api GET "/repos/$REPO/collaborators?limit=100" \ | jq -c --arg owner "$owner" '[.[].login] + [$owner] | unique') -log "Authorized commenters: $(printf '%s' "$COLLAB" | jq -r 'join(", ")') (bot=$BOT_LOGIN)" +log "Authorized commenters: $(printf '%s' "$COLLAB" | jq -r 'join(", ")')" # --- find Claude-raised open PRs (head branch under the prefix, or labelled claude-pr) --- prs=$(api GET "/repos/$REPO/pulls?state=open&limit=50" \ | jq -c --arg pfx "$PR_BRANCH_PREFIX" \ '[ .[] | select((.head.ref | startswith($pfx)) or (((.labels//[])|map(.name))|index("claude-pr"))) ] | sort_by(.number)') -prs=$(printf '%s' "$prs" | jq -c ".[:$MAX_PRS]") +# Scan ALL matching PRs (not truncated) -- the per-run cap below limits only how +# many PRs Claude actually RUNS on, so comment-less PRs never crowd out newer ones. n_prs=$(printf '%s' "$prs" | jq 'length') -log "Found $n_prs Claude-raised open PR(s) to scan for '$MARKER' comments" +log "Found $n_prs Claude-raised open PR(s) to scan for '$MARKER' comments (will run Claude on up to $MAX_PRS with new comments)" # Pull the instruction text that follows the marker out of a comment body. instr_of() { # BODY -> text after the first marker occurrence, trimmed @@ -117,6 +119,7 @@ instr_of() { # BODY -> text after the first marker occurrence, trimmed } p=0 +processed=0 while [ "$p" -lt "$n_prs" ]; do pr=$(printf '%s' "$prs" | jq -c ".[$p]") p=$((p+1)) @@ -133,15 +136,17 @@ while [ "$p" -lt "$n_prs" ]; do handled=$(printf '%s' "$conv" | jq -c --arg tag "$HANDLED_TAG" \ '[ .[].body // "" | select(contains($tag)) | scan("(?:conv|summary|inline):[0-9]+") ] | unique') + # A candidate must carry the marker, NOT be one of the bot's own ack comments + # (those carry HANDLED_TAG), and come from an authorized (collaborator) user. sel='select(.body != null) | select(.body | contains($m)) - | select(.user.login as $u | ($collab | index($u))) - | select(.user.login != $bot)' + | select(.body | contains($tag) | not) + | select(.user.login as $u | ($collab | index($u)))' - conv_tasks=$(printf '%s' "$conv" | jq -c --arg m "$MARKER" --argjson collab "$COLLAB" --arg bot "$BOT_LOGIN" " + conv_tasks=$(printf '%s' "$conv" | jq -c --arg m "$MARKER" --arg tag "$HANDLED_TAG" --argjson collab "$COLLAB" " [ .[] | $sel | { key:(\"conv:\"+(.id|tostring)), kind:\"conv\", id:.id, user:.user.login, loc:\"PR conversation\", body:.body } ]") - summary_tasks=$(printf '%s' "$reviews" | jq -c --arg m "$MARKER" --argjson collab "$COLLAB" --arg bot "$BOT_LOGIN" " + summary_tasks=$(printf '%s' "$reviews" | jq -c --arg m "$MARKER" --arg tag "$HANDLED_TAG" --argjson collab "$COLLAB" " [ .[] | select(.body != \"\") | $sel | { key:(\"summary:\"+(.id|tostring)), kind:\"summary\", id:.id, user:.user.login, loc:\"review summary\", body:.body } ]") @@ -151,7 +156,7 @@ while [ "$p" -lt "$n_prs" ]; do for rid in $(printf '%s' "$reviews" | jq -r '.[].id'); do rc=$(api_soft GET "/repos/$REPO/pulls/$num/reviews/$rid/comments") [ -z "$rc" ] && continue - t=$(printf '%s' "$rc" | jq -c --arg m "$MARKER" --argjson collab "$COLLAB" --arg bot "$BOT_LOGIN" " + t=$(printf '%s' "$rc" | jq -c --arg m "$MARKER" --arg tag "$HANDLED_TAG" --argjson collab "$COLLAB" " [ .[] | $sel | { key:(\"inline:\"+(.id|tostring)), kind:\"inline\", id:.id, user:.user.login, loc:(\"inline \"+(.path//\"?\")+\":\"+((.line // .original_line // 0)|tostring)), @@ -164,7 +169,12 @@ while [ "$p" -lt "$n_prs" ]; do fresh=$(printf '%s' "$fresh" | jq -c ".[:$MAX_COMMENTS]") n=$(printf '%s' "$fresh" | jq 'length') if [ "$n" -eq 0 ]; then log " no new '$MARKER' comments"; continue; fi - log " $n new '$MARKER' comment(s) to address" + if [ "$processed" -ge "$MAX_PRS" ]; then + log " $n new '$MARKER' comment(s) but per-run cap ($MAX_PRS) reached; deferring PR #$num to next run" + continue + fi + processed=$((processed+1)) + log " $n new '$MARKER' comment(s) to address (PR $processed/$MAX_PRS this run)" # ---- check out the PR branch in the work clone ---- git -C "$WORKDIR" fetch origin -q