Continuous integration
GitHub Actions owns triggers, permissions, runners, caches, artifacts, matrices, and job dependencies. The justfile
owns validation commands so each CI command also runs from a checkout.
Pull requests
The required workflow runs these job groups:
source: Rust formatting,cargo check, Clippy, and dependency policyautomation: repository hooks and workflow validationcontracts: snapshots, the release plan, and Cargo discovery of publishable packagesplatform: platform-boundary tests on macOS and Windowscoverage: the native workspace suite with all featuresfrontend: native and Wasm browser coveragedocs: rustdoc, Markdown, Mermaid regeneration, and the site build
There is no semver job. Every crate reads 0.0.1, a version at which Cargo permits any change, so cargo-semver-checks
compared each package against itself, assumed a major bump and skipped all 254 checks while reporting success. Seven
shards spent 43 minutes of runner time per run to check nothing, and a reader could not tell that green from a green
that had verified something. Nothing consumes these crates as libraries either: a release ships binaries through dist
and a PyPI package, and no workflow runs cargo publish.
Bring the job back when a crate is published, or when the workspace reaches a version where a bump means something. It
needs the baseline output on the contracts job as well, which went with it. Until then just semver answers the
same question on demand and states a release type so the checks run.
A LEAK or LEAK-FAIL from nextest names a victim rather than a culprit. Nextest reports one when a test process has
exited and its captured output has not reached EOF, which means another live process still holds the write end of that
pipe. macOS creates a pipe and sets FD_CLOEXEC on it in two steps, so a test binary spawned inside that window
inherits another test's capture pipe and holds it until it exits. The retaining process is whichever test happened to
start during the window, and the reported test is whichever happened to finish first. Neither did anything wrong.
Measured on this workspace: of 2,560 children spawned from tests, 37 inherited a descriptor they were never given, and
every one was a pipe, often both ends of the same one. Twelve tests that only sleep for three seconds, beside a hundred
that only print, reproduce LEAK-FAIL on the printing ones with no subprocess anywhere in the package. Closing
inherited descriptors in a child changes nothing, because a child is not what holds the pipe.
So a LEAK says nothing about the test it names, and the test needs no repair. Record it against
#1629 and move on. Raising leak-timeout, excluding a test, retrying,
or serialising the run would each hide the report without changing what it reports.
nextest-rs/nextest#3553 fixes the spawn boundary upstream and is not
yet in a release.
The nightly mutation run examines production code only. .cargo/mutants.toml excludes benchmark workloads under
crates/*/src/bench/, the shared harness in crates/peryx-test-support/, and the fixture binaries under
crates/*/tests/. A surviving mutant means the code could behave another way and no test would notice, which marks a
gap where the behaviour is a promise to somebody. None of those three paths promises anything outside this repository,
so a survivor in them is noise that each run reports again. The exclusions take 18,902 mutants down to 17,839.
The harness is the one worth arguing about, since a fault injector that stopped injecting would leave every test using
it green while testing nothing. That failure shows up red instead. A test that arms a fault asserts the faulted outcome,
so it fails when the fault does not arrive, and -D dead_code covers harness behaviour no test reaches. Both checks
land before mutation asks the question, and the inventory agrees, with no mutant in that crate surviving a run. Add a
path back if that stops holding, and say in the config why.
Excluding paths shortens the matrix rather than the shards. The nightly derives its shard count from the same list it
mutates, at mutation-shard-count "$(just mutation-count)" 128, so the run goes from 148 shards to 140 with each still
targeting 128 mutants. A shard takes as long as it did.
The coverage jobs reject uncovered source lines. ci-gate gives branch protection one check name and fails unless every
required job succeeds.
CodSpeed runs the ecosystem benchmark packages on standard GitHub-hosted runners in
simulation mode. This avoids quota-limited Macro Runners. Run the same benchmark path with
just codspeed PACKAGE.
Test synchronization
Tests wait for observable state changes. Child-process cases use ProcessHarness::spawn_until_event,
Node::await_event, or the topology event stream. In-process async cases use channels or
tokio::sync::Notify. Code that measures elapsed time uses Tokio's paused clock.
Deadlines bound failed waits, and the CI profile supplies a per-test termination guard.
Nightly analysis
The nightly workflow runs feature combinations, direct dependency lower bounds, Miri, Loom, AddressSanitizer, mutation testing, each cargo-fuzz target, and the live PyPI client boundary. Each matrix leg invokes a public Just recipe.
Sanitizer and mutation jobs build Nextest archives once, then run partitions from those archives.
AddressSanitizer follows Rust's -Zsanitizer and -Zbuild-std invocation. Nextest 0.9.143
classifies Rust's gnuasan target as a custom target, but Rust does not publish custom
target JSON for that built-in target. The workflow uses the standard Linux target.
The async suite does not run under ThreadSanitizer. Tokio issue 7299 records internal false positives and identifies Miri and Loom as its race-analysis tools; nightly CI runs both.
Local commands
Install the locked tools, then run the recipe named by a CI job:
mise install --locked
just lint
just platform-test
just coverage-native
just frontend-deps
just coverage-frontend
just docs
Browser recipes install their checksum-verified Chrome for Testing revision from the scoped browser mise environment.
Chrome for Testing ships no Linux ARM or Windows ARM builds, so mise.browser.lock covers the four platforms it does
publish.
just test is hermetic. just storage-s3 and just coverage-native require a running Docker daemon for the MinIO
boundary tests.
Nightly commands are local too:
just features
just direct-minimum
just miri
just loom
just sanitizer-address
just mutation-baseline
just mutation 1/8
just fuzz peryx-ecosystem-oci oci_reference 60
just e2e-live
Generated files stay under .tox/. just coverage-clean, just clean, and just clean-all remove increasing amounts
of local build state.