Get started
Mergestro Gate mutates the lines a pull request changed and reruns your tests. A mutation nothing catches is a line your tests don't really check. This page adds it to a GitHub workflow, reads what it reports, and makes it a required check.
Add it to a pull-request workflow
One file. It needs the full history (the gate diffs against the pull request's base) and a Rust toolchain, because every mutant is compiled and tested. Start it advisory: it reports and comments, and never fails the check.
.github/workflows/mergestro-gate.yml
on: pull_request
permissions: { contents: read, pull-requests: write }
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: lucheeseng827/mergestro-gate@v0.6.1
with:
advisory: "true" # report, never block — start here
The Action installs cargo-mutants and a prebuilt, checksummed
slop-gate binary, works out the merge-base, and runs the gate on your own
runner. There is no account, no API key and no service behind it.
Pin the release tag (@v0.6.1), not @main. A gate that can
block merges should change only when you change it.
Read what it reports
The job log opens with a summary. This is the gate on a small crate whose tests catch every mutation of the changed line (an excerpt of a real 0.6.1 run):
preflight: green & stable across 1 run(s)
mutants: 5 candidate(s), 5 tested, 0 capped out
outcomes: 5 caught · 0 timed out · 0 unviable · 0 SURVIVED
verdict: PASS
- preflight — your suite ran once, green, before any mutant. A red or flaky suite stops here: survivors would be indistinguishable from flakiness.
- mutants — how many mutations of the changed lines were generated and
tested.
max-per-functioncaps them per function; capped out counts the ones it skipped. - outcomes — caught means a test failed on the mutant, which is what you want. Unviable mutants did not compile and count for nothing. SURVIVED means the suite passed over a real change to your code.
Each survivor is listed with its file, line, severity and the exact mutation — for
example replace is_test_scaffold -> bool with false: the function could
always return false and no test would notice. The same list goes into the
pull-request comment, which is updated in place on every push and says what is new,
still open and resolved since the last run.
It also flags tests that contain no assertion at all, and — as advisory lanes — slop, security, convention and weakened-test patterns in the diff.
Exit codes
| Code | Meaning |
|---|---|
0 | Passed, or ran advisory. |
2 | Blocked: survivors over max-survivors, or another gate you turned on tripped. This is what fails a required check. |
1 | Operational: no merge-base, cargo-mutants missing, the engine died. Never a verdict about your code. |
Make it a required check
Watch a dozen pull requests first. When the survivors it reports are ones you would have wanted to know about, turn blocking on:
- uses: lucheeseng827/mergestro-gate@v0.6.1
with:
max-survivors: "0" # any survivor blocks
block-on-zero-assertion: "true" # so does a test that asserts nothing
budget: "10m" # stay inside your CI time limit
Then add the job to the branch-protection rule as a required status check. To start
softer, max-survivors: "3" tolerates a few survivors while a codebase catches
up; add block-on-severity: high so a high or critical one still blocks
whatever the count.
Before turning it on for a large change, slop-gate estimate predicts how
many mutants a diff produces without building anything — see
Run it anywhere.
Next: Run it anywhere →