-
Notifications
You must be signed in to change notification settings - Fork 1
review: never post to a merged or closed PR #213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "review": minor | ||
| --- | ||
|
|
||
| Never post a review to a merged or closed PR. | ||
|
|
||
| A review run takes ~12-17 minutes while PRs often merge minutes after their final push, so an in-flight run could complete after the merge and post a stale verdict (observed in production: CHANGES_REQUESTED with 11 blocking inline comments landing 16 minutes after the PR merged). Nothing in the pipeline checked PR state: gh-aw's safe-output handlers post regardless of it, and the compiled per-PR concurrency group only cancels a run when a newer run of the same workflow starts. | ||
|
|
||
| Two layers close the race: | ||
|
|
||
| - The workflow now subscribes to `pull_request: closed` (which also fires on merge) solely so the event enters the existing per-PR concurrency group and cancels an in-flight review within seconds. The job-level `if:` excludes `closed` runs from doing any work, so they skip every job and cost nothing. | ||
| - A guard step injected via `safe-outputs.steps` runs in the safe-outputs job immediately before the posting handlers. If the PR is not open at that moment, it records why in the job summary and cancels the run, so nothing posts; this also covers manual reruns of stale runs after a merge. The guard fails open on API errors and checks only PR state, never recency: a run whose PR is still open posts normally even if a newer push exists. | ||
|
|
||
| Consumers pick this up by updating the pin and recompiling (`gh aw update` and committing the regenerated `review.md` + `review.lock.yml`). Requires gh-aw >= v0.81.6 at compile time for `safe-outputs.steps`; no config.md changes and no new secrets (`KHAN_ACTIONS_BOT_TOKEN` is already required). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,13 +7,23 @@ description: > | |
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened, ready_for_review] | ||
| types: [opened, synchronize, reopened, ready_for_review, closed] | ||
| # Run automatically on every code push to a PR (`synchronize`) and when a PR | ||
| # leaves draft (`ready_for_review`), not via a slash command. Reviewer requests | ||
| # are gated on draft status in the prompt (Step 8). Do NOT post a "review | ||
| # started / completed" status comment — only the review itself, the | ||
| # risks/patterns comment (Step 7), and reviewer requests should appear on the | ||
| # PR. | ||
| # | ||
| # `closed` (which also fires on merge) is subscribed ONLY to cancel an in-flight | ||
| # review of the same PR: gh-aw compiles a workflow-level concurrency group keyed | ||
| # on the PR number with `cancel-in-progress: true`, and cancellation happens at | ||
| # run level as soon as the closed-event run enters the group, before any job | ||
| # condition is evaluated. The `if:` below excludes `closed` runs from doing any | ||
| # work themselves, so every job skips and the run costs nothing. Without this, a | ||
| # review still running when its PR merges (median run is ~12 minutes) posts a | ||
| # stale verdict on the merged PR. The safe-outputs guard step below is the | ||
| # deterministic backstop for the same race. | ||
| status-comment: false | ||
| # Disable gh-aw's pre-activation permission + confused-deputy gate so a same-repo | ||
| # collaborator pushing to a PR they didn't open still triggers the review (the | ||
|
|
@@ -32,7 +42,11 @@ on: | |
| # (zero AI credits) and posts nothing. The label is evaluated on each trigger event | ||
| # (open/synchronize/reopen/ready), so adding it prevents the *next* run — it does not | ||
| # retroactively dismiss a review already left on an earlier push. | ||
| # | ||
| # `closed` events are excluded entirely: those runs exist only to enter the per-PR | ||
| # concurrency group and cancel an in-flight review (see the trigger comment above). | ||
| if: >- | ||
| github.event.action != 'closed' && | ||
| !startsWith(github.event.pull_request.head.ref, 'deploy/') && | ||
| github.event.pull_request.head.ref != 'changeset-release/main' && | ||
| !contains(github.event.pull_request.labels.*.name, 'skip-ai-review') | ||
|
|
@@ -126,6 +140,62 @@ safe-outputs: | |
| # .github/aw/review/config.md (see the `imports:` note above), because its | ||
| # `allowed-team-reviewers` allowlist is repo-specific. Defining it here would override | ||
| # the import and drop the consumer's allowlist. | ||
| # | ||
| # Deterministic guard against the merge race: a run that started before its PR | ||
| # merged or closed would otherwise post its verdict after the fact (observed on | ||
| # a production PR: CHANGES_REQUESTED with 11 blocking comments landed 16 minutes | ||
| # after the merge). gh-aw injects these steps into the safe-outputs job after | ||
| # checkout/setup and before any safe-output handler executes; if the PR is no | ||
| # longer open at that moment, the step records why in the job summary and | ||
| # cancels the run, so nothing posts. The `closed` trigger above normally cancels | ||
| # such runs within seconds of the merge; this guard covers what that cannot: | ||
| # manual reruns of stale runs, and any cancellation miss. The guard fails OPEN | ||
| # on API errors (a transient GitHub error must never suppress a legitimate | ||
| # review of an open PR) and fails CLOSED once a non-open state is positively | ||
| # observed (if the cancel call is rejected, it fails the job so the handlers | ||
| # still never run). It checks only `state`, never recency: a run whose PR is | ||
| # still open posts normally even if a newer push exists, since that push's own | ||
| # run supersedes it. The bot token is used because this job's GITHUB_TOKEN | ||
| # lacks the `actions: write` scope that cancelling the run requires. | ||
| steps: | ||
| - name: Skip posting when the PR is no longer open | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| with: | ||
| github-token: ${{ secrets.KHAN_ACTIONS_BOT_TOKEN }} | ||
| script: | | ||
| const prNumber = context.payload.pull_request?.number; | ||
| if (!prNumber) return; | ||
| let pr; | ||
| try { | ||
| ({data: pr} = await github.rest.pulls.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: prNumber, | ||
| })); | ||
| } catch (error) { | ||
| core.warning( | ||
| `Could not check PR #${prNumber} state (${error.message}); posting normally.`); | ||
| return; | ||
| } | ||
| if (pr.state === 'open') return; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note (non-blocking): Layer 2 narrows but doesn't fully close the race. The guard reads PR state once ( Lower-confidence note
|
||
| const why = pr.merged ? `merged at ${pr.merged_at}` : 'closed'; | ||
| await core.summary | ||
| .addRaw(`Review not posted: PR #${prNumber} was ${why} before this run ` + | ||
| 'finished, so its verdict could no longer affect the merge decision.') | ||
| .write(); | ||
| try { | ||
| await github.rest.actions.cancelWorkflowRun({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| run_id: context.runId, | ||
| }); | ||
| // Wait for the runner to receive the cancellation and kill this step. | ||
| await new Promise((resolve) => setTimeout(resolve, 120000)); | ||
| } catch (error) { | ||
| core.warning(`Could not cancel the run (${error.message}).`); | ||
| } | ||
| core.setFailed( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note (non-blocking): In the cancel-rejected fallback this |
||
| `PR #${prNumber} is ${pr.state}; refusing to post a review after the fact.`); | ||
|
Comment on lines
+186
to
+198
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear to me why this is needed - it seems like we could just intentionally fail the step, which would have the same effect as cancelling the rest of the workflow, I believe. |
||
|
|
||
| network: | ||
| allowed: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): No test asserts this guard survives a future
gh aw updateor hand-edit. The repo already backstopsreview.mdfrontmatter with text-level regex tests (version-sync.test.ts,review-pins.test.ts); a cheap assertion that theclosedtrigger, theif:exclusion, the guard step, and thepr.statecheck are all present would catch the highest-likelihood regression — silently losing the guard and reopening the merge race this PR fixes.