From 311f6f2ae5bbb8d4552cb229b1cfcc8cf57a4a71 Mon Sep 17 00:00:00 2001
From: "rearden-grok[bot]"
<317016512+rearden-grok[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 19:07:01 -0700
Subject: [PATCH] ci: shard PR cargo-mutants next to coverage
A ~300-mutant in-diff at -j 2 needs about 50 minutes, and the single
mutants-pr job was canceled at 30. Four shards start after fmt, clippy,
and test, in parallel with coverage, and stay advisory. Weekly sweep
is unchanged.
---
.github/workflows/ci.yml | 103 +++++++++++++++++++++++++++++++++-
.github/workflows/mutants.yml | 31 +---------
CHANGELOG.md | 18 ++++--
TESTING.md | 2 +-
4 files changed, 115 insertions(+), 39 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 543d3d950..e607f1a7d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,14 +24,15 @@ env:
# every GitHub Actions “stable” bump. Bump intentionally with the toolchain.
#
# Each gate is its own check. Short ubuntu gates share one runner so
-# concurrent-job slots stay free for clippy, test, and future cargo-mutants
-# shards (mutants.yml):
+# concurrent-job slots stay free for clippy, test, coverage, and the PR
+# mutants shards:
# fmt → deny → ast-grep → nixos-module-eval
# Later gates use !cancelled() so a red earlier gate still reports its own
# check (a skip would hide required deny / ast-grep).
# clippy, test, windows, and macos start immediately. clippy stays parallel:
# coverage waits on it, and a cold rust-cache is a full workspace compile.
-# coverage waits for fmt / clippy / test.
+# coverage and the advisory mutants shards wait for fmt / clippy / test.
+# Weekly full mutants sweep stays mutants.yml.
# nixos-module-runtime is a labeled job (nixos-module-runtime.yml), not this file.
# Operator binaries are GitHub Releases (release.yml), not this workflow.
#
@@ -204,6 +205,102 @@ jobs:
- name: store + node smoke
run: ./scripts/ci-os-smoke.sh
+ # Crates, Cargo manifest/lock, or these workflow files. Docs-only PRs
+ # skip the four shard runners. Starts immediately; shards still wait for test.
+ mutants-diff:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ rust: ${{ steps.d.outputs.rust }}
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ - name: rust or workflow diff
+ id: d
+ env:
+ BASE_REF: ${{ github.base_ref }}
+ run: |
+ set -euo pipefail
+ git diff "origin/${BASE_REF}".. --unified=0 -- \
+ crates Cargo.toml Cargo.lock \
+ .github/workflows/ci.yml .github/workflows/mutants.yml \
+ > git.diff
+ if [[ -s git.diff ]]; then
+ echo "rust=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "rust=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ # PR in-diff, four shards, same window as coverage. Advisory: a missed
+ # mutant does not fail the job. One ~300-mutant diff at -j 2 is ~50m and
+ # was canceled at 30m. Weekly sweep stays mutants.yml. Owner: TESTING.md.
+ mutants:
+ name: mutants (${{ matrix.shard }}/4)
+ needs: [fmt, clippy, test, mutants-diff]
+ if: >-
+ github.event_name == 'pull_request'
+ && needs.fmt.result == 'success'
+ && needs.clippy.result == 'success'
+ && needs.test.result == 'success'
+ && needs.mutants-diff.result == 'success'
+ && needs.mutants-diff.outputs.rust == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ shard: [0, 1, 2, 3]
+ env:
+ # Workflow env disables incremental for rust-cache and denies warnings.
+ # cargo-mutants rebuilds one mutant at a time; incremental is the point.
+ # Weekly mutants.yml does not pass -D warnings. A warning-only build
+ # failure would look caught here and missed on Sunday.
+ CARGO_INCREMENTAL: "1"
+ CARGO_TERM_COLOR: never
+ RUSTFLAGS: ""
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 0
+ - uses: dtolnay/rust-toolchain@a5f673d0ba8626c3977bb416a1612774bc82181b # rustc 1.95.0
+ - uses: taiki-e/install-action@9534c84618278caac52cb373bb164ed464dbd8af # v2.87.11
+ with:
+ tool: cargo-mutants@27.1.0
+ - name: in-diff shard
+ id: run
+ continue-on-error: true
+ env:
+ BASE_REF: ${{ github.base_ref }}
+ run: |
+ set -euo pipefail
+ git diff "origin/${BASE_REF}".. --unified=0 > git.diff
+ set +e
+ cargo mutants --workspace --in-diff git.diff \
+ --shard "${{ matrix.shard }}/4" --sharding slice -j 2 \
+ > mutants-shard.log 2>&1
+ ec=$?
+ set -e
+ missed=$(grep -c '^MISSED' mutants-shard.log || true)
+ echo "missed=${missed}" >> "$GITHUB_OUTPUT"
+ cat mutants-shard.log
+ exit "$ec"
+ - name: summary
+ if: always()
+ run: |
+ {
+ echo "### mutants ${{ matrix.shard }}/4"
+ echo "Advisory (not a merge gate). Outcome: ${{ steps.run.outcome }}. Missed: ${{ steps.run.outputs.missed }}."
+ } >> "$GITHUB_STEP_SUMMARY"
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ if: always()
+ with:
+ name: mutants-pr-${{ matrix.shard }}
+ path: mutants.out
+ if-no-files-found: ignore
+ retention-days: 5
+
# Line-coverage 92% floor + CRAP --fail-above 30 (allowlist in .cargo-crap.toml).
# Slow; waits for the fast gates so a red fmt/clippy/test does not start
# this job. Required. Job+step timeouts so a hung llvm-cov test binary
diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml
index 6dc0e50c9..f6bcf88d7 100644
--- a/.github/workflows/mutants.yml
+++ b/.github/workflows/mutants.yml
@@ -1,4 +1,5 @@
-# Weekly cargo-mutants + PR in-diff advisory. Not a required PR check.
+# Weekly cargo-mutants. Not a required check.
+# PR in-diff shards are ci.yml job `mutants` (parallel with coverage).
# Must pass --workspace: default-members is rbitcoin-node only.
# Owner: TESTING.md (Mutation testing). Snapshot lists: docs/mutants/.
@@ -7,40 +8,12 @@ on:
schedule:
- cron: "0 3 * * 0"
workflow_dispatch:
- pull_request:
- paths:
- - "Cargo.toml"
- - "Cargo.lock"
- - "crates/**"
- - ".github/workflows/mutants.yml"
permissions:
contents: read
jobs:
- mutants-pr:
- if: github.event_name == 'pull_request'
- concurrency:
- group: mutants-pr-${{ github.ref }}
- cancel-in-progress: true
- runs-on: ubuntu-latest
- timeout-minutes: 30
- steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- persist-credentials: false
- submodules: false
- fetch-depth: 0
- - uses: dtolnay/rust-toolchain@a5f673d0ba8626c3977bb416a1612774bc82181b # rustc 1.95.0
- - uses: taiki-e/install-action@9534c84618278caac52cb373bb164ed464dbd8af # v2.87.11
- with:
- tool: cargo-mutants@27.1.0
- - run: git diff origin/${{ github.base_ref }}.. --unified=0 > git.diff
- - run: cargo mutants --workspace --in-diff git.diff -j 2
- continue-on-error: true
-
mutants-weekly:
- if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 360
strategy:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 814a5fd99..d031685a5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,15 +33,21 @@ before 1.0).
### Changed
+- **PR cargo-mutants is 4 in-diff shards:** `ci.yml` `mutants`, after
+ fmt/clippy/test, in parallel with coverage. Advisory
+ (`continue-on-error`), not a merge gate. One 30-minute job was canceled
+ on a ~300-mutant diff (`-j 2` is about 50 minutes). Weekly 8-shard sweep
+ is unchanged.
+
- **CI short gates share one runner:** `fmt` → `deny` → `ast-grep` →
`nixos-module-eval`. clippy, test, windows, and macos still start
- immediately. Concurrent-job slots stay free for future cargo-mutants
- shards.
+ immediately. Concurrent-job slots stay free for coverage and the PR
+ mutants shards.
-- **Weekly cargo-mutants:** `mutants.yml` — PR `--workspace --in-diff`
- advisory (`continue-on-error`); Sunday 8-shard `--workspace` sweep.
- Must use `--workspace` (`default-members` is node). Snapshot lists:
- [`docs/mutants/`](docs/mutants/). How to run: [`TESTING.md`](TESTING.md).
+- **Weekly cargo-mutants:** `mutants.yml` — Sunday 8-shard `--workspace`
+ sweep. Must use `--workspace` (`default-members` is node). Snapshot
+ lists: [`docs/mutants/`](docs/mutants/). How to run:
+ [`TESTING.md`](TESTING.md).
- **Electrum/Esplora no longer require `--sh-index` to bind.** Address and
scripthash methods return `scripthash index disabled` (Electrum JSON-RPC
diff --git a/TESTING.md b/TESTING.md
index ee487dce2..2efdcf5b1 100644
--- a/TESTING.md
+++ b/TESTING.md
@@ -254,7 +254,7 @@ matches scalar in default tests). Owner: [`docs/quality.md`](./docs/quality.md).
| **cargo-crap** | After LCOV, `./scripts/coverage.sh` calls `./scripts/coverage-crap.sh` (skip if `cargo-crap` missing). `--fail-above --threshold 30`; `.cargo-crap.toml` allowlists today's production CRAP>30 functions (remove a name when it scores ≤30). Dry-run: `CRAP_DRY_RUN=1 ./scripts/coverage-crap.sh`. Self-test: `./scripts/coverage-crap.test.sh` | Rides required `coverage`. No `--fail-regression` (llvm-cov coverage % jitters per function) |
| **coverage ignore / badge** | `./scripts/coverage.test.sh` (filename ignore, Tier A IBD not skipped, 92% floor, Shields JSON). Publish dry-run: `BADGE_DRY_RUN=1 ./scripts/publish-coverage-badge.sh` | `test` job self-test; `coverage` job writes `coverage/badge.json` and, on green `master`, pushes `badges/coverage.json` |
| **Miri** | `./scripts/miri.sh` → `cargo +nightly miri test -p rbitcoin-primitives`. Dry-run: `MIRI_DRY_RUN=1 ./scripts/miri.sh`. Self-test: `./scripts/miri.test.sh` | Nightly `miri.yml` (not required). Never `--workspace` |
-| **cargo-mutants** | Must pass `--workspace` (`Cargo.toml` `default-members` is `rbitcoin-node` only; without `--workspace` the list is ~7 mutants). PR advisory: `git diff origin/.. --unified=0 > git.diff` then `cargo mutants --workspace --in-diff git.diff -j 2`. Weekly: `cargo mutants --workspace --shard N/8 -j 2`. Snapshot of missed/timeouts: [`docs/mutants/`](docs/mutants/). | Weekly `mutants.yml` Sunday 03:00 UTC, 8 shards, 360min cap (not required). `workflow_dispatch`. PR job `mutants-pr` is `continue-on-error` (not a merge gate). First Sunday wall-clock is still a measurement. |
+| **cargo-mutants** | Must pass `--workspace` (`Cargo.toml` `default-members` is `rbitcoin-node` only; without `--workspace` the list is ~7 mutants). PR: `git diff origin/.. --unified=0 > git.diff` then `cargo mutants --workspace --in-diff git.diff --shard k/4 --sharding slice -j 2` (same flags on every shard). Weekly: `cargo mutants --workspace --shard N/8 -j 2`. Snapshot of missed/timeouts: [`docs/mutants/`](docs/mutants/). | PR: `ci.yml` `mutants`, 4 shards, needs fmt/clippy/test (same window as coverage). `continue-on-error` (not a merge gate). Skipped when the diff does not touch `crates/`, `Cargo.toml`, `Cargo.lock`, `ci.yml`, or `mutants.yml`. Weekly `mutants.yml` Sunday 03:00 UTC, 8 shards, 360min cap (not required). `workflow_dispatch`. First Sunday wall-clock is still a measurement. |
Artifact silos above are unchanged: ast-grep / Miri dry-run / crap dry-run do
not write `target/`. `mutants.out/` is gitignored.