Skip to content

feat(promotion): gate promotion out of an org on a passing check - #66

Open
scott-lowe-vapi wants to merge 1 commit into
fix/promotion-commit-applied-transitionsfrom
feat/promotion-check-gate
Open

scott-lowe-vapi wants to merge 1 commit into
fix/promotion-commit-applied-transitionsfrom
feat/promotion-check-gate

Conversation

@scott-lowe-vapi

@scott-lowe-vapi scott-lowe-vapi commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Value

V.A.L.U.E. tier: project — PR 10 of 10 for inline simulation PR checks (TEST-141), the "check before deploy" step in promotion.

Stacked on #65 (the promotion partial-failure fix), now that #57–#64 have merged. This PR is the single gate commit.

  • Problem: promotion copies staging's reviewed files into production, but nothing checks that staging's agents still behave before they move on. Teams promoting dev → staging → prod need a behaviour gate between orgs, without new infrastructure.
  • Who it affects: multi-org gitops users (the promotion pipeline), who get a "check before deploy" step with one line of promotion.yml. Single-org users are unaffected.
  • What changes:
    • promotion.yml accepts orgs.<slug>.check: <name> (a slug), naming a vapi-checks.yml check.
    • New src/promotion-gate.ts:
      • Validation before any transition: the check must exist, and its org and runOrg must be the gated org, otherwise the run errors. vapi-checks.yml is required once any org is gated.
      • Plan line: what the gate would run, built offline.
      • Live gate: the check runs live and reduces to the worst target result.
    • src/promote-cmd.ts: in each transition, after the plan is built:
      • no changes skips the gate;
      • plan-only prints check would run <name> in <org> (<n> simulations × <t> targets);
      • --apply runs the check (after the bindings refresh, before promotionPlanApply writes anything). Any non-pass throws Promotion out of <org> blocked: check <name> <outcome> (<run url>).
      • A pass is cached per source org and dropped once a transition applies into that org.
      • promotionCommandRun(args, overrides) now takes Partial<PromotionDeps> (childRun, checkRun).
    • .github/workflows/promotion.yml: timeout-minutes: 90 on the "Reconcile configured promotions" step, not the job, so the if: always() commit step (fixed in fix(promotion): commit the files of transitions that applied when a later one fails #65) still runs after a blocked or slow gate.
    • Docs: promotion.example.yml (a commented check:), a README "Check before promoting" section, and a pointer from "PR Checks".

Evidence of value

The real gate, run live in the owner's test org on the TEST-141 parity squad.

  • Setup: a scratch repo whose promotion.yml gates parity on check core, with pipeline parity → parity-prod.
  • The run: promote --pipeline release --from parity --to parity-prod --apply.
  • The fake: the child runner was faked, so bindings pulls were no-ops and the downstream apply.ts was recorded but not run. No second org was needed or touched.
Variant Gate run Result Downstream apply resources/parity-prod/
Degraded scheduler prompt 7ed19587: 2 of 3 failed Promotion out of parity blocked: check core failed (https://dashboard.vapi.ai/simulations/run/7ed19587-…) none empty (nothing written)
Fixture as-is 95470670: 3 of 3 passed promoted ["parity-prod"] written; 20 applied paths recorded

The test org's resource counts were identical before and after both gate runs.

Tests: npm test goes from 484 (#65) to 492 passing, and #68's golden promotion test passes unchanged.

Testing plan

  • tests/promotion-gate.test.ts (6 tests, real git fixture, injected childRun / checkRun):
    • a pass applies;
    • failed and incomplete both block with the exact message, with no apply and the target untouched;
    • plan-only prints the line and runs nothing;
    • no changes skips the gate;
    • the three config errors (no vapi-checks.yml, unknown check, check in another org) stop before anything applies;
    • the pass cache: reused for two pipelines out of one org, and re-run after a transition applies into the gated org.
  • No gate configured, no change: with no check: in promotion.yml and an invalid vapi-checks.yml present, plan and --apply both succeed, checkRun is never called, and the plan output equals a pinned string. That string is exactly what fix(promotion): commit the files of transitions that applied when a later one fails #65's code (before the gate existed) prints for the same fixture, which I confirmed by running fix(promotion): commit the files of transitions that applied when a later one fails #65's promote-cmd on it. So the gate is invisible unless someone opts in.
  • tests/promotion.test.ts: orgs.<slug>.check is parsed, and a non-slug is rejected.
  • Not tested:
    • A real two-org promotion: only one test org was available. The downstream apply was faked, so the blocked case shows nothing written, and the pass case shows the apply was called.
    • A GitHub Actions promotion run with a gate, including the step timeout firing.
  • Found while testing (pre-existing, out of scope): promotion's dependency check rejects simulations that reference a stock personality by UUID, with "Referenced managed dependency is missing from source: personalities/a0000000-…". So a gated org's tests need local personality files until that's fixed.

Stacked on #65.

Refs TEST-141

🤖 Generated with Claude Code

scott-lowe-vapi commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Oct 3, 5:59 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Oct 3, 6:14 AM UTC: Graphite couldn't merge this PR because it had merge conflicts.

@scott-lowe-vapi
scott-lowe-vapi changed the base branch from feat/vapi-checks-workflow to graphite-base/66 October 3, 2026 06:12
@scott-lowe-vapi
scott-lowe-vapi changed the base branch from graphite-base/66 to main October 3, 2026 06:13
@scott-lowe-vapi
scott-lowe-vapi changed the base branch from main to graphite-base/66 October 3, 2026 06:18
@scott-lowe-vapi
scott-lowe-vapi force-pushed the feat/promotion-check-gate branch from 849c59f to 95379be Compare October 3, 2026 06:18
@scott-lowe-vapi
scott-lowe-vapi changed the base branch from graphite-base/66 to fix/promotion-commit-applied-transitions October 3, 2026 06:18
`orgs.<slug>.check: <name>` in promotion.yml names a vapi-checks.yml
check that must pass in that org before any transition promotes out of
it. The gate runs the same inline check as the PR workflow, built from
the source org's files at the promoted commit, using that org's key from
VAPI_PROMOTION_TOKENS.

- Checks are validated before any transition: the named check must exist
  and read and run in the gated org.
- Transitions with no changes skip the gate; plan-only runs print
  `check  would run <name> in <org> (<n> simulations × <t> targets)`
  and run nothing.
- On --apply the check runs after bindings refresh and before
  promotionPlanApply writes the target. Any non-pass (failed,
  incomplete, build error) throws `Promotion out of <org> blocked: check
  <name> <outcome> (<run url>)`, so the target is untouched and earlier
  transitions are still committed (previous change).
- A pass is reused for later transitions out of the same org in the same
  run, and dropped once a transition applies into that org.
- The "Reconcile configured promotions" step gets timeout-minutes: 90 on
  the step, not the job, so the always() commit step still runs.
- With no check: configured, promotion is unchanged: a test pins the
  plan output to what the pre-gate code prints, and asserts no check runs
  and vapi-checks.yml is never read.
- Docs: promotion.example.yml and README ("Check before promoting", and a
  pointer from "PR Checks").

Refs TEST-141

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.

3 participants