Documentation

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 policy
  • automation: repository hooks and workflow validation
  • contracts: snapshots, the release plan, and Cargo discovery of publishable packages
  • platform: platform-boundary tests on macOS and Windows
  • coverage: the native workspace suite with all features
  • frontend: native and Wasm browser coverage
  • docs: 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.

On this page