Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f95f650
docs(specs): changed-file mutation PR gate design (brainstorm 2026-08…
nzneit Aug 5, 2026
0404088
docs(specs): fold adversarial-review findings into the mutation PR ga…
nzneit Aug 5, 2026
92e11ce
docs(specs): pin sibling existence-at-HEAD so removed modules aren't …
nzneit Aug 5, 2026
3bf398a
docs(plans): implementation plan for the changed-file mutation PR gate
nzneit Aug 5, 2026
08e8d6c
feat: mutation-gate diff parsing and line counting
nzneit Aug 5, 2026
b75518a
feat: mutation-gate glob subset matcher with loud refusal
nzneit Aug 5, 2026
70079d6
feat: mutation-gate sibling rule and mutate-set selection
nzneit Aug 5, 2026
31d1320
feat: mutation-gate env config and size decision
nzneit Aug 5, 2026
a61d711
feat: mutation-gate report interpretation over the full status enum
nzneit Aug 5, 2026
090c694
feat: mutation-gate summaries and GitHub output formatting
nzneit Aug 5, 2026
6bed101
feat: mutation-gate main orchestration, real deps, CLI entry
nzneit Aug 5, 2026
343cdf5
test: pin mutation-gate incremental mode
nzneit Aug 5, 2026
e71f840
ci: mutation PR gate workflow with label overrides and sticky comment
nzneit Aug 5, 2026
45cbe52
rehearsal: plant an untested probe (never merge)
nzneit Aug 5, 2026
7662e2a
rehearsal: point tsconfigFile at a nonexistent file to sidestep a str…
nzneit Aug 5, 2026
1e13ef5
rehearsal: raise bun.timeout to 120s so the dry run's full-suite pass…
nzneit Aug 5, 2026
59c3380
rehearsal: force skip-size
nzneit Aug 5, 2026
ec21de2
Revert "rehearsal: force skip-size"
nzneit Aug 5, 2026
a273fbe
rehearsal: kill the probe
nzneit Aug 5, 2026
ccde339
rehearsal: artifact always-on, touch index.ts into the mutate set (co…
nzneit Aug 5, 2026
d472077
rehearsal: concurrency 4 measurement
nzneit Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Portable mutation PR gate (StrykerJS). To adopt in another repo:
# 1. copy scripts/mutation-gate.mjs and this file;
# 2. replace the toolchain setup below (setup-bun + bun install) with your own
# (e.g. drop setup-bun, use actions/setup-node + `npm ci`);
# 3. tune the MUTATION_GATE_* env knobs (full table in the script header);
# 4. add the `mutation` check to branch protection; create the
# mutate-force / mutate-skip labels.
# Modes: changed (default: mutates the PR's changed files) or incremental
# (MUTATION_GATE_MODE: incremental; needs a baseline, see the commented-out
# job at the bottom, plus an actions/cache restore step in this job).
name: mutation
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled, unlabeled]
concurrency:
group: mutation-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write # sticky comment
jobs:
mutation:
runs-on: ubuntu-latest
timeout-minutes: 15 # backstop against a mispredicted run
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }} # the PR as authored, not the synthetic merge ref
fetch-depth: 0 # all branches: merge-base needs the real base branch
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14" # same pin rationale as gates (bunfig coverage semantics)
- uses: actions/setup-node@v5
with:
node-version: "24" # Stryker CLI host (D-010); >=20 required, 20 is EOL
- run: bun install --frozen-lockfile
- name: gate
id: gate
env:
MUTATION_GATE_BASE: origin/${{ github.event.pull_request.base.ref }} # the branch, never the payload SHA
MUTATION_GATE_FORCE: ${{ contains(github.event.pull_request.labels.*.name, 'mutate-force') && '1' || '' }}
MUTATION_GATE_SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'mutate-skip') && '1' || '' }}
MUTATION_GATE_EXTRA_ARGS: "--concurrency 4" # rehearsal: concurrency-4 measurement
run: node scripts/mutation-gate.mjs
- name: sticky comment
if: always() && steps.gate.outputs.decision != ''
continue-on-error: true # fork PRs get a read-only token; the verdict is the gate step's alone
env:
GH_TOKEN: ${{ github.token }}
DECISION: ${{ steps.gate.outputs.decision }}
SUMMARY: ${{ steps.gate.outputs.summary }}
PR: ${{ github.event.pull_request.number }}
run: |
BODY_FILE="$(mktemp)"
printf '<!-- mutation-gate -->\n\n%s\n' "$SUMMARY" > "$BODY_FILE"
EXISTING="$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate \
--jq '[.[] | select(.body | startswith("<!-- mutation-gate -->"))][0].id // empty')"
case "$DECISION" in
pass|pass-empty) [ -z "$EXISTING" ] && exit 0 ;; # quiet pass: only update an existing comment
esac
if [ -n "$EXISTING" ]; then
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING}" -F body=@"$BODY_FILE"
else
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" -F body=@"$BODY_FILE"
fi
- name: report artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: mutation-report
path: reports/mutation/
retention-days: 14
if-no-files-found: ignore
# baseline: # incremental-mode adopters: produce/refresh the baseline on pushes to main
# # (also add `push: { branches: [main] }` to `on:` and an `if: github.event_name == 'push'` guard)
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v7
# - <your toolchain setup + dependency install>
# - uses: actions/cache@v4
# with:
# path: reports/stryker-incremental.json
# key: stryker-incremental-${{ github.sha }}
# restore-keys: stryker-incremental-
# - run: node_modules/.bin/stryker run --incremental --incrementalFile reports/stryker-incremental.json
Loading
Loading