Skip to content

build(repo): mutate only each package's *.workflow.ts files at the release gate - #86

Merged
ryanleecode merged 2 commits into
mainfrom
build/mutate-only-workflows
Oct 9, 2026
Merged

ryanleecode merged 2 commits into
mainfrom
build/mutate-only-workflows

Conversation

@systemfsoftware-maker

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

Copy link
Copy Markdown
Collaborator

Until now the release gate mutated whatever set each package chose for itself. A package can no longer widen or narrow that set: Stryker mutates exactly each package's *.workflow.ts decision files, and any other way of setting the scope is refused before Stryker starts. Plan: docs/plans/2026-10-09-1040-build-mutate-only-workflows-plan.md (ruling A-8, A-8b, A-8c). It supersedes the 1002 plan from the first commit.

What changes

  • One glob. stryker.shared.ts exports WORKFLOW_FILES = '**/*.workflow.ts'. Its StrykerConfig import is type-only, so the plan job, which has no node_modules, can load it through Deno.
  • Planner (scripts/mutation-shards.ts). For each package it lists that package's workflow files and outputs them. The walk skips the shared config's ignorePatterns (reports, coverage, dist) plus node_modules and .stryker-tmp; there is no second list. The plan output is as shards=[{"package","mutate":[...]}]. It refuses, by package directory and with a tagged refusal:
    • OwnMutate: the package's package.json sets stryker.mutate.
    • MutationScriptNotStrykerRun: the mutation script is not exactly stryker run. That closes the route where a script passes its own -m.
    • NoWorkflowFiles: the package mutates but has no workflow files.
    • WorkflowFilesNotMutated: the package has workflow files but no package name or mutation script.
    • UnsafeWorkflowPath: a workflow path contains a comma or a glob metacharacter (*?[]{}()!\), which the comma-joined --mutate list would split or expand.
    • NoWorkflowFilesInWorkspace: the workspace has no workflow files, so the set would be empty.
  • Gate (release-gate.yml). The matrix runs over shard. PACKAGE and MUTATE (join(matrix.shard.mutate, ',')) both go through env. The gate runs in two steps:
    1. Mutation dependencies: runs the tasks that turbo run mutation --filter="$PACKAGE" --dry=json lists other than mutation itself. Today that is @endgame/site#generate.
    2. Mutation: runs turbo run mutation --filter="$PACKAGE" --only -- --mutate "$MUTATE". The fork merges the CLI record over the config file (mergeConfig(file, cli), vendored stryker-js 17.0.2 dist/main.mjs:84525), so the list replaces any mutate a config sets.
  • apps/site no longer has a stryker field. Its config is export default packageStrykerConfig.
  • turbo.json: stryker.shared.ts is added to the //#test:scripts inputs, because the planner tests now import it.
  • README: the mutation FAQ states the rule once, and says the planner refuses anything that would change the set.

Evidence

  • Turbo passthrough (A-8c). turbo run mutation --filter=@endgame/site --dry=json -- --mutate x on turbo 2.11.7 gives the args to the dependency too:

    {"taskId":"@endgame/site#generate","command":"tsr generate","cliArguments":["--mutate","x"],"dependencies":[]}
    {"taskId":"@endgame/site#mutation","command":"stryker run","cliArguments":["--mutate","x"],"dependencies":["@endgame/site#generate"]}
    

    With the fix, the dependency step's dry run gives {"taskId":"@endgame/site#generate","cliArguments":[]}. The mutation step's dry run (--only ... -- --mutate src/api/check-health.workflow.ts) gives:

    {"taskId":"@endgame/site#mutation","cliArguments":["--mutate","src/api/check-health.workflow.ts"],"dependencies":[]}
    

    I also ran the dependency step for real: it derived @endgame/site#generate, ran it, and exited 0. actionlint on the workflow exits 0. Stryker never started.

  • A1 probe: the fork's real mergeConfig, under Node, given a config widened to src/**/*.ts and a CLI list of the two workflow files, returned exactly those two files. The probe file was deleted afterwards.

  • A3 run: the planner run on the real tree with no node_modules printed @endgame/site with src/api/check-health.workflow.ts, src/features/guestbook/moderate-guestbook-entry.workflow.ts and src/features/guestbook/sign-guestbook.workflow.ts, and exited 0.

  • pnpm test:scripts: 14 passed, 0 failed. Of these, 11 are planner tests over real temporary workspaces, compared against hand-written plans.

  • Sabotage (CONST-T10). I made these edits to mutation-shards.ts:

    • removed the OwnMutate refusal;
    • removed the WorkflowFilesNotMutated refusal;
    • removed the NoWorkflowFiles refusal;
    • made the script check script.length < 0, which never fires;
    • widened the walk to **/*.ts with no excludes.

    Result: FAILED | 3 passed | 9 failed. All 9 planner tests went red; the 3 that passed are check-branch-rules tests. The file was restored byte-identical. Log: .cache/sabotage.log (not committed).

  • Sabotage for A-8c. I dropped the shared ignorePatterns from the walk and made the unsafe-path check match nothing. Result: FAILED | 12 passed | 2 failed; the two that failed are the dist-copy test and the unsafe-path test. The file was restored byte-identical. Log: .cache/sabotage-a8c.log (not committed).

  • pnpm check:ci: exit 0 on 76394c9; it was also exit 0 on 3db7628. A fresh worktree first needs pnpm run prepare, because the effecttsgo oxlint plugin exists only after that step.

  • Stryker was not run locally. The release gate runs it on main.

Judgment surfaces (CONST-W3)

scripts/mutation-shards.ts, scripts/mutation-shards.test.ts, .github/workflows/release-gate.yml, stryker.shared.ts, apps/site/stryker.config.ts and the stryker field in apps/site/package.json, and the //#test:scripts inputs in turbo.json. The owner directed these changes (ruling A-8, A-8b).

…lease gate

The planner lists every package's workflow files from one glob in
stryker.shared.ts, and the gate hands that list to Stryker with --mutate,
which replaces whatever mutate a package config sets. The planner refuses,
by package, its own stryker.mutate, a mutation script other than exactly
'stryker run', a mutating package without workflow files, workflow files
no mutation script covers, and a workspace with none (ruling A-8, A-8b).

Judgment surfaces (CONST-W3), owner-directed: scripts/mutation-shards.ts,
release-gate.yml, stryker.shared.ts, apps/site stryker config and field,
turbo.json test:scripts inputs.
@systemfsoftware-maker systemfsoftware-maker changed the title build/mutate only workflows build(repo): mutate only each package's *.workflow.ts files at the release gate Oct 9, 2026
The planner walk skips the shared config's ignorePatterns plus node_modules
and .stryker-tmp, and refuses by name a workflow path with a comma or glob
metacharacter, which the comma-joined --mutate list would split or expand.
turbo 2.11.7 forwarded '-- --mutate' to the generate dependency, so the gate
now runs mutation's dependencies from turbo's dry-run graph first and then
mutation alone with --only; the package name goes through env. The plan is
superseded by a new dated plan carrying the corrections.

@ryanleecode ryanleecode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified: 10/10 required checks green on 76394c9, up to date with main d3e2f1b, sabotage red, A-8c findings applied (scan scope, unsafe paths, turbo passthrough fixed with --only after a dry-run proof), hunt grep clean.

@ryanleecode
ryanleecode merged commit 375e468 into main Oct 9, 2026
12 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