Skip to content

feat(api): gate apply start on confirmed sibling PR check holds - #941

Draft
aparajon wants to merge 3 commits into
armand/check-hold-fanoutfrom
armand/check-preflight-gate
Draft

feat(api): gate apply start on confirmed sibling PR check holds#941
aparajon wants to merge 3 commits into
armand/check-hold-fanoutfrom
armand/check-preflight-gate

Conversation

@aparajon

@aparajon aparajon commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

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 holds a hard precondition of the apply itself — the drive does not start engine work until every sibling PR's check flip and hold comment are confirmed landed, and it fails closed on any uncertainty. Stack 6/7, on top of #940.

What it does

  • Preflight gate (gateApplyStartOnCheckPreflight), run when a driver claims an apply, before engine work:
    • No merge gate consumer registered (no GitHub runtime) → skip; nothing to hold.
    • Apply has no tasks (a plan with no diff) → skip; an apply with no diff changes nothing, so there is nothing to hold against.
    • Otherwise: record a durable preflight request, kick the processor, and wait for the request to complete. A terminally failed request is re-armed with ReopenForRetry and re-kicked.
    • Timeout or storage error → the drive attempt is abandoned and the apply stays claimable; the gate never converts uncertainty into a started apply.
  • Settle on every terminal state: completed applies always record a settle; failed/cancelled/errored applies record one when a preflight exists, so holds are always released by a re-plan against the real schema — never by cleanup alone.
  • Gate outcome metric (schemabot.merge_gate.preflight_gate_total with passed / timeout / error): a sustained non-passed rate means the processor is failing to drain preflights and applies on GitHub-configured servers are not starting — the metric comment says exactly that.
  • The core stays code-host neutral. The gate keys off the registered merge gate consumer and durable request state — never a GitHub type. Core-layer (pkg/api, pkg/storage, pkg/metrics) comments, logs, and metric docs describe that contract (a merge gate consumer, sibling change checks, a code-host outage); GitHub vocabulary lives only in the adapter (pkg/webhook, pkg/github), which is one implementation of the hold/re-plan fan-out. A future code-host integration registers its own consumer and renders the same stored state.
  • Test harness now mirrors production: the default webhook integration handler starts the merge gate processor (the gate requires one), with an explicit no-processor constructor for tests that drive the drain lifecycle manually.
 driver claims apply
        │
        ▼
 consumer registered? ──no──► start engine work (no GitHub runtime)
        │yes
 apply has tasks? ────no────► start engine work (no diff, nothing held)
        │yes
 record preflight ──► kick processor ──► wait for holds confirmed
        │                                    │
        │ storage error / timeout            │ completed
        ▼                                    ▼
 abandon drive attempt                 start engine work
 (apply stays claimable,               (sibling checks now
  fail closed)                          action-required + commented)

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, which is exactly the safety bar a git-as-interface schema workflow has to clear.

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 main as its base merges.

🤖 Generated with Claude Code

aparajon and others added 3 commits August 5, 2026 11:14
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>
@aparajon
aparajon force-pushed the armand/check-hold-fanout branch from 3973ef6 to d30dc67 Compare August 5, 2026 15:14
@aparajon
aparajon force-pushed the armand/check-preflight-gate branch from aeae2bb to 44166f4 Compare August 5, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant