Skip to content

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

Open
scott-lowe-vapi wants to merge 3 commits into
feat/vapi-checks-workflowfrom
feat/promotion-check-gate
Open

scott-lowe-vapi wants to merge 3 commits into
feat/vapi-checks-workflowfrom
feat/promotion-check-gate

Conversation

@scott-lowe-vapi

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.

Review only the last commit (849c59f). The plan cuts this from main after #63 and #65 merge, and neither has yet. So it's stacked on #64 with #65 merged in (f3f3209 resolves #65 against #59's promote-cmd.ts refactor). Once both land, rebase onto main and retarget.

  • 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 477 (the merge with #65) to 484 passing.

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.
  • 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 #64, with #65 merged in.

Refs TEST-141

🤖 Generated with Claude Code

scott-lowe-vapi and others added 3 commits October 1, 2026 16:38
…ater one fails

When a promotion failed partway, the workflow's commit step staged only
the UUID state, but the failing transition had already rewritten tracked
files in its target org, so `git pull --rebase` refused and nothing was
pushed — not the state, and not the files of transitions that had
already reached the platform.

- promote-cmd truncates tmp/promotion-applied.txt at the start of each
  --apply run and, after each successful apply.ts, appends the paths git
  reports changed under resources/<target>/ (from git, not the plan:
  apply's own pull and push can rewrite other files).
- On a non-success outcome, the commit step adds the state files plus
  exactly those paths, commits, then resets and cleans resources/ so the
  failed transition's rewrites can't block the rebase.
- promotionCommandRun(args, deps) takes an injectable child runner, used
  by the new tests/promote-cmd.test.ts.

Refs TEST-141

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eck-gate

The promotion check gate needs #65's partial-failure commit step and its
promote-cmd test seam. Resolved against the config-free org-connection
refactor: the deps seam wraps orgScriptRun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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.
- 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 <noreply@anthropic.com>
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