Repository navigation
ci(repo): run the release gate on pull requests, scoped to the change - #78
Merged
Merged
Conversation
Capability PRs never ran the mutation gate, so #77 merged src/graph with 45 live mutants (31 Survived, 14 NoCoverage) that main's gate then caught. The release gate now also runs on pull_request. The plan job lists the files the pull request changes (the merge commit against its first parent), and stryker-plan-gate.ts decides the scope: the declared files the change touches; a package's whole declared set when the change touches any other file in that package (a test, a fixture, a config, a source file outside the set); every package's whole set when it changes the gate (this workflow, the gate script, stryker.shared.ts, the lockfile, the Nix toolchain). The scope reaches Stryker as MUTATION_SCOPE, which stryker.shared.ts intersects with each package's declared mutate list; unset (local, main without a diff), the whole set is mutated. The gate refuses an in-scope package the plan scheduled nothing for, as before, and passes a change that touches no mutated package with no plan. The verdict job now runs whenever the workflow was not cancelled and fails unless the plan, and every shard when there are shards, finished, so a skipped mutation job can no longer read as a pass. Pull-request runs skip the incremental cache so every scoped mutant runs fresh, and a newer push cancels the older run.
systemfsoftware-maker
added this pull request to stack #80
October 9, 2026 12:30
When a shard child fails, the shard runner prints only the first 1024 characters of the child's stderr, and those are startup INFO lines. The child's error envelope goes to its stdout, which the runner discards. #79's gate runs 37930507588 and 37933520982 both failed with exit 3 and left nothing that names the cause. Every package's Stryker config now writes stryker.log at info level. When the mutation job fails, a follow-up step re-runs each of the shard's project runs directly, with the same arguments the shard runner passes, keeping stdout, stderr and the exit code under reports/diagnostics/. The job then uploads that directory and every stryker.log as mutation-diagnostics-shard-N. A green job runs neither step.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Capability PRs never ran the mutation gate. #77 merged
src/graphwith 45 live mutants (31 Survived, 14 NoCoverage), and main's release gate (run 37922084024 on 3742392) found them only after the merge. This PR runs the same release gate on every pull request, scoped to what the pull request changes, so a capability PR's own run is the proof.What the pull-request run mutates
The plan job lists the files the pull request changes:
git diff --name-only HEAD^1 HEADon the merge commit, which is the change the merge would make to its base.scripts/stryker-plan-gate.tsturns that list into a scope (scopeOf):release-gate.yml,.github/actions/**,scripts/stryker-plan-gate.ts,scripts/deno.json/deno.lock,stryker.shared.ts, rootpackage.json,pnpm-lock.yaml,pnpm-workspace.yaml,flake.nix/flake.lockmutatelistThe scope rule does not sort files into tests and modules by name (CONST-T12). Any change to a package file outside the declared set counts as a possible test of the set. The rule can therefore mutate more than a change needs, but never less. A source file outside the set may be one the mutated files import, and changing it can change their mutants' outcomes. That is also why such a change mutates the whole set.
The scope reaches Stryker as
MUTATION_SCOPE, a comma list of project directories (whole set) and repo-relative files.stryker.shared.tsintersects it with each package's declared list. It resolves the package from the working directory, which is where Stryker loads the config and resolvesmutatefor bothstryker planand each shard child. When the variable is unset (a local run, or a push or dispatch run with no diff), the config keeps the whole declared set. Onmainthe scope is every mutating project, somain's run is unchanged.Gate and verdict
gatePlantakes the changed list. It still refuses an in-scope package that the plan scheduled nothing for, and a malformedstryker.config.ts(nomutatelist) is a new refusal. A change that touches no mutating package passes with no plan and logs0 mutants for <pkg>: the change touches none of its files. A push run with an empty plan and no ledger entry is still refused as vacuous.needs: [plan, mutation]and runsif: !cancelled(). Its first step fails unless the plan job succeeded and, when there are shards, every shard succeeded. Before this change, a failed plan skippedmutationand so skippedverdict, and a skipped job reports as success. With no shards, the job passes and says that no mutated file is in scope.pull_requesthas no branch filter, so a stacked layer whose base is another branch is gated too.This PR's own run
This PR changes the gate, so its run mutates the whole xstate set. The run must report exactly main's 45 graph survivors and nothing else. That proves the PR-scoped gate catches what main's gate caught. The PR stacked on top, #79, kills the survivors. Its gate run 37941873978 on 39a4780 (A plus B) passed: 0 Survived and 0 NoCoverage (Killed 203, Timeout 10, CompileError 416, Ignored 2). This PR's own run 37935773972 on 42a4aed failed on exactly main's 45 src/graph mutants and on nothing else.
Not changed
verdict · merged report) runs and reports on every pull request, and the conductor enforces it at merge time.mutatelist, no disable comments.Gates (local, on 23cfd00)
deno test scripts/stryker-plan-gate.test.ts: 15 passed. That is the 8 existing tests plus 7 for the scope rule and the scoped gate.pnpm test:sandbox: 8 passed (10 steps), including the 5 release-gate sandbox proofs that read this workflow.deno check,deno lintandtsc -p tsconfig.node.json: exit 0.dprint checkis clean.packages/xstate/stryker.config.tsunder Node:mutatesrc/graph/adjacency.ts,src/graph/graph.ts, a changesetsrc/graph/adjacency.ts+tests/graph.conformance.test.tspackages/xstatesrc/stateUtils.tspackages/xstatedocs/notes.mdrelease-gate.ymlpackages/xstatepackages/xstate