feat(github): store born-held checks while a preflighted apply changes the target - #942
feat(github): store born-held checks while a preflighted apply changes the target#942aparajon wants to merge 1 commit into
Conversation
bcd6aa4 to
4d42d5f
Compare
aeae2bb to
44166f4
Compare
…s the target Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
44166f4 to
9614adb
Compare
4d42d5f to
c2ddcf0
Compare
|
🤖 Review findings - created by Kiran's code review agent - for pull/942, 4d42d5f. Verdict: 8 findings — none blocking; 4 non-blocking (a narrow residual TOCTOU window plus observability and test-convention gaps), 4 suggestions. Non-blocking
General suggestions
The one thing that could have broken, verifiedThe born-held conversion of a would-pass verdict at the plan-write choke point (check_records.go#L221–:249). I verified its supporting premises directly in the worktree: (1) Verified correct
This review was generated by Claude Code (claude-fable-5). |
morgo
left a comment
There was a problem hiding this comment.
🤖 Approving on Morgan's behalf (automated review).
First, a correction that is mine to make: this PR is green and I had it recorded as failing. The rollup shows three FAILURE entries — E2E Tests, LocalScale Tests, K8s E2E Tests — but all three belong to run 31223145894, which was cancelled wholesale (13 of its jobs are CANCELLED, several still carrying unexpanded ${{ matrix.shard }} names). They are aggregate gate jobs reporting FAILURE because their matrix dependencies were cancelled, not because anything failed. The live run 31223147191 is green on all three of those same names. mergeStateStatus is CLEAN, which agrees. My earlier sweep counted non-SUCCESS as failure and held this PR on that basis; that was wrong, and it's why it sat.
On the change itself — small (+144/-0), additive, and it closes a real hole rather than papering one.
It's the missing half of a race the fan-out could previously only log. holdStoredPRChecks writes each hold through MarkBlockedForApplyInFlight, which is head-SHA-conditional, and its !flipped branch already reasons about "a racing synchronize re-planned a newer head against the pre-apply schema." Before this PR that racing plan won and the gate reopened mid-apply; now the plan checks the target first and is born held. The two pieces fit, and the fit is what makes me confident the mechanism is right rather than merely plausible.
Checked the failure directions:
- Storage error fails the write, not open. The comment claims "never by assuming the target is quiet" and the code delivers — it returns the error rather than falling through to a passing check. That's the one place where a fail-open would be serious, and it isn't one.
- Only a would-pass verdict is flipped. Verdicts that already block keep their more specific reason, and they don't need the hold since they block on their own merits.
ErrorMessageis newly set in the struct literal but was already the zero value on the unheld path, so the non-held case is unchanged rather than newly clearing a field.
Two residual windows, both narrow and both failing safe — worth recording because neither is closed, not because either should block:
- Hold lands between the query and the write.
HasActivePreflightedApplyOnTargetreturns false, a preflight then holds the row, and this write overwrites it passing. The window is two adjacent storage calls, so it's small — and it self-corrects, becausefanOutCheckSettlere-plans every sibling check for the target, not only held ones. Worth knowing thatrearmPreflightRendersForActiveAppliesis not a backstop here: it re-arms renders, not stored holds. - Born held after the settle fan-out already ran. The check is written held with nothing left to release it until the next push or plan. Fail-closed (a stuck block, not a stuck open), and
RecordMergeGatePlanTimeHold's doc anticipates exactly this — "a sustained rate with no matching settle re-plans means holds are piling up on the target." Instrumenting the failure mode you can't fully close is the right call.
One note on landing order: this is stacked on #941 (armand/check-preflight-gate), which I'm holding separately as a core change to the apply-start path. This approval is on #942's own diff and doesn't carry to its base.
Nit: clampDriftSummary is doing duty as a generic length clamp for a hold summary that has nothing to do with drift; the name will read oddly at the next call site someone adds.
Not blocking.
Why this matters
The preflight fan-out holds every sibling check that exists when an apply starts — but a sibling that pushes a commit (or runs a manual plan) mid-apply gets a fresh plan against the mid-change schema, and a passing verdict from that plan would mint a fresh green check that sidesteps the hold entirely. This closes the last first-party gap in the merge gate: plan-time writes now know when the target is mid-apply. Stack 7/7, on top of #941.
What it does
upsertPlanCheckRecord— the single choke point every plan-time stored-check write funnels through (auto plans, manual plans, and the settle fan-out's re-plans) — consultsHasActivePreflightedApplyOnTargetbefore storing a verdict that would pass. If a preflighted apply is active on the (environment, database type, database) target, the check is stored born held:action_requiredwith the sameapply_in_flight_on_targetblocking reason the preflight fan-out writes, so the aggregate check and its release path are identical to any other hold.schemabot.merge_gate.plan_time_holds_total— a sustained rate with no matching settle re-plans means holds are piling up on a target; check the merge gate processor's logs.How it moves us toward the northstar
With this, every path that can produce a passing check during an apply window is closed at the source: existing checks are held by the preflight fan-out, and new checks are born held. The merge gate on a busy target is now airtight against first-party surfaces — what remains (admin overrides, unprotected branches) is outside the code host's checks and is handled by drift detection and, eventually, merge-time revalidation.
The chain: #867 (storage) → #868 (drive-tail recording) → #866 (settle re-plan processor) → #939 (request kinds + hold storage) → #940 (preflight hold fan-out) → #941 (apply-start gate) → this PR (plan-time holds). Merges bottom-up; each PR retargets to
mainas its base merges.🤖 Generated with Claude Code