feat(api): gate apply start on confirmed sibling PR check holds - #941
feat(api): gate apply start on confirmed sibling PR check holds#941aparajon wants to merge 4 commits into
Conversation
2d89e15 to
aeae2bb
Compare
8ab2a4c to
3973ef6
Compare
3973ef6 to
d30dc67
Compare
aeae2bb to
44166f4
Compare
Before a driver starts an apply's engine work, it now records a durable preflight check refresh request and waits for the processor to confirm every sibling PR's stored check on the target is held action-required with its hold comment posted. The gate fails closed: a storage error or an unconfirmed hold abandons the drive attempt and leaves the apply claimable, so uncertainty is never converted into a started apply racing a green sibling check. A terminally failed preflight is re-armed for retry and the processor kicked again. The gate skips servers with no check refresh consumer (no GitHub runtime — nothing to hold) and applies with no tasks (a plan with no diff changes nothing, so there is nothing to hold against). Settles are now recorded on every terminal state — always for completed applies, and for failed/cancelled applies whose preflight held sibling checks — so a hold is always released by a re-plan against the live schema. A new preflight_gate_total metric counts passed/timeout/error outcomes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The check refresh request, the operator preflight gate, and their storage contracts are code-host independent: the gate keys off a registered consumer callback and durable request state, and any code-host integration can run the processor that drains requests. Core-layer comments, logs, and metric docs now describe that contract — a check refresh consumer, sibling change checks, a code-host outage — instead of naming GitHub, which is one adapter that implements it. GitHub vocabulary stays where the GitHub adapter lives (pkg/webhook, pkg/github). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The check preflight gate now waits only on the storage-only hold phase — holds_recorded_at, or a completed request for preflights coalesced into a same-target sibling's fan-out — so a code-host outage can never block an apply on the rendering of its own holds. The render keeps retrying separately, and the gate's timeout error and metrics name the hold phase so a sustained block points at storage or the processor, not GitHub. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d30dc67 to
5b5c1e6
Compare
44166f4 to
9614adb
Compare
|
🤖 Review findings - created by Kiran's code review agent - for pull/941, 44166f4. Verdict: 9 findings — 2 blocking (stop-then-restart bypasses the gate with holds released; stop reconciliation gated on GitHub), 3 non-blocking, 4 suggestions. Blocking
Non-blocking
General suggestions
The one thing that could have broken, verifiedThe completed-preflight fast path at operator.go#L1160: the gate's entire safety story rests on the invariant that a completed preflight implies its sibling-check holds remain in force whenever engine work resumes. I tried to prove that invariant and instead disproved it — it is unsafe (Blocking #1). Every link was verified in the worktree: Verified correct
This review was generated by Claude Code (claude-fable-5). |
Why this matters
Holding sibling PR checks is only a guardrail if it happens before the apply changes anything. A multi-hour copy/cutover started from the CLI must not race the flip: if the holds land late, a sibling PR can merge on a green check the apply is about to invalidate. This PR makes the stored holds a hard precondition of the apply itself — and only the stored holds. The gate waits on storage-only writes, never on the code-host rendering of them, so a code-host outage can never block an apply — least of all the CLI apply mitigating an incident. It fails closed on any storage uncertainty. Stack 6/7, on top of #940.
What it does
gateApplyStartOnCheckPreflight), run when a driver claims an apply, before engine work:holds_recorded_at, or a completed request for preflights coalesced into a same-target sibling's fan-out. The code-host rendering (Check Run update, hold comment) retries separately and never blocks the start. A terminally failed request is re-armed withReopenForRetryand re-kicked.schemabot.merge_gate.preflight_gate_totalwithpassed/passed_render_pending/timeout/error):passed_render_pendingmeans the apply started on stored holds while the code-host rendering is still retrying — expected and healthy during a code-host outage; a sustainedtimeoutrate means the processor is not draining or storage is failing.pkg/api,pkg/storage,pkg/metrics) comments, logs, and metric docs describe that contract; GitHub vocabulary lives only in the adapter (pkg/webhook,pkg/github).Closing the loop: a commit pushed to a sibling PR while the apply is mid-flight would re-plan against the pre-apply schema and could mint a fresh green check — #942 closes that by storing such checks born held.
How it moves us toward the northstar
An apply's first observable effect is now telling every affected PR "this target is changing" — before a single row moves. Merge decisions and schema changes stop being able to race each other, and the dependency points the safe direction: the code host depends on SchemaBot's stored truth, never the other way around.
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) → #942 (plan-time holds). Merges bottom-up; each PR retargets to
mainas its base merges.🤖 Generated with Claude Code