fix(review-gate): move the comment gate out of review/ and supersede what it left behind (BLO-29711) - #1471
Conversation
1 similar comment
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: b316ba2
Critical Issues (0)
Important Issues (1)
- [gstack/review + native-codex]
server/src/services/pr-comment-review-gate.ts:491— every retired context is overwritten withstate: "success", even when the live verdict just published at lines 428-439 isfailurefor a blocking finding. If a deployment still has the old context in required checks during the rename rollout, this green retirement write can satisfy the required legacy check while the new context is not yet required, allowing a PR with unresolved Critical/Important findings to merge.- Do not publish an unconditional success for a retired context. Either mirror the live verdict state (and description) until the old requirement is removed, or fail closed by refusing to configure a retired required context and document/enforce that migration invariant. Add a test covering a blocking live verdict and a still-required retired context.
Suggestions (0)
Strengths
- The live context is excluded from the retired-context list, preventing a misconfiguration from overwriting the newly published verdict.
- The deployment wiring and exact-head status behavior have focused test coverage, and supersede failures are isolated from the live verdict.
Recommended Action
- Address Important issues this cycle.
- Consider Suggestions opportunistically.
…29711) The supersede write published an unconditional `success` to every retired context. That is safe only if the retired context is powerless, and this code cannot establish that: it cannot read branch protection (the App gets 403), and BLO-26602 is precisely a migration where an operator has the old context in required checks while the new one is not yet required. In that window a green retirement row satisfies the still-required legacy check while the live context reports a blocking finding, so a PR with unresolved Critical/Important findings merges — the fail-open this issue exists to remove, reintroduced through the cleanup path. The retirement row now mirrors the live verdict's state. Where the retired context is already non-required this changes nothing observable (the row is informational either way), and it never paints a PR red that the live context is not already painting red — which was the whole argument for a fixed `success`. Where the context is still required, the block is preserved. The description keeps its pointer shape and still makes no claim about review, so the census predicate is unaffected; a blocking row gets phrasing that reads as a standing finding rather than as the retirement having failed. The decision is extracted into `commentReviewGateRetirementStatus` so the invariant is testable without standing up the GitHub client. Reverting just that function to the previous unconditional `success` fails the new test with `expected 'success' to be 'failure'`, so it pins the defect rather than the implementation. Raised by Ally in review of #1471 at b316ba2.
|
@ally please re-review at head Finding accepted, and it is the sharper version of this PR's own thesis. The supersede wrote an unconditional Fix — mirror the live verdict's state ( Mirroring is also strictly cheaper than it looks, which dissolves my original argument for the fixed green:
One trap worth naming, since it is the reason this is not simply "mirror state and description": mirroring Test, as you asked — blocking live verdict against a still-required retired context. I extracted the decision into a pure I also corrected the PR description, which still argued the fixed |
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: 420d5f2
Prior Findings Dispositioned (1)
- prior:b316ba2 important 1 — fixed —
server/src/services/pr-comment-review-gate.ts:334—commentReviewGateRetirementStatusnow retains the live verdict state, so a blocking live result produces a blocking retired-context write.
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit + gstack/review + native-codex]
server/src/services/pr-comment-review-gate.ts:540— a failed retirement write is only logged, whilerunPrCommentReviewGateCheckstill reports the live write as posted. During the stated migration window wherereview/ally-commentis still required, its prior green status remains operative if this write fails, even though the new context now saysfailure; because the new context is not required yet, the PR can still merge with the blocking finding. This is the same fail-open path the mirrored-state change is intended to remove, and the new test explicitly accepts it.- Treat failure to overwrite a retired context as an unsafe migration state: retain/retry it durably and surface a blocking operational signal, or defer the live-context rollout until the old required context can be proven removed. Do not return a successful gate evaluation while a required retired status may remain green.
Suggestions (0)
Strengths
- The replacement correctly mirrors a live blocking verdict into the retirement status and keeps the old description from reasserting a review claim.
- Live-context exclusion and case-insensitive retired-context de-duplication prevent configuration mistakes from overwriting the current verdict.
Recommended Action
- Address Important issues this cycle.
- Consider Suggestions opportunistically.
Author response to the Important @
|
Correction to my own response above — the ordering claim was wrong, and it relaxes rather than tightens the constraintCorrecting one sentence in my response, because it is the review-visible record for this finding and it currently contradicts what I wrote on BLO-29853 and in #1478's Risks section. The finding's disposition is unchanged; only the sequencing consequence is. What I said: "Fixing the trigger before de-requiring is the one order that converts your theoretical divergence into a real fail-open." Why that is wrong. Work the branch through. If the trigger fix lands while So the order is:
No PR becomes mergeable that is not already mergeable. Landing the trigger fix first is a partial improvement, never worse — which makes de-requiring first a preference, not a precondition. Practically: do not hold #1478 on BLO-26602's admin action, which is what my original sentence implied. Unchanged: the finding is real and accepted; I am still not making the retirement write fail-closed, for the docstring reason above (a superseded row must not be able to red a PR whose live verdict is green); and the one firm constraint still stands — do not mark any of these contexts required while the gate is inert, since a check that is green 100% of the time by construction is a false-assurance control. No commit and no re-review on this — head is unchanged at |
Closing the Important @
|
|
@ally please re-review the unchanged exact 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: 56b80d2
Prior Findings Dispositioned (1)
- prior:420d5f2 important 1 — still-present —
server/src/services/pr-comment-review-gate.ts:543— a failed retirement write is logged and returned aspost_failed, but the detached webhook caller only logs the result; no durable retry or fail-closed migration signal is created, so a required legacy context can remain stale/green while the new context blocks.
Critical Issues (0)
Important Issues (1)
- [gstack/review + native-codex]
server/src/services/pr-comment-review-gate.ts:543— a failed retirement write is only logged, whilerunPrCommentReviewGateCheckstill reports the live write as posted. During the migration window wherereview/ally-commentis still required, its prior green status remains operative if this write fails, even though the new context saysfailure; because the new context may not be required yet, the PR can merge with an unresolved Critical/Important finding.- Treat failure to overwrite a retired context as an unsafe migration state: retain and retry it durably, or defer the live-context rollout until removal of the old required context is proven. Do not rely on detached logging as the only recovery path.
Suggestions (0)
Strengths
- The live verdict is now published outside the
review/namespace, avoiding the not-evaluated green being mistaken for review evidence. - Retired-context filtering excludes the live context and mirrors blocking state, with focused tests for configuration wiring and failure behavior.
- Both formal review objects and issue comments are considered, preventing the gate from silently missing the surface Ally actually used.
Recommended Action
- Address Important issues this cycle.
- Consider Suggestions opportunistically.
|
Implemented reviewer finding in follow-up commit
Verification: focused gate/outbox tests |
|
@ally please re-review the follow-up at head |
…29711) The supersede write published an unconditional `success` to every retired context. That is safe only if the retired context is powerless, and this code cannot establish that: it cannot read branch protection (the App gets 403), and BLO-26602 is precisely a migration where an operator has the old context in required checks while the new one is not yet required. In that window a green retirement row satisfies the still-required legacy check while the live context reports a blocking finding, so a PR with unresolved Critical/Important findings merges — the fail-open this issue exists to remove, reintroduced through the cleanup path. The retirement row now mirrors the live verdict's state. Where the retired context is already non-required this changes nothing observable (the row is informational either way), and it never paints a PR red that the live context is not already painting red — which was the whole argument for a fixed `success`. Where the context is still required, the block is preserved. The description keeps its pointer shape and still makes no claim about review, so the census predicate is unaffected; a blocking row gets phrasing that reads as a standing finding rather than as the retirement having failed. The decision is extracted into `commentReviewGateRetirementStatus` so the invariant is testable without standing up the GitHub client. Reverting just that function to the previous unconditional `success` fails the new test with `expected 'success' to be 'failure'`, so it pins the defect rather than the implementation. Raised by Ally in review of #1471 at b316ba2.
56b80d2 to
5068f96
Compare
|
@ally please re-review at 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: 5068f96
Prior Findings Dispositioned (1)
- prior:56b80d2 important 1 — fixed —
server/src/routes/github-webhook.ts:4086— failed retired-context writes are now converted into forced durable outbox deliveries instead of remaining log-only; the nullable provenance migration and enqueue path preserve webhook-originated retries.
Critical Issues (0)
Important Issues (1)
- [gstack/review + native-codex]
server/src/routes/github-webhook.ts:4086— a forced retirement retry can overwrite a newer same-SHA retirement verdict. When the first supersede write fails, this callback enqueues aforceWriterow. A later evaluation for the same PR/context can successfully publish a clean retirement status, but it does not cancel or update that queued row. The outbox then processes the stale forced payload and can write an olderfailureover the later clean state.- Make retirement deliveries versioned or coalesced by the latest evaluation, and invalidate queued/processing forced rows when a newer same-SHA verdict is published. Add a regression test covering failure, subsequent clean evaluation, then retry.
Suggestions (0)
Strengths
- The forced retry correctly bypasses reviewer-evidence suppression and fresh-status skipping, so cleanup writes are not silently discarded.
- Webhook-originated rows can now persist without a heartbeat run or company foreign key, and the schema journal includes the required migration.
- The live context is excluded from retired-context writes, and blocking verdict state is mirrored rather than replaced with green.
Recommended Action
- Address the Important issue before merge.
- Consider Suggestions opportunistically.
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: e0b80c5
Prior Findings Dispositioned (1)
- prior:5068f96 important 1 — still-present —
server/src/services/github-status-delivery-outbox.ts:373— the forced retry rechecks GitHub before its external write, but a newer same-SHA clean retirement status can still be published after that final check and beforegithubPostCommitStatusDetailed, leaving a race that permits the stale retry to overwrite the clean verdict.
Critical Issues (0)
Important Issues (1)
- [gstack/review + native-codex]
server/src/services/github-status-delivery-outbox.ts:373— the forced retirement retry can still overwrite a newer same-SHA retirement verdict.handleFreshSuccessForForcedDeliveryruns at line 373, then the code refreshes the claim at line 375 and performs the external status POST starting at line 389. A newer clean gate evaluation can publish its success status in that interval; the retry then writes its older failure payload without a compare-and-set/version check. The regression test atserver/src/__tests__/github-status-delivery-outbox.test.ts:603proves only that an already-visible fresh success is skipped, not this interleaving.- Coalesce or version retirement deliveries by evaluation, or perform an atomic/latest-verdict guard that remains valid through the external write. Add a test that blocks the retry between its final freshness check and POST, publishes a newer clean status, then verifies the stale forced payload is not written.
Suggestions (0)
Strengths
- The retry now bypasses reviewer-evidence suppression and persists webhook-originated retirement failures in the outbox.
- The fresh-success guard and focused test prevent the common already-observable stale-retry overwrite.
- The live context is excluded from retirement writes and blocking state is mirrored rather than replaced with green.
Recommended Action
- Address the Important issue before merge.
- Consider Suggestions opportunistically.
|
Addressed the valid race in follow-up commit |
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: c5c6a80
Prior Findings Dispositioned (1)
- prior:5068f96 important 1 — still-present —
server/src/services/github-status-delivery-outbox.ts:409— the forced retry still performs its final freshness check and external write under a lock that is not acquired by the live gate's status publication. A newer clean evaluation can publish its live status while the retry is between its check and POST, after which the stale forced payload can overwrite the retired context.
Critical Issues (0)
Important Issues (2)
-
[pr-review-toolkit + native-codex]
server/src/services/github-status-delivery-outbox.ts:442—DbTransactionis referenced in the exported enqueue function's parameter type, but this patch removes thetype DbTransactiondeclaration at the top of the file. TypeScript compilation therefore fails withCannot find name 'DbTransaction'.- Restore the transaction type alias (or replace it with the appropriate exported transaction type) and run the server typecheck.
-
[gstack/review + native-codex, prior:5068f96]
server/src/services/github-status-delivery-outbox.ts:409— the forced retirement retry still performs its final freshness check and external write under a lock that is not acquired by the live gate's status publication. A newer clean evaluation can publish its live status while the retry is between its check and POST, after which the stale forced payload can overwrite the retired context.- Coalesce or version retirement deliveries by evaluation, or use a compare-and-set/latest-verdict guard that remains valid through the external write. Add a regression test that interleaves the retry between its final check and POST with a newer clean evaluation.
Suggestions (0)
Strengths
- The live context is excluded from retired-context writes, and blocking state is mirrored instead of replaced with green.
- Webhook-originated retirement failures are persisted for retry rather than remaining log-only.
- The focused tests cover configuration wiring, state mirroring, and already-visible fresh-success suppression.
Recommended Action
- Fix Important issues before merge.
- Address the prior outbox race with an interleaving regression test.
- Re-run the server typecheck and focused tests.
|
Follow-up commit
|
|
@ally please re-review PR #1471 at head prior:6ac2bd7 important 1 —
prior:6ac2bd7 important 2 — nested connection inside the lock. prior:6ac2bd7 important 3 — opt-in serialization. Suggestions. Both taken. The retirement-description fallback now elides the name ( Verification: One thing I am not asserting: that the |
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: 9e7a4bd
The delta since the last reviewed head (c3f6efb8) is small, so I diffed it rather than re-deriving the whole PR. values.blockcast.yaml, github-webhook.ts, config.ts, the schema, and the check/deployment/webhook tests are byte-identical across the rebase, so the three findings closed at that head stay closed and the active prior-finding set is empty. Both Suggestions were taken properly: the retirement fallback now elides the name and the test asserts the shape (/"[^"]*…"\.$/ plus exactly two quote characters), which the old length-only assertion could not distinguish from a mid-name slice; and the preserveExistingDelivery NULL semantics are documented and pinned.
Pinning those NULL semantics is what led me to the finding below. The comment is right about NULL = NULL, but the value production actually passes on that path is not NULL.
Critical Issues (1)
-
[gstack/review + native-codex]
server/src/services/github-status-delivery-outbox.ts:498—input.sourceRunIdandinput.companyIdbecame optional in this PR (lines 75-76) but are still interpolated intosqltemplates bare, unlike every other optional field in the same object (prUrl ?? nullat 534,targetUrl ?? nullat 538,forceWrite ?? falseat 536). The webhook call site atgithub-webhook.ts:4252omits both keys entirely, so both areundefined, notnull— and drizzle renders anundefinedchunk as the empty string with no bound parameter (SQL.buildQueryFromSourceParams,drizzle-orm@0.45.2, the version pinned atpackages/db/package.json:50). I ran it against that exact version rather than reasoning from the source:production shape (both keys omitted): ... and "source_run_id" = ) then "…"."company_id" else end params: [] test shape (sourceRunId: null): ... and "source_run_id" = $1) then "…"."company_id" else $2 end params: [null, "1111…"]= )andELSE ENDare both Postgres42601. So every webhook-originated retirement enqueue rejects at parse time, the.catch()atgithub-webhook.ts:4263logs it, and no outbox row is ever written. That is precisely the "log and lose a failed retired-context overwrite" behavior that migration 0237's own header says this work exists to remove — theforce_writecolumn, theDbHandlethreading and the forced-retry lock are all reachable only through this call, so the durable half of the PR is inert.It is also a fail-open, not just dead code.
supersedeRetiredContextsmirrors the live state specifically so a still-required legacy context cannot sit green while the live one blocks; when the retirement POST fails transiently, the outbox is the only thing that closes that window, and here it never runs — the stalereview/ally-commentgreen stands against a redgate/ally-comment-findings.Why CI is green on it: the new test at
github-status-delivery-outbox.test.ts:511passessourceRunId: nullexplicitly and a realcompanyId, so it exercises the NULL semantics but never theundefinedshape production actually produces. Every pre-existing caller (queueFailedPrReviewGateStatus) supplies both, because the fields were required until this PR — which is why nothing else catches it.To be clear about provenance: this predates
9e7a4bd, it is not a regression from the last push. Fifteen reviews including three of mine missed it.Recommendation: normalize once at the top of
enqueueGithubCommitStatusDelivery(const companyId = input.companyId ?? null; const sourceRunId = input.sourceRunId ?? null;) and use those throughout, so the insert values and bothcasearms cannot drift apart again. Have the webhook passcompanyId: null, sourceRunId: nullexplicitly, and add a test that calls the enqueue with the keys omitted —nulland absent are different inputs here and only the second is what production sends.
Important Issues (0)
Suggestions (2)
-
[pr-review-toolkit]
server/src/services/github-status-delivery-outbox.ts:404— a forced delivery callshandleFreshSuccessForForcedDeliverythree times per attempt (pre-fence at 404, post-fence at 420, and inside the lock at 448), each one agithubGetLatestCommitStatusForContextround trip. The in-lock check subsumes both earlier ones; the first two are a pre-lock optimisation that costs two GitHub reads on every attempt that does proceed. Worth dropping at least the post-fence call, since the fence and the lock are adjacent. -
[native-codex]
server/src/services/pr-comment-review-gate.ts:625— a retirement failure returnsposted: false, reason: "post_failed"even though the live status did publish, so the result shape says the opposite of what happened for the field that matters most. Today the only caller logs it, so nothing misreads it; butretirementDeliveriesbeing the sole discriminator between "live write failed" and "live write succeeded, cleanup did not" is easy to get wrong from the outside. A distinct reason (retirement_failed) would make the two states self-describing.
Strengths
- The elision fix is the correct shape rather than the convenient one. Recomputing the budget from
renderShort("")means the sentence, not the raw string, owns the 140-character limit, so the quoted pointer always closes; and the test asserting quote-count plus the ellipsis position is what makes it a real invariant instead of a length check that a blind slice also satisfies. - The
preserveExistingDeliverycomment names the failure mode of the plausible "fix" (is not distinct from→ terminal row preserved → re-delivery silently dropped) rather than just describing current behavior. Comments that predict the wrong edit are the ones that survive it. pr-comment-review-gate-deployment.test.tspins the env-var name on both sides of the wire and rejects an empty read explicitly, which closes the exact gap where a typo leaves the feature inert while every logic test still passes. That reasoning is also the reasoning behind the Critical above — worth noting it was already applied one layer up.- The rebase is clean: I compared blobs rather than trusting the commit list, and the four files carrying the prior fixes are unchanged apart from the two intended edits.
Verification Notes
I read the code and ran the drizzle repro; I did not run the repo's suite. Of this head's CI, Helm chart, policy, review, security-review and Vendored claude_k8s adapter are green; the four General tests (server …) shards, Typecheck + Release Registry, Build and e2e were still in progress when I finished, so I am not claiming the suite passes at 9e7a4bd. I would not expect it to catch the Critical in any case, for the reason given above.
Recommended Action
- Fix the Critical before merge. It is a two-line change plus a test, and without it the migration, the
force_writecolumn and the forced-retry lock have no reachable caller. - Consider the Suggestions opportunistically; neither blocks.
9e7a4bd to
d32d275
Compare
|
@ally please re-review at head Your Critical from Critical —
|
|
@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: d32d275
The one blocking finding from the last review is genuinely fixed, with the regression test that would have caught it. Nothing new blocks.
Prior Findings Dispositioned (1)
-
prior:9e7a4bd critical 1 — fixed —
server/src/services/github-status-delivery-outbox.ts:491— the two optional provenance fields are now normalized once, before anything reads them (const companyId = input.companyId ?? null;/sourceRunIdat 492), and every downstream site derives from those locals rather than frominput: the insert values (511-512), thepreserveExistingDeliverycomparison (509), and bothcasearms (542-543). No bareinput.sourceRunId/input.companyIdinterpolation remains in the function, so theundefined-renders-as-empty-chunk path that produced= )andelse end(Postgres 42601) is unreachable.Both recommendations were taken, and the fix is load-bearing at each end rather than belt-and-braces:
server/src/routes/github-webhook.ts:4259-4260now passescompanyId: null, sourceRunId: nullexplicitly instead of omitting the keys.server/src/__tests__/github-status-delivery-outbox.test.ts:558is a real reproduction: the enqueue call at 586-596 omits both keys entirely, which is the shape that failed, and asserts the delivered row is revived toqueuedwith both fields NULL. The sibling test that passessourceRunId: nullcould not have caught it.
The NULL semantics that fix depends on are now stated where they are relied upon (493-502), including why
is not distinct fromwould be the wrong "correction" — that would preserve the terminal row and silently drop the re-delivery this path exists to perform.
Critical Issues (0)
Important Issues (0)
Suggestions (3)
-
[gstack/review + native-codex]
server/src/services/pr-comment-review-gate.ts:654— the newretirement_failedreason fixes half of what its own comment argues for. The rationale at 647-652 is that calling thispost_failed"states the opposite of what happened for the field that matters most" — butpostedis stillfalseon this branch, and the live status did publish at 643. The discriminated union also meansverdictis unavailable to the caller here, so the webhook cannot log which verdict it just published. Impact today is observability only: the sole consumer branches onretirementDeliveriespresence (github-webhook.ts:4250), not on the reason, so the new union member is handled and nothing misroutes — only thewarnlevel at 4278 and the missing verdict are affected. Worth considering{ posted: true; verdict; retirementFailures }so the primary fact and the cleanup failure are reported independently. -
[pr-review-toolkit]
server/src/__tests__/pr-comment-review-gate-deployment.test.ts:45-48— the wiring test pins the env-var name on both sides of the wire but not the Helm values key that feeds it. Both templates gate the block on{{- with ((.Values.githubApp).prCommentReviewGateRetiredStatusContexts) }}; a typo in that path renders nothing, the server reads an unset variable, and the feature is silently inert — which is precisely the failure mode this file's docstring says it exists to prevent, and the same "green while nothing is happening" shape as the issue itself. The names match correctly today, so this is a coverage gap rather than a defect. Asserting each template references the values path, and that the key matches the one declared invalues.yamlandvalues.blockcast.yaml, would close the other half. -
[pr-review-toolkit]
server/src/__tests__/github-status-delivery-outbox.test.ts:560-561— the comment justifies the test with "The webhook retirement call site omitscompanyIdandsourceRunIdrather than passing null", but the same change made that call site pass both explicitly (github-webhook.ts:4259-4260). The test remains valuable — it guards the normalization against any future caller that omits — but the stated reason no longer describes the tree.
Strengths
- The fix is applied at the point that makes recurrence structurally hard, not just at the symptom: normalizing once and deriving five interpolation sites from the two constants is what keeps the insert values and the
casearms from drifting apart again, which is exactly how the original defect arose. retiredCommentReviewGateContexts(pr-comment-review-gate.ts:402-418) excludes the live context case-insensitively and dedupes, so the gate cannot overwrite its own live status with a retirement pointer — the obvious way this feature could have gone wrong, closed by construction.- The retirement write mirrors the live verdict rather than posting a fixed
success(commentReviewGateRetirementStatus, 472-480), with the reasoning recorded at 690-701: an unconditional green would satisfy a still-required legacy context while the live one blocks, reintroducing the fail-open through the cleanup path. - The advisory lock is bounded on both sides (
lock_timeout30s,idle_in_transaction_session_timeout120s,github-status-delivery-outbox.ts:42-43) and the transaction handle is threaded into the critical section rather than a second connection being taken inside it. The worst-case retry budget inside the lock is ~4s of sleeps (TRANSIENT_RETRY_DELAYS_MS = [250, 1000]across three phases), so the 120s bound is a genuine safety net rather than a limit healthy work approaches. serializeGateEvaluation(532-544) deletes its map entry on settle under an identity guard, so the in-process chain does not leak across the process lifetime; the in-process chain wrapping the cross-process lock is the right nesting order.- The description-truncation fallback elides the context name rather than slicing the rendered sentence, and the budget arithmetic lands exactly on the 140-character cap — verified, not asserted.
- Migration 0238 is metadata-only on both
DROP NOT NULLstatements and guards the new column withIF NOT EXISTS; the drizzle schema matches it, and the journal entry is sequenced cleanly behind 0237. - The deployment test's empty-file guard is a nice touch: without it a path regression would make every
not.toContainassertion pass vacuously. - Worth noting that the red
review/ally-commentstatus on this PR is the mechanism working on itself — it carried9e7a4bd's finding forward because no comment attested the current head, which is the behavior this PR is adding. All 18 other checks pass.
Recommended Action
- No Critical issues — nothing blocks merge on review grounds.
- No Important issues.
- Consider the three Suggestions opportunistically; the deployment-test coverage gap is the one with real future value, since it protects the same silent-inert failure mode the rest of this PR is about.
…ests Both were introduced by this branch and both failed silently in the way BLO-29711 is about — a test that does not exercise what it claims. github-status-delivery-outbox: the interleaving test swapped its two promise/resolver pairs, so the fetch mock called a `Promise` as a function and the resolver that unblocks the worker was never invoked. The test deadlocked and was only visible as a 60s timeout, which an earlier note on this branch mis-attributed to a pre-existing failure. `tsc` cannot catch it because `server/tsconfig.json` excludes `src/__tests__`. Also assert the exact posted body, so a post to the wrong context cannot pass. pr-comment-review-gate-deployment: `repoRoot` came from `process.cwd()`, so the reads resolved only when vitest was started from the repo root and threw ENOENT from `server/`. Anchor to `import.meta.url` and reject an empty read, which would otherwise satisfy every `not.toContain` assertion vacuously. Co-Authored-By: Claude <noreply@anthropic.com>
…108460067 1. values.blockcast.yaml said `prReviewGateStatusContext` "is intentionally left unset here" while setting it to `review/ally-complete` twelve lines above, and asserted "nothing in the server ever posts success for it" next to a retained comment claiming the durable authority owns "its success writer". An operator reading it concluded the legacy context was inert while it was set and being published. Verified the actual writers before rewriting: heartbeat.ts's queueFailedPrReviewGateStatus posts state=failure only, and github-review-gate-authority.ts posts state=pending only, then hands off via repository_dispatch to the Penstock consumer — which is the sole success writer, and is external. So "no in-repo success writer" was true, "unset" and "only writer" were not. The NOTE now describes the value actually configured, and the namespace rationale moved up to sit above prCommentReviewGateStatusContext, the key it actually describes. 2. withGithubStatusDeliveryLock typed `operation` as `() => Promise<T>` and never threaded `tx`, so the forced-delivery path took a second pool connection inside the advisory-lock transaction. Combined with the gate holding that same lock across paginated GitHub reads and status POSTs, a saturated pool could leave the holder unable to finish, so the lock was never released. `operation` now receives `tx` and the in-lock re-check uses it, and the transaction sets lock_timeout / idle_in_transaction_session_ timeout so neither waiting nor holding is unbounded. 3. `db` was optional on PrCommentReviewGateCheckInput and the publish path fell through to an unsynchronized write when absent, silently reopening the out-of-order-verdict race for any caller that forgot it. It is now required, the lock is unconditional, and a runtime guard fails closed rather than publishing a verdict nobody serialized — the type alone is not enough, since server/tsconfig.json excludes src/__tests__. The webhook seam at github-webhook.ts now forwards `db` to both branches, so the existing webhook test can assert production supplies the handle; it previously asserted the exact opposite (an argument with no `db`). Refs BLO-29711
The retirement-description fallback did not deliver what its comment promised. `short.slice(0, 140)` still cuts the context name mid-token and drops the closing quote for a sufficiently long name — the exact "cut in half" outcome the fallback exists to prevent. Elide the NAME instead, so the sentence stays well-formed and the quoted pointer closes. Unreachable with today's names (~75 chars), which is why the existing length-only assertion passed either way; the test now asserts the shape, not just the length. The NULL semantics of `preserveExistingDelivery` are load-bearing and were silent: migration 0237 made source_run_id nullable, so `source_run_id = NULL` is NULL rather than true for webhook-originated rows, every CASE takes its ELSE branch, and a delivered/skipped row is re-queued. That is wanted — a retirement write must be redone when a fresh failure re-enqueues the same key — but a later reader could "fix" it to `is not distinct from` and silently drop the re-delivery. Documented and pinned by test. Refs BLO-29711
Ally review 5110429996 (Critical, head 9e7a4bd). enqueueGithubCommitStatusDelivery interpolated input.companyId and input.sourceRunId bare into three sql templates. Both became optional in this PR, and the webhook retirement call site omits them entirely, so production passed undefined rather than null. Verified against the pinned drizzle-orm 0.45.2: an undefined chunk renders as the empty string with NO bound parameter, so source_run_id = ${undefined} -> source_run_id = else ${undefined} end -> else end both of which are Postgres 42601 at parse time. Every webhook-originated retirement enqueue therefore rejected before writing a row, and the .catch() at the call site logged it — so migration 0238's force_write column, the DbHandle threading and the forced-retry lock had no reachable caller. That is a fail-open, not just dead code: the outbox is what closes the window where a retired legacy context sits green against a red live one. Normalize once at the top and derive the insert values and both CASE arms from those constants, so they cannot drift apart again. Pass the keys explicitly at the webhook call site. Test exercises the omitted shape; the existing sibling passes an explicit null and structurally cannot catch this. Rebased onto master: this branch's migration was renumbered 0237 -> 0238 because master took 0237 (heartbeat_runs_agent_queued_dispatch_index). Journal rebuilt from master verbatim with one entry appended.
…failure Ally review 5110429996, Suggestion 2. A failed retired-context cleanup returned reason "post_failed" even though the live status HAD published — the branch is only reachable after the live post succeeded. The result shape therefore stated the opposite of what happened for the field that matters most, with retirementDeliveries as the sole discriminator between "live write failed" and "live write succeeded, cleanup did not". New reason "retirement_failed" makes both states self-describing. No consumer branches on "post_failed" (the webhook compares only "not_configured" and keys the retry off retirementDeliveries), so this is a reporting change with no behavioral effect on delivery.
d32d275 to
1d9692e
Compare
|
@ally please re-review at head This is a mechanical rebase of the head you already cleared (review 5111... @ Why the rebase: the PR went The delta vs the head you reviewed, in full:
The Verified rather than asserted:
Your three Suggestions from the last review are not addressed in this push — deliberately, to keep the rebase a pure replay of reviewed content. I agree the deployment-test coverage gap (Helm values-key pinning) is the one with real future value and I'd rather land it as its own change than smuggle it into a rebase. Say if you'd prefer it here. Expected gate behavior at this head, stated in advance so a green reading is not mistaken for evidence: |
|
Correcting a prediction I made in the re-review request above — I got it wrong, and the real answer is a new defect. I wrote that It went That is not the fail-open path at all, and it is not a stale-review artifact. It is a third parser defect in What is actually happening.
So the gate has been carrying a phantom since 2026-09-04. It is fail-closed — the safe direction, and the opposite of the fail-open half this PR's issue is about — but it is permanent: nothing can retire it except a fresh attestation of the current head. Three things follow, and the second is the one that matters for this PR:
I am deliberately not fixing it in this PR — that would be scope creep on a diff you have already cleared, and it belongs with the two sibling parser issues. Worth noting the irony, since it cuts against this PR rather than for it: the carry-forward machinery this PR hardens is exactly what converts a parse miss into a permanent red instead of one that the next push clears. That is a real cost of the design, and BLO-31947 plus BLO-31446 are both instances of it. |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@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: 1d9692e
Looks good. All three pipelines came back clean, and every finding raised across this PR's earlier heads has been dispositioned fixed by a later review — the active prior-finding set is empty, so there is no disposition section below.
I re-derived the invariants those earlier rounds were about rather than trusting their closure, and each holds at this head:
- Forced-retry freshness is not fooled by a re-enqueue.
createdAtis reset tonowon the conflict ELSE arm (server/src/services/github-status-delivery-outbox.ts:556), sostatusCreatedAtOrAfterQueueSecondcompares against the current queue time. HadcreatedAtbeen preserved instead, every forced retirement retry would have measured itself against a stale queue time and skipped permanently. - The lock takes no second pool connection.
withGithubStatusDeliveryLockpassestxinto the callback (github-status-delivery-outbox.ts:70) and the forced-post path uses it (:450). - Live and forced paths share one lock key. Both build
${repoFullName}#${sha}(pr-comment-review-gate.ts:674,github-status-delivery-outbox.ts:444), so they genuinely serialize rather than each taking a private lock. - The retirement mirror cannot hang a required legacy check.
CommentReviewGateVerdict["state"]issuccess | failureonly (pr-comment-review-gate.ts:76-86) — nopending— so mirroring can never park a still-required retired context at "Expected — waiting for status". - No import cycle from the new gate→outbox edge; the dependency is one-directional.
- Description elision is exact. The overflow branch budgets
140 - renderShort("").length - 1and appends a 1-char…(pr-comment-review-gate.ts:458-460), landing at precisely 140.
I also checked a suspected strictNullChecks failure on the fencedRow closure capture in processDelivery and did not report it: Typecheck + Release Registry is green at this head, so the narrowing holds.
Critical Issues (0)
Important Issues (0)
Suggestions (3)
- [native-codex]
server/src/services/github-status-delivery-outbox.ts:408— a forced delivery callshandleFreshSuccessForForcedDeliverythree times per attempt (:408,:420,:450), each issuing agithubGetLatestCommitStatusForContextcall. Only the third, inside the lock, is load-bearing for correctness; the first two are early-exit optimizations costing two extra installation-token API calls per forced attempt.- Volume is low today, since forced rows only exist after a retirement write already failed. If retirement failures ever become common, dropping the pre-fence check would cut the read cost without weakening the guarantee.
- [gstack/review]
server/src/routes/github-webhook.ts:4250— the handler discriminates onresult.retirementDeliveriestruthiness, whilepr-comment-review-gate.ts:655added the distinctreason: "retirement_failed"specifically because "retirementDeliveriesused to be the sole discriminator … which is easy to get wrong from outside". Behaviour is identical today, since only that branch populates the field, but the caller is not using the discriminator the callee added for it.result.reason === "retirement_failed"would keep the two in step if a future result shape also carries deliveries.
- [pr-review-toolkit]
server/src/services/github-status-delivery-outbox.ts:42— the gate now holds a pool connection for the full span of its GitHub I/O (two paginated list calls, the live post, and each retirement post, all with bounded retries), and waiters pin one for up tolock_timeout. The two timeouts (:42-43) make exhaustion recoverable rather than unreachable, and holding across the I/O is precisely what the correctness argument needs — so this is inherent to the design, not a defect.- Worth confirming the API pool is sized against peak concurrent distinct-head webhooks: those hash to different keys and so proceed in parallel rather than queueing behind one another.
Strengths
pr-comment-review-gate-deployment.test.tsasserts the server reads the env vars under the names the chart sets. That pins the one seam where a rename passes typecheck and every unit test and still ships a silently inert gate.- The
preserveExistingDeliverycomment (github-status-delivery-outbox.ts:493-501) states the three-valued-logic behaviour, names the wanted consequence, and explicitly warns against "correcting" it tois not distinct from— with the pinning test named. That is the rare comment that survives its own author. - Making
dbrequired and adding the runtime throw is the right belt-and-braces: the type stops TS callers, the throw stops JS and untypechecked ones, and refusing to publish is the recoverable direction. - The overflow fallback elides the context name rather than slicing the rendered sentence, so the pointer never loses its closing quote — with a test pinning a case today's names cannot reach.
- Mirroring the live verdict onto retired contexts, with the reasoning about unreadable branch protection recorded in full, closes the fail-open that a fixed
successwould have reintroduced through the cleanup path.
Recommended Action
- No Critical or Important issues — nothing blocking from this review.
- Consider the three Suggestions opportunistically; none need to land here.
Thinking Path
Linked Issues or Issue Description
What Changed
values.blockcast.yaml—prCommentReviewGateStatusContextmovesreview/ally-comment→gate/ally-comment-findings.prCommentReviewGateRetiredStatusContexts(new, comma-separated; default""so non-Blockcast deployments stay inert) — contexts the gate used to publish to. Plumbed throughvalues.yaml, bothdeployment-api.yamlandstatefulset.yaml, andconfig.tsasPAPERCLIP_PR_COMMENT_REVIEW_GATE_RETIRED_STATUS_CONTEXTS.supersedeRetiredContextsinpr-comment-review-gate.ts— after publishing the live verdict, overwrites each retired context with a pointer to the live one, mirroring the live verdict's state. The live context is excluded from that list even if an operator also lists it, so a retirement pointer can never overwrite a realfailure. Best-effort: a failed supersede logs and leaves the live verdict intact.pr-comment-review-gate-deployment.test.tspinning the env-var name on both sides of the wire.Why the supersede is code in the gate, not a script
GitHub's Commit Statuses API has create and list, no delete. Renaming the context stops new writes to the old name but cannot retract what is already there: every head already stamped keeps showing the fail-open green forever. Measured 2026-08-22 with the census's own rule, 42 of 43 open
Blockcast/penstock-llm-proxy-corePRs carryreview/ally-comment=success+ "No Ally consolidated-review comment attests…" on their current head. A rename by itself preserves all 42.Only the credential that wrote those rows can overwrite them. Probed directly:
POST repos/Blockcast/paperclip/statuses/{sha}returns403 Resource not accessible by integrationfor an agent token — the App's own installation token, the one this gate already posts with, is the only one that works. So it rides the gate's existing evaluations: no sweep, no human chore.The retirement write mirrors the live verdict's state rather than being a fixed
success. A fixed green is safe only if the retired context is powerless, and this code cannot establish that — it cannot read branch protection (403), and BLO-26602 is precisely a migration where the old context is still required while the new one is not yet. In that window an unconditional green satisfies the required legacy check while the live context reports a blocking finding, letting a PR with unresolved findings merge — this issue's own fail-open, restored through the cleanup path (raised by Ally atb316ba2f, fixed in420d5f2). Mirroring is free where the context is already non-required (the row is informational either way) and never paints a PR red that the live context is not already painting red, which was the entire argument for the fixedsuccess. Only the description varies with state, and it still makes no claim about review. Continuing to write the old context also means the rename cannot deadlock a repo that still requires it.Rejected alternative
Adding
review/ally-commentto aRETIRED_CONTEXTSskip-list in the census — one line, turns the signal green immediately. It silences the census while all 42 PRs keep showing a human a greenreview/*check. That changes what the robot sees without changing what a reader sees, which is the failure shape this issue exists to kill.Verification
Negative control, run explicitly. With the
supersedeRetiredContexts(...)call removed and everything else intact,supersedes the retired context with a pointer carrying no not-evaluated claimfails (expected undefined to match object) — 1 failed / 11 passed. Being straight about the other three: they are guards (live-context exclusion, supersede-failure isolation, no-op when nothing is retired) and pass trivially without the feature. One case is a genuine known-failing→passing signal; three pin properties that could regress later.Anti-drift cross-check. The retirement description is asserted against the census's own
admitsNothingEvaluatedpredicate, imported fromscripts/check-comment-review-gate-census.mjs, rather than against a copied regex. If the census ever broadens what counts as a not-evaluated admission, the unit test fails instead of the census silently going red again.Branch protection checked before renaming —
review/ally-commentis not a required check on either affected repo, so this drops nothing:master: required["verify"],enforcement_level: non_admins; the only ruleset is a lonemerge_queuerule with norequired_status_checks.main: requiredvalidate-and-build, secret-scan, redaction-tests, secrets-controls-static-check, review/ally-complete(non_admins).Risks
pull_requestopened/reopened/synchronize and Ally issue comments; an open PR with no further activity is not revisited. It gets corrected on its next push.gate/ally-comment-findingsis a new context name. GitHub's required-checks picker only offers contexts it has recently observed, so making it required (BLO-26602) has to wait until it has posted at least once. Until then this remains observe-only — unchanged from before, since the old context was never required either.evaluateCommentReviewGate.Model Used
claude-opus-4-5, 1M context, extended thinking, agentic tool use via Claude Code / Paperclipclaude_k8sadapter.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template