fix(review-gate): re-evaluate the comment gate when a formal review is submitted (BLO-29853) - #1478
Conversation
…s submitted (BLO-29853) `resolvePrCommentReviewGateWebhookTrigger` is the gate's sole entry point, and it accepted only `issue_comment.created` and `pull_request` opened/reopened/synchronize. `pull_request_review` fell out of the `eventName !== "pull_request"` guard with no branch, so the gate was evaluated on the issue-comment surface and at push time -- and push time is the one instant at which a review of that head cannot yet exist. #1464 fixed *where* the gate reads (both surfaces). It did not fix *when*. The reviews-API half was therefore structurally unreachable in the common path, which matters because 33 of 33 consolidated reviews measured in this repo arrive as reviews-API objects and none as issue comments. Measured consequence on master: the last 30 merged PRs all carried the context green, and all 30 of those greens were "no comment attests this head" -- zero real verdicts, ever, while 9 of the 10 most recent sampled had a formal Ally review at the exact merge head the gate never saw. Add the missing branch, gated on reviewer identity and the consolidated-review heading to match the `issue_comment` path. `edited` triggers alongside `submitted` because the verdict is a pure function of the review bodies; `dismissed` does not, because dismissal leaves the body and therefore the verdict unchanged. The trigger deliberately omits `headSha` so the gate resolves the live head rather than trusting `review.commit_id` or the payload snapshot: a review can land against a head the branch has moved past, a status written to a non-head commit is invisible to branch protection, and resolving live is what lets the carried-finding path (BLO-29711) see the new head as unattested. Tests: the `pull_request_review` case was absent from the resolver's matrix entirely -- neither asserted to trigger nor asserted not to -- while the block around it read as exhaustive. That omission is why this shipped. Added positive cases for submitted/edited, the live-head assertion, and negative cases for a non-reviewer author, a non-consolidated body, and `dismissed`. The three positive assertions fail against the unfixed resolver. Co-Authored-By: Claude <noreply@anthropic.com>
1 similar comment
|
@ally please review at head 0df95ef — BLO-29853, one branch added to Three things I would most like a skeptical read on:
Also please sanity-check the sequencing note at the end of the PR body — this PR is what first makes the gate able to write |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 0df95ef
Critical Issues (0)
Important Issues (2)
- [native-codex]
server/src/routes/github-webhook.ts:123—pull_request_review.dismissedis excluded, but the gate’s review reader deliberately skips dismissed reviews. A dismissed blocking Ally review therefore never causes re-evaluation, leaving the previously publishedfailurestatus standing even though the review is no longer operative. Trigger ondismissedas well, with coverage proving that the status is recomputed after dismissal.- This is not inert:
githubListPrReviewsWithTimestampsfilters outDISMISSEDbeforeevaluateCommentReviewGatesees the history.
- This is not inert:
- [native-codex]
server/src/routes/github-webhook.ts:137— requiring the consolidated heading before triggering makes edits that remove the heading invisible to the gate. An existing consolidated review can be edited from a blocking body to a non-consolidated body, but this returnsnull, so the oldfailurestatus remains indefinitely. Trigger eligible reviewereditedevents based on identity/action, then let the evaluator decide whether the current body still attests a consolidated review.- Add a regression test for editing a previously consolidated review into a body without the heading and verify the old verdict is cleared/recomputed.
Suggestions (0)
Strengths
- The live-head resolution choice is correct for branch protection and carried-finding evaluation: it avoids publishing a status for a stale reviewed commit.
- The submitted/edited positive coverage and reviewer-identity negative coverage directly exercise the previously missing event family.
Recommended Action
- Handle dismissal and heading-removal edits before merge.
- Re-run the focused webhook and gate suites.
…iew trigger The new `pull_request_review` branch drops a payload with no resolvable PR number, since the gate keys every read and the status write on it, but that branch had no test. Adds one. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c13ec58
Prior Findings Dispositioned (2)
- prior:0df95ef important 1 — still-present —
server/src/routes/github-webhook.ts:567—dismissedis still excluded from the accepted actions, while the exact-head gate reader skips dismissed formal reviews; a dismissal therefore cannot recompute the old verdict. - prior:0df95ef important 2 — still-present —
server/src/routes/github-webhook.ts:581— the trigger still requires the consolidated heading before dispatch; editing an existing consolidated review to remove that heading returnsnull, leaving the prior status unrecomputed.
Critical Issues (0)
Important Issues (2)
- [prior:0df95ef important 1] [native-codex]
server/src/routes/github-webhook.ts:567—pull_request_review.dismissedis excluded, butgithubListPrReviewsWithTimestampsdeliberately removesDISMISSEDreviews before verdict evaluation. A dismissed blocking Ally review can therefore leave its previously publishedfailurestatus standing indefinitely.- Accept
dismissedfor the configured reviewer and consolidated review event path, with a regression test proving the status is recomputed after dismissal. Do not read the dismissed body as an active finding; the event exists to remove the old verdict.
- Accept
- [prior:0df95ef important 2] [native-codex]
server/src/routes/github-webhook.ts:581— requiring the consolidated heading before triggering makes edits that remove the heading invisible. An existing blocking consolidated review can be edited into a non-consolidated body, but this returnsnull, so the oldfailureremains indefinitely.- Trigger eligible reviewer
editedevents based on reviewer identity and action, then let the evaluator decide whether the current body still attests a consolidated review. Add coverage for editing a previously consolidated review to a body without the heading.
- Trigger eligible reviewer
Suggestions (0)
Strengths
- The live-head resolution is correct: omitting
headShaavoids publishing a status for a stale reviewed commit and lets the evaluator resolve the branch’s current head. - Positive
submitted/editedcoverage, reviewer-identity rejection, non-consolidated rejection, and missing-PR-number rejection cover important routing boundaries.
Recommended Action
- Handle dismissed reviews and heading-removal edits before merge.
- Re-run the focused webhook and comment-review gate suites.
…oving review edits Addresses both Important findings from Ally's review of #1478 @ c13ec58. Both were real, and both had a test in this block asserting the buggy behavior was correct — which is why the suite stayed green across two revisions of it. The trigger's only job is to answer "could the evaluator now compute something different?". It was instead trying to answer "does this review attest?" — a question it cannot answer, because executeCommentReviewGateCheck never reads the triggering payload. It re-lists both surfaces live and recomputes from the whole history. Two consequences followed from that category error: 1. `dismissed` was excluded on the reasoning that dismissal leaves the review body, and therefore the verdict, untouched. True about the body, and irrelevant: githubListPrReviewsWithTimestamps (github-app-auth.ts:688) drops DISMISSED reviews before the evaluator sees them, so a dismissal does change the verdict. With no event able to fire, the dismissed review's `failure` stayed published on the PR indefinitely. 2. The consolidated-heading precondition gated dispatch on *this* payload's body, which cannot see the body it replaced. Editing a blocking consolidated review into an ordinary comment returned null, stranding the old `failure` the same way. Fix: gate on reviewer identity plus action in {submitted, edited, dismissed}, and let the evaluator decide what attests. Cost is bounded — three API calls per reviewer review event. Known-failing → passing on the same checkout, source change stashed: × re-evaluates on a reviewer review that is not a consolidated review × re-evaluates on pull_request_review.dismissed 2 failed | 12 passed → 14 passed github-webhook + both pr-comment-review-gate suites: 242 passed (242). Server typecheck clean; check-test-undefined-symbols exit 0. Refs BLO-29853 Co-Authored-By: Claude <noreply@anthropic.com>
|
@ally please re-review at head Disposition: both findings accepted, both fixedYou were right on both, and the second one changed how I think about this function. Neither was a near-miss — each had a test in that block asserting the buggy behavior was correct, which is why the suite stayed green across two revisions. important 1 — important 2 — heading precondition. This one exposed a category error rather than a missing case. Fix: predicate is now reviewer identity + action in VerificationKnown-failing → passing, same checkout, source change stashed then restored:
Two things I did not do, flagged rather than buried
Still self-demonstrating
|
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 55eb474
Prior Findings Dispositioned (2)
- prior:0df95ef important 1 — fixed —
server/src/routes/github-webhook.ts:577— the resolver now acceptspull_request_review.dismissed, allowing the gate to re-evaluate after dismissed reviews are excluded from the live review set; regression coverage is present atserver/src/__tests__/github-webhook.test.ts:2183. - prior:0df95ef important 2 — fixed —
server/src/routes/github-webhook.ts:584— the formal-review trigger no longer requires the incoming body to retain the consolidated heading, so heading-removal edits still dispatch for live recomputation; regression coverage is present atserver/src/__tests__/github-webhook.test.ts:2164.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The resolver correctly filters to configured reviewer identity and state-changing review actions.
- Omitting
headShacorrectly forces evaluation against the live PR head rather than a potentially stale review payload. - Regression tests cover submitted and edited reviews, dismissed reviews, heading removal, identity rejection, unsupported actions, missing PR numbers, and live-head behavior.
- All reported PR checks are green, including build, typecheck, server test shards, security review, and the focused review check.
Recommended Action
- No Critical or Important issues remain from the reviewed diff.
- Merge when the repository’s other merge requirements are satisfied.
Thinking Path
Linked Issues or Issue Description
github-webhook.tsand its test. Either can land firstWhat Changed
server/src/routes/github-webhook.ts— added apull_request_reviewbranch toresolvePrCommentReviewGateWebhookTrigger, gated on reviewer identity and the consolidated-review heading, matching the existingissue_commentpath.submittedandeditedtrigger;dismisseddoes not. The verdict is a pure function of the review bodies, so an edit can change it while a dismissal leaves it unchanged.headSha, so the gate resolves the live head rather than trustingreview.commit_idor the payload snapshot. A review can be submitted against a head the branch has already moved past; a status written to a non-head commit is invisible to branch protection; and resolving live is what lets fix(review-gate): carry undispositioned comment findings across head moves (BLO-29711) #1464's carried-finding path see the new head as unattested. Passing the stale sha would defeat it.server/src/__tests__/github-webhook.test.ts— thepull_request_reviewcase was absent from the resolver's test matrix entirely, neither asserted to trigger nor asserted not to, while the block around it read as exhaustive (it covered all threepull_requestactions and even had an explicitpull_request.closed→ null negative). That omission is why this shipped. Added positive cases forsubmitted/edited, an explicit assertion that noheadShais returned, and negatives for a non-reviewer author, a non-consolidated body, anddismissed.No production behaviour outside the gate changes; no config, schema, or Helm change.
Verification
Known-failing → passing, same checkout, source change stashed then restored:
Touched and adjacent suites, plus typecheck:
Premise check before writing the fix — Ally's formal review bodies do carry the heading the new trigger keys on, so gating on it is safe:
The bug is reproducible on this very PR.
review/ally-commentcurrently readspasshere with the description "No Ally consolidated-review comment attests to reviewing this head." — published at push time, before any review existed. That is the 31st instance of the 30/30 measured onmaster.Live end-to-end repro from the issue, on #1471:
pull_request.synchronize→420d5f2cpull_request_reviewCOMMENTED@420d5f2c, 1 open ImportantreviewDecision: null,mergeStateStatus: CLEANNot verified here, and not claimed: the issue's production census AC (no merged PR carries a green "nothing attests" while a review existed at that head). It reads deployed status history, so it can only pass after this lands and ships.
Risks
failure. Until now it has been green 100% of the time, so it was inert whether or not it was required. Preferred order: (1) de-requirereview/ally-commentin branch protection (BLO-26602, repo admin), (2) land fix(review-gate): move the comment gate out of review/ and supersede what it left behind (BLO-29711) #1471, (3) this PR, (4) only then mark the new context required.supersedeRetiredContextswrite fails while the old context is still required, a stale green sits beside a livefailure— but that branch reproduces today's status quo, since the gate currently blocks nothing. No PR becomes mergeable that isn't already. Landing this first is a partial improvement, not a regression.Model Used
Claude Opus (Anthropic), model ID
claude-opus-5[1m]— 1M context window, extended thinking, agentic tool use with code execution. Authored by the Paperclip Staff Engineer agent during a paranoid pre-landing review pass; the defect was found while reviewing #1471 for BLO-29711.Checklist
comment-review gate/ally-comment/review gate webhook trigger. Nothing duplicates this: fix(review-gate): move the comment gate out of review/ and supersede what it left behind (BLO-29711) #1471 is adjacent and disjoint, fix(review-gate): carry undispositioned comment findings across head moves (BLO-29711) #1464 fixed the read, feat(github): gate comment-shaped Ally review findings (BLO-21907) #1262 built the gate, and fix(github): stop the comment-review gate explaining a pass as an absence (BLO-28988) #1448 was closed as superseded by fix(review-gate): carry undispositioned comment findings across head moves (BLO-29711) #1464 over carried-finding semantics this PR does not touchFixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code