Skip to content

ci(release): deploy only after CI and a real mutation run on the same commit - #77

Merged
ryanleecode merged 3 commits into
mainfrom
gates/u2-deploy-waits
Oct 8, 2026
Merged

ryanleecode merged 3 commits into
mainfrom
gates/u2-deploy-waits

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Unit U2 of docs/plans/2026-10-08-0705-feat-gates-bind-and-inline-suppression-plan.md (C1, R23). Conductor rulings 4 and 6.

Change

  • .github/workflows/ci.yml adds on: workflow_call, so the release gate can run CI on its own commit. pull_request and workflow_dispatch are unchanged; release.yml still dispatches ci.yml on the release PR branch.
  • Pending (staged, not yet pushed; conductor ruling 6): ci.yml drops its push: branches: [main] trigger. The release gate already runs ci.yml on every push to main, so the standalone trigger ran the full suite twice per merge. After this change, main's CI runs once, inside the release gate. It lands in the next push, together with a merge of main.
  • .github/workflows/release-gate.yml:
    • adds ci: uses: ./.github/workflows/ci.yml;
    • changes deploy to needs: [ci, plan, mutation]. Its if keeps only the template guard, so the implicit success() requires all three to succeed. The needs.mutation.result == 'skipped' arm is gone;
    • removes mutation's if: packages != '[]', which the planner can no longer produce on success.
  • scripts/mutation-shards.ts: a workspace with no *.workflow.ts is now a refusal (exit 1) instead of an empty plan plus a ::notice.
  • scripts/mutation-shards.test.ts: the zero-decision case now expects that refusal. The expected value comes from R23 and CONST-T3, not from the planner's output.

Judgment surfaces changed (GATE1, conductor Q7): ci.yml, release-gate.yml, scripts/mutation-shards.ts.

Predicate

  • AE9: given a commit that deletes every *.workflow.ts, the mutation plan refuses and no production deploy runs.
  • AE6: given a commit whose journeys fails while mutation passes, no deploy runs. Deploy needs ci, which contains journeys, to succeed.

Must not count

  • A deploy gated on a different commit's CI run. The called workflow's github context is the caller's, so it runs on the same SHA.
  • A deploy that treats a skipped CI or mutation job as success.
  • Mutation run locally. It wasn't.

Concurrency groups (KTD1), with the pending ci.yml change

A called workflow evaluates its own workflow-level concurrency in the caller's context, so ${{ github.workflow }} there is the caller's name (community discussion 30708, answer confirmed by GitHub support). pnpm-release-management's reusable release.yml declares no concurrency. No workflow here has a merge_group trigger.

Event Run Declared in Group cancel-in-progress
push to main Release gate release-gate.yml release-gate-refs/heads/main false
push to main CI called by the gate (ci job) ci.yml, caller's context Release gate-refs/heads/main true
push to main Release release.yml release-refs/heads/main false
push to main standalone CI none: the push trigger is removed — —
push to main (release step) CI dispatched on the release PR branch ci.yml CI-refs/heads/changeset-release/main true
pull request #N CI ci.yml CI-refs/pull/N/merge true
manual dispatch of ci.yml on ref R CI ci.yml CI-R true

Why none can cancel another:

  • Concurrency only acts inside one group. The groups above differ in their literal text, not just in case: release-gate-…, Release gate-…, release-… and CI-… share no string. So a run in one row can never cancel or queue behind a run in another.
  • A PR cannot cancel the gate's CI. A PR's CI is in CI-refs/pull/N/merge. Both the prefix (CI vs Release gate) and the ref (refs/pull/N/merge vs refs/heads/main) differ from the gate's CI group. The gate has no pull_request trigger. A manual dispatch of ci.yml on main lands in CI-refs/heads/main, which is distinct as well.
  • A later push to main cannot cancel the gate's CI either. The later gate run waits in release-gate-refs/heads/main (cancel-in-progress: false) until the running one finishes. It never starts, so its ci call never reserves Release gate-refs/heads/main while the earlier gate's CI holds it.
  • The called CI does not collide with its own caller. The called ci.yml resolves to Release gate-… and its caller holds release-gate-…. Collision there is the failure mode in discussion 30708, where the called workflow cancels its caller. Here the strings differ.
  • Residual behaviour, safe by construction. GitHub keeps at most one pending run per group. If three pushes A, B, C land while A's gate runs, C's gate replaces B's pending gate. B is then never deployed, and C deploys after its own CI and mutation pass. That outcome is a skipped deploy, never a deploy without passing gates. If a later rename made the called CI's group equal the gate's group, the gate's ci would be cancelled and deploy would refuse rather than ship (plan, Risks).

Evidence

  • pnpm test:scripts, locally: ok | 6 passed | 0 failed.
  • pnpm format:check, locally: clean.
  • AE9, on throwaway branch spike/u2-ae9 (384b2a1, deleted afterwards). That branch is this PR's head with every *.workflow.ts deleted, plus the gate's push trigger extended to the branch. Run 37742540342:
    • The plan · mutation shards job failed with mutation-shards: no workspace package has a *.workflow.ts file; the release gate refuses an empty set and Process completed with exit code 1.
    • The mutation job was skipped, so no mutation ran.
    • deploy · production was skipped. A caveat: this repo is a template, so its !is_template guard would also skip deploy. The refused plan is what blocks deploy in a copy, because the implicit success() requires every needs job to succeed. This run alone can't separate those two causes.
    • The ci / … legs also ran in the same run on the same commit. They failed there because the deleted decisions break the tests and build.
  • PR CI on cfe16b4: run 37742556709, success. The pending ci.yml change gets its own PR CI run when it is pushed.

… commit

The release gate deployed when the plan and mutation jobs passed, without
waiting for CI or journeys, and accepted a skipped mutation job: a main
with no *.workflow.ts planned an empty set and deployed unmutated (R23).
The gate now calls ci.yml as a reusable workflow on the same commit, and
deploy needs ci, plan and mutation to succeed. The planner refuses a
workspace with no *.workflow.ts instead of returning an empty plan.

Judgment surfaces changed: .github/workflows/ci.yml,
.github/workflows/release-gate.yml, scripts/mutation-shards.ts (GATE1,
conductor Q7)

Conductor ruling 4
@systemfsoftware-maker systemfsoftware-maker changed the title gates/u2 deploy waits ci(release): deploy only after CI and a real mutation run on the same commit Oct 8, 2026
ci.yml no longer triggers on push to main. The Release gate calls ci.yml
as a reusable workflow on every push to main, so the standalone push
trigger ran the full suite twice per merge. pull_request,
workflow_dispatch (release.yml dispatches it on the release PR branch)
and workflow_call remain, per conductor ruling 6
@ryanleecode
ryanleecode merged commit db171aa into main Oct 8, 2026
11 checks passed
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.

2 participants