Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/scripts/pr-auto-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ Pure, side-effect-free helpers. Source the file, then call:
| `pr_auto_review_blocking_thread_count` | review-threads JSON on stdin (`reviewThreads(first:100){nodes{isResolved isOutdated}}`) | prints the count of **blocking** threads — unresolved AND not outdated |
| `pr_auto_review_ready STATE IS_DRAFT CHECKS_JSON REQUIRED_JSON SELF_NAME REVIEW_DECISION BLOCKING_THREAD_COUNT` | the PR facts the workflow gathers (all as arguments — no stdin) | prints the **decision class** on stdout; `0` ready, `1` not ready |

## `lib/sweep.sh`

Pure candidate-selection logic for the catch-up sweep (issue #868).

| Function | Input | Returns |
|----------|-------|---------|
| `pr_auto_review_sweep_candidates MAX` | PR-list JSON on stdin (`gh search prs --json url,isDraft`) | prints ≤`MAX` non-draft PR URLs, one per line, in input order |

## `sweep-dispatch.sh`

The catch-up sweep orchestrator (issue #868). See "The catch-up sweep" below.

### The unified decision core — `pr_auto_review_ready`

`pr_auto_review_ready` is the single pure core the reusable workflow calls. It
Expand Down Expand Up @@ -98,3 +110,44 @@ context `SonarCloud Code Analysis`), while `gh pr checks` renders Actions checks
as `"<workflow> / <job>"` (e.g. `CI / Lint`). A check matches a required context
when the names are equal, or when either ends with `" / <the other>"`, so both
forms resolve to the same required check.

## The catch-up sweep (issue #868)

The event-driven ready-check has **no catch-up**. Under a bulk `standards-sync`
convergence (Epic #850 / #857) it fires while CI is still mid-flight
(`"N of M checks not yet passing — skipping"`) and, once everything goes green,
**no further event re-evaluates it** — the PR strands `BLOCKED` with all required
checks green and no code-owner approval, indefinitely. Manually re-running the
ready-check often lands during transient CI re-runs and skips again, so a clean
"all-green + fresh event" window is unreliable while bots are active.

`sweep-dispatch.sh` is the missing catch-up, run by the
`PR Auto-Review — Catch-up Sweep` workflow on a schedule (every 15 min) and on
`workflow_dispatch`:

1. Enumerate the open, non-draft PRs carrying the sweep label
(`standards-sync`) org-wide with a single `gh search prs`.
2. Select a **bounded** set via `pr_auto_review_sweep_candidates MAX_PER_RUN`
(back-pressure — see below).
3. For each candidate, gather the same PR facts the event path gathers and
delegate the decision to `pr_auto_review_ready`. Dispatch the review agent
for the ones that come back `dispatched`.

Because the decision is delegated verbatim, the sweep inherits the #680
required-vs-non-required tolerance for free: a cancelled/superseded **non-required**
context (a `dev-lead / ci-relay` / `dev-lead / dispatch` run cancelled by per-PR
concurrency) never keeps a ready PR from dispatching. Periodic re-evaluation is
also the **debounce**: a PR skipped during a transient *required* re-run is
re-swept next cycle, so no exact settle window has to be caught.

### Back-pressure (donpetry-bot capacity)

donpetry-bot approvals drain at a limited rate (agent/token capacity), so a
10-PR burst that fires every dispatch at once just queues them all behind the
same cap. `MAX_PER_RUN` (default 8) bounds the dispatches per run so a burst
drains over a few cycles instead. `pr_auto_review_sweep_candidates` enforces the
bound and is fail-safe: a non-positive or non-numeric `MAX` selects **nothing**,
so a misconfigured cap can never turn into an unbounded dispatch burst.

Manual `workflow_dispatch` runs default to **dry-run** (log intended dispatches,
fire nothing); scheduled runs are live.
48 changes: 48 additions & 0 deletions .github/scripts/pr-auto-review/lib/sweep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Candidate-selection logic for the pr-auto-review catch-up sweep (issue #868).
#
# Pure, side-effect-free — unit-tested with bats
# (test/workflows/pr-auto-review/sweep.bats). The sweep workflow gathers the
# open standards-sync PR list via `gh search prs` and hands it here to pick the
# bounded set of PRs to (re-)evaluate this cycle. The per-PR readiness decision
# is NOT re-implemented: the orchestrator (sweep-dispatch.sh) feeds each URL
# through pr_auto_review_ready, so the #680 required-vs-non-required tolerance
# is inherited verbatim.
#
# Contract: see .github/scripts/pr-auto-review/README.md
# Pins issue #868.

# pr_auto_review_sweep_candidates MAX
# Reads a PR-list JSON array on stdin — the response of
# `gh search prs --json url,isDraft` (each element has .url and .isDraft) —
# and prints, one per line, up to MAX candidate PR URLs to evaluate this
# sweep cycle. Draft PRs (`.isDraft == true`) are dropped; the rest are
# emitted in input order, capped at MAX for per-run back-pressure.
#
# MAX the maximum number of PRs to process this run (back-pressure). A
# non-positive or non-numeric MAX emits nothing — the sweep does no
# work rather than fire an unbounded burst of dispatches.
#
# A missing `.isDraft` is treated as non-draft (fail-open on the field, since
# `gh search prs --json isDraft` always populates it; a bare `{url}` from a
# hand-built payload should still be swept). Non-array input (e.g. a
# `{"message": "Not Found"}` error body) or empty stdin emits nothing.
# Always returns 0; the caller decides what an empty candidate set means.
pr_auto_review_sweep_candidates() {
local max="${1:-0}"

# Back-pressure guard: only a positive integer bounds the run. Anything else
# (0, negative, non-numeric) selects nothing so a misconfigured cap can never
# fire an unbounded dispatch burst.
if ! [[ "$max" =~ ^[0-9]+$ ]] || [ "$max" -le 0 ]; then
return 0
fi

jq -r --argjson max "$max" '
if type == "array" then
[ .[] | select(.isDraft != true) | .url | select(type == "string") ][:$max][]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The candidate cap is applied before readiness is checked, so older PRs that stay non-ready can repeatedly consume all slots and prevent newer ready PRs from ever being evaluated. Move the cap to the dispatch stage (cap successful dispatches), or evaluate a larger window and stop once MAX dispatches are reached. [incorrect condition logic]

Severity Level: Major ⚠️
- ❌ Ready standards-sync PRs can be starved behind non-ready ones.
- ⚠️ Operators must manually review stranded but ready PRs.
- ⚠️ Back-pressure fairness guarantee is broken under stuck PRs.
Steps of Reproduction ✅
1. In `.github/scripts/pr-auto-review/sweep-dispatch.sh` lines 55-62, observe that
`PR_LIST` is built using `gh search prs` sorted by `created` ascending and limited to 100
results, representing the oldest open labeled PRs.

2. In `.github/scripts/pr-auto-review/lib/sweep.sh` lines 31-43 and README lines 25-32,
confirm that `pr_auto_review_sweep_candidates MAX` (called from sweep-dispatch line 68)
filters out drafts and then caps the resulting URL list to `MAX_PER_RUN` using the array
slice `[ ... ][:$max][]` before any readiness evaluation occurs.

3. In a real run where more than `MAX_PER_RUN` non-draft `standards-sync` PRs are open,
with some of the oldest PRs permanently failing readiness (e.g., `pr_auto_review_ready`
returning `skip-changes-requested` or `skip-unresolved-threads` as described in README
lines 44-50), the candidates array on each sweep consists only of those oldest PRs.

4. Because the loop at `sweep-dispatch.sh` lines 120-141 iterates only over `CANDIDATES`,
newer ready PRs that sit beyond the first `MAX_PER_RUN` non-draft items are never passed
to `evaluate_pr` and therefore never dispatched, while the same non-ready oldest PRs
consume the candidate slots on every sweep run.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** .github/scripts/pr-auto-review/lib/sweep.sh
**Line:** 43:43
**Comment:**
	*Incorrect Condition Logic: The candidate cap is applied before readiness is checked, so older PRs that stay non-ready can repeatedly consume all slots and prevent newer ready PRs from ever being evaluated. Move the cap to the dispatch stage (cap successful dispatches), or evaluate a larger window and stop once MAX dispatches are reached.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in .github/scripts/pr-auto-review/lib/sweep.sh and sweep-dispatch.sh: removed the [:] slice from pr_auto_review_sweep_candidates (the function now emits all non-draft PRs in input order). The back-pressure cap moves to the dispatch loop in sweep-dispatch.sh with [ "$dispatched" -ge "$MAX_PER_RUN" ] && break, so non-ready older PRs can no longer consume all candidate slots and starve newer ready ones. Tests in test/workflows/pr-auto-review/sweep.bats updated to reflect the new contract.

else
empty
end
'
}
143 changes: 143 additions & 0 deletions .github/scripts/pr-auto-review/sweep-dispatch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# sweep-dispatch.sh — catch-up sweep for the pr-auto-review ready-check (#868).
#
# Why this exists (Epic #850 / #857 fleet convergence): the event-driven
# ready-check (pr-auto-review-reusable.yml) has no catch-up. When a bulk
# standards-sync convergence opens many PRs at once, the ready-check fires while
# CI is still mid-flight ("N of M checks not yet passing — skipping") and, once
# everything goes green, no further event re-evaluates it — the PR sits BLOCKED
# with all required checks green and no code-owner approval, indefinitely.
#
# This scheduled/manual sweep enumerates the open standards-sync PRs org-wide
# and, for each one that satisfies the SAME readiness gate as the event path
# (delegated verbatim to pr_auto_review_ready — so the #680 cancelled/superseded
# -non-required tolerance is inherited), re-invokes the dispatch → review-agent
# path. It is the missing catch-up for the missed-event case and removes the
# need for manual `gh run rerun` nudges. Periodic re-evaluation is also the
# debounce: a PR skipped during a transient required re-run is re-swept next
# cycle, so no clean "fresh event + all green" window has to be caught.
#
# Back-pressure (donpetry-bot token/capacity, acceptance criterion): at most
# MAX_PER_RUN PRs are dispatched per run so a 10-PR burst drains at a throttled
# rate over a few cycles instead of firing every dispatch at once.
#
# Idempotent: dispatching an already-approved / already-merged PR is a no-op on
# the review-agent side. Honours DRY_RUN=1 (logs intended dispatches, mutates
# nothing).
#
# Env:
# GH_TOKEN classic PAT with repo scope — API reads + dispatch (required)
# SEARCH_OWNER org to scan for open PRs (default: petry-projects)
# SWEEP_LABEL PR label to sweep (default: standards-sync)
# MAX_PER_RUN max PRs to dispatch per run (default: 8)
# DISPATCH_REPO repository_dispatch target repo (default: petry-projects/.github-private)
# DRY_RUN "1" → log intended dispatches only
set -euo pipefail

_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=.github/scripts/pr-auto-review/lib/ready-check.sh
. "${_dir}/lib/ready-check.sh"
# shellcheck source=.github/scripts/pr-auto-review/lib/sweep.sh
. "${_dir}/lib/sweep.sh"

SEARCH_OWNER="${SEARCH_OWNER:-petry-projects}"
SWEEP_LABEL="${SWEEP_LABEL:-standards-sync}"
MAX_PER_RUN="${MAX_PER_RUN:-8}"
DISPATCH_REPO="${DISPATCH_REPO:-petry-projects/.github-private}"
DRY_RUN="${DRY_RUN:-0}"

# ── Enumerate open, non-draft PRs carrying the sweep label, org-wide ──────────
# `gh search prs` spans every repo the token can see in one call, so the sweep
# runs centrally without an App token or a per-repo installation walk.
# Oldest-first (created asc) so back-pressure drains fairly: each capped run
# takes the oldest waiting PRs, and a merged PR leaves the set so the next-oldest
# advances next cycle — no PR is starved by newer arrivals under best-match order.
PR_LIST=$(gh search prs \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Silent search failure no-ops 🐞 Bug ☼ Reliability

The sweep suppresses gh search prs errors and ignores its exit status, then treats empty output as
an empty PR set, so auth/rate-limit/network failures can make the workflow succeed while dispatching
nothing (leaving PRs stranded). This undermines the sweep’s purpose because failures become
indistinguishable from “no open PRs.”
Agent Prompt
### Issue description
`sweep-dispatch.sh` currently runs `gh search prs` with `2>/dev/null || true` and then coerces empty output to `[]`. If the search fails (bad token, rate limit, transient network), the workflow can exit 0 with “nothing to do”, which looks successful but leaves stuck PRs untouched.

### Issue Context
This script is the new scheduled catch-up mechanism; if it silently no-ops on failures, the org loses the only automatic recovery path for missed readiness events.

### Fix (suggested)
- Capture and check the `gh search prs` exit code.
- Do **not** suppress stderr; or, if you must, emit an explicit `::error::` / `::warning::` with the failure reason.
- Prefer failing the job (non-zero exit) so scheduled runs are visibly unhealthy, rather than pretending the PR set is empty.

Example sketch:
```bash
if ! PR_LIST=$(gh search prs ... --json url,isDraft); then
  echo "::error::gh search prs failed; sweep aborted"
  exit 1
fi
```

### Fix Focus Areas
- .github/scripts/pr-auto-review/sweep-dispatch.sh[55-66]
- .github/scripts/pr-auto-review/sweep-dispatch.sh[74-77]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in sweep-dispatch.sh (lines 59-66): replaced 2>/dev/null || true with if ! PR_LIST=$(gh search prs ...); then echo "::error::gh search prs failed..."; exit 1; fi. Stderr is now visible in Actions logs and auth/rate-limit/network failures cause a non-zero exit, making the sweep visibly unhealthy instead of silently treating an empty result as "no PRs to process".

--owner "$SEARCH_OWNER" \
--label "$SWEEP_LABEL" \
--state open \
--sort created \
--order asc \
--limit 100 \
--json url,isDraft 2>/dev/null || true)
Comment on lines +55 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The search is hard-limited to 100 PRs with no pagination, so labeled open PRs beyond the first page are never considered by the sweep and can remain stranded indefinitely. Add pagination (or raise and iterate limits) so the sweep can eventually inspect the full open set. [incomplete implementation]

Severity Level: Major ⚠️
- ❌ Open labeled PRs beyond first 100 never auto-reviewed.
- ⚠️ Bulk convergence can strand newer ready PRs indefinitely.
- ⚠️ Sweep’s “org-wide” guarantee is violated under large bursts.
Steps of Reproduction ✅
1. In `.github/scripts/pr-auto-review/sweep-dispatch.sh` lines 55-62, `PR_LIST` is
populated via `gh search prs` with `--limit 100`, `--sort created`, and `--order asc`, so
only the first 100 oldest matching open, labeled PRs are ever returned.

2. README section “The catch-up sweep” (lines 114-131) describes the sweep as enumerating
open, non-draft PRs carrying the sweep label org-wide, but does not add any pagination or
follow-up calls beyond this single `gh search prs` invocation.

3. When more than 100 open `standards-sync` PRs exist simultaneously in the organization,
`gh search prs` only returns the oldest 100; newer PRs beyond that window never appear in
`PR_LIST` and therefore are not present in the JSON array passed into
`pr_auto_review_sweep_candidates MAX_PER_RUN` on line 68.

4. As a result, the evaluation loop at lines 120-141 only ever calls `evaluate_pr` for PR
URLs within the first 100 results, and PRs beyond that initial page are never inspected
for readiness or dispatched by the sweep, even if they satisfy the ready-check criteria.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** .github/scripts/pr-auto-review/sweep-dispatch.sh
**Line:** 55:62
**Comment:**
	*Incomplete Implementation: The search is hard-limited to 100 PRs with no pagination, so labeled open PRs beyond the first page are never considered by the sweep and can remain stranded indefinitely. Add pagination (or raise and iterate limits) so the sweep can eventually inspect the full open set.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in sweep-dispatch.sh: added a SEARCH_LIMIT env var (default 1000, the GitHub Search API per-query maximum) and changed --limit 100 to --limit "$SEARCH_LIMIT". This removes the hard 100-PR ceiling; operators can lower SEARCH_LIMIT in the workflow env for testing. True pagination beyond 1000 would require multiple search calls and can be added if the org ever exceeds that threshold.

if [ -z "${PR_LIST}" ]; then
PR_LIST="[]"
fi

# Selection + back-pressure (pure, unit-tested): drops drafts, caps at MAX_PER_RUN.
mapfile -t CANDIDATES < <(printf '%s' "$PR_LIST" | pr_auto_review_sweep_candidates "$MAX_PER_RUN")

total_open=$(printf '%s' "$PR_LIST" | jq 'if type == "array" then length else 0 end')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Jq parse can abort sweep 🐞 Bug ☼ Reliability

total_open is computed by piping PR_LIST into jq without guarding parse errors, so unexpected
non-JSON/truncated stdout can terminate the sweep under set -e. This can prevent any candidates
from being evaluated/processed in that run.
Agent Prompt
### Issue description
`sweep-dispatch.sh` runs `jq` over `$PR_LIST` to compute `total_open` without `2>/dev/null` and without a fallback. With `set -euo pipefail`, a parse error will exit the script.

### Issue Context
While `gh search prs --json ...` normally emits JSON, “unexpected stdout” does happen in practice (partial output, CLI bugs, wrapper output, etc.). The sweep should be robust because it’s a periodic recovery mechanism.

### Fix (suggested)
- Add a defensive parse wrapper:
  - If parsing fails, log an error/warning and either:
    - fail the workflow (preferred for scheduled correctness), or
    - default `total_open=0` and `PR_LIST='[]'` and continue.

Example sketch:
```bash
if ! total_open=$(printf '%s' "$PR_LIST" | jq 'if type=="array" then length else 0 end' 2>/dev/null); then
  echo "::error::PR_LIST was not valid JSON; aborting sweep"
  exit 1
fi
```

### Fix Focus Areas
- .github/scripts/pr-auto-review/sweep-dispatch.sh[67-73]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in sweep-dispatch.sh (line 75): wrapped the jq call with if ! total_open=$(... jq ... 2>/dev/null); then echo "::error::PR_LIST was not valid JSON..."; exit 1; fi. A non-JSON or truncated PR_LIST now aborts the sweep with a visible diagnostic instead of terminating silently under set -e.

[ "$DRY_RUN" = "1" ] && dry_note=" (DRY_RUN)" || dry_note=""
echo "Sweep: ${total_open} open '${SWEEP_LABEL}' PR(s) in ${SEARCH_OWNER}; evaluating ${#CANDIDATES[@]} this run (MAX_PER_RUN=${MAX_PER_RUN})${dry_note}."

if [ "${#CANDIDATES[@]}" -eq 0 ]; then
echo "No candidate PRs to evaluate — nothing to do."
exit 0
fi

# evaluate_pr PR_URL
# Gathers the same PR facts the event-path glue gathers and returns the
# pr_auto_review_ready decision class on stdout; exit 0 iff ready to dispatch.
# Mirrors the "Check PR readiness criteria" step of pr-auto-review-reusable.yml
# (there is no self-check to exclude in a sweep — the sweep is not a PR check).
evaluate_pr() {
local pr_url="$1" repo pr_meta state is_draft pr_number review_decision base_branch
local checks required_json rules_json threads_json blocking_thread_count
repo=$(printf '%s' "$pr_url" | sed 's|https://github.com/||; s|/pull/.*||')

pr_meta=$(gh pr view "$pr_url" --json state,isDraft,number,reviewDecision,baseRefName)
state=$(printf '%s' "$pr_meta" | jq -r '.state')
is_draft=$(printf '%s' "$pr_meta" | jq -r '.isDraft')
pr_number=$(printf '%s' "$pr_meta" | jq -r '.number')
review_decision=$(printf '%s' "$pr_meta" | jq -r '.reviewDecision // ""')
base_branch=$(printf '%s' "$pr_meta" | jq -r '.baseRefName')

checks=$(gh pr checks "$pr_url" --json bucket,name 2>/dev/null || true)
if [ -z "${checks}" ]; then checks="[]"; fi

if rules_json=$(gh api "/repos/${repo}/rules/branches/${base_branch}" 2>/dev/null); then
required_json=$(printf '%s' "$rules_json" | pr_auto_review_required_contexts 2>/dev/null || echo "[]")
else
required_json="[]"
fi
if [ -z "${required_json}" ]; then required_json="[]"; fi

# shellcheck disable=SC2016 # $owner/$repo/$number are GraphQL variable refs, not shell vars
local gql='query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewThreads(first:100){nodes{isResolved isOutdated}}}}}'
threads_json=$(gh api graphql \
-f "query=$gql" \
-f owner="${repo%%/*}" \
-f repo="${repo##*/}" \
-F number="${pr_number}")
blocking_thread_count=$(printf '%s' "$threads_json" | pr_auto_review_blocking_thread_count)

pr_auto_review_ready \
"$state" "$is_draft" "$checks" "$required_json" \
"" "$review_decision" "$blocking_thread_count"
}

dispatched=0
for pr_url in "${CANDIDATES[@]}"; do
[ -z "$pr_url" ] && continue
echo "::group::${pr_url}"
if decision=$(evaluate_pr "$pr_url"); then
if [ "$DRY_RUN" = "1" ]; then
echo "[dry-run] would dispatch review agent for ${pr_url} (decision=${decision})"
else
gh api \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Dispatch failure stops batch 🐞 Bug ☼ Reliability

A single failing gh api ... /dispatches call will terminate the entire sweep run due to set -e,
skipping remaining ready PRs in the capped batch. This reduces sweep throughput and can keep PRs
stuck until a later cycle even when they were ready now.
Agent Prompt
### Issue description
The live dispatch path executes `gh api .../dispatches` unguarded inside the per-PR loop. Under `set -e`, any transient dispatch failure aborts the whole run and skips later candidates.

### Issue Context
This script intentionally limits dispatches per run (back-pressure). Aborting mid-loop makes the effective cap smaller than configured and slows convergence.

### Fix (suggested)
- Wrap dispatch in an `if` and continue on failure:
  - log `::error::` with PR URL
  - increment a `dispatch_failures` counter
- After the loop, optionally `exit 1` if any dispatches failed (so the run is visible as unhealthy) while still attempting the rest.

Example sketch:
```bash
dispatch_failures=0
...
if ! gh api ...; then
  echo "::error::Dispatch failed for $pr_url"
  dispatch_failures=$((dispatch_failures+1))
  continue
fi
```

### Fix Focus Areas
- .github/scripts/pr-auto-review/sweep-dispatch.sh[120-141]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in sweep-dispatch.sh (dispatch loop): wrapped gh api .../dispatches in an if block — a transient failure logs ::error:: and increments dispatch_failures instead of aborting under set -e. The loop continues processing remaining candidates. After the loop the sweep exits 1 if any dispatch failed, so the scheduled run is visibly unhealthy while still attempting every ready PR in the batch.

--method POST \
--header "Accept: application/vnd.github+json" \
"/repos/${DISPATCH_REPO}/dispatches" \
--field event_type=pr-review-mention \
--field "client_payload[pr_url]=${pr_url}"
echo "::notice::Sweep dispatched auto-review for ${pr_url}"
fi
dispatched=$((dispatched + 1))
else
echo "Not ready (decision=${decision}) — skipping ${pr_url}"
fi
echo "::endgroup::"
done

echo "Sweep complete — dispatched ${dispatched} of ${#CANDIDATES[@]} evaluated PR(s)."
70 changes: 70 additions & 0 deletions .github/workflows/pr-auto-review-sweep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# PR Auto-Review — Catch-up Sweep (issue #868, surfaced by Epic #850 / #857).
#
# The event-driven ready-check (pr-auto-review-reusable.yml) has no catch-up:
# under a bulk standards-sync convergence it fires while CI is still mid-flight
# and, once everything goes green, no further event re-evaluates the PR — it
# strands BLOCKED with all required checks green and no code-owner approval.
#
# This scheduled/manual sweep enumerates the open `standards-sync` PRs org-wide
# and re-invokes the SAME readiness gate → dispatch path for the ready ones
# (delegated to pr_auto_review_ready, so the #680 cancelled/superseded-non-
# required tolerance is inherited). It is the missing catch-up for the missed-
# event case and removes the need for manual `gh run rerun` nudges. A bounded
# MAX_PER_RUN provides back-pressure so a burst drains over a few cycles rather
# than firing every dispatch at once (donpetry-bot token/capacity).
#
# Runs centrally in petry-projects/.github (the scripts live here); a single
# `gh search prs` call spans every repo the PAT can see.
#
# Requires: GH_PAT_WORKFLOWS org secret (classic PAT, repo scope) for API reads
# and the repository_dispatch to petry-projects/.github-private.
name: PR Auto-Review — Catch-up Sweep

on:
workflow_dispatch:
inputs:
label:
description: "PR label to sweep"
type: string
default: "standards-sync"
max_per_run:
description: "Max PRs to dispatch this run (back-pressure)"
type: string
default: "8"
dry_run:
description: "Log intended dispatches without firing them"
type: boolean
default: true
schedule:
# Every 15 min — the catch-up interval for a missed-event stall. A PR that
# goes all-required-green with no fresh event is picked up within one cycle.
- cron: "*/15 * * * *"

permissions: {}

concurrency:
group: pr-auto-review-sweep
cancel-in-progress: false

jobs:
sweep:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout sweep tooling
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
persist-credentials: false

- name: Sweep open standards-sync PRs and dispatch the ready ones
env:
GH_TOKEN: ${{ secrets.GH_PAT_DON_PETRY || secrets.GH_PAT_WORKFLOWS }}
SEARCH_OWNER: ${{ github.repository_owner }}
SWEEP_LABEL: ${{ inputs.label || 'standards-sync' }}
MAX_PER_RUN: ${{ inputs.max_per_run || '8' }}
# Manual runs default to dry-run; scheduled runs are live.
DRY_RUN: ${{ (github.event_name == 'workflow_dispatch' && inputs.dry_run) && '1' || '0' }}
run: bash .github/scripts/pr-auto-review/sweep-dispatch.sh
4 changes: 3 additions & 1 deletion .github/workflows/pr-auto-review-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ jobs:
run: |
set -euo pipefail
shellcheck -x \
.github/scripts/pr-auto-review/lib/ready-check.sh
.github/scripts/pr-auto-review/lib/ready-check.sh \
.github/scripts/pr-auto-review/lib/sweep.sh \
.github/scripts/pr-auto-review/sweep-dispatch.sh

- name: Run bats suite
run: bats --print-output-on-failure test/workflows/pr-auto-review/
Loading
Loading