ci: run the Linux/macOS suite under nextest, cancel superseded PR runs

nextest's per-test processes cut the test step from 381s to 242s on
Linux and from 977s to 427s on macOS. A `ci` profile keeps it a full
gate: every tier, no retries, a hang kill at 10 minutes. Doctests still
run through `cargo test --doc`, which nextest does not cover.

Windows stays on `cargo test`. nextest (732s vs 665s), a Dev Drive
(1161s warm) and a two-way nextest partition (841s wall vs 820s) were
each measured there and none beat it.

ci, nix and macos-app now cancel an in-progress run when a newer commit
lands on the same pull request; pushes and dispatches are never cancelled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
This commit is contained in:
AkitaOnRails
2026-10-08 13:38:23 -03:00
co-authored by Claude Opus 5.5
parent f296bab558
commit d1da58982a
7 changed files with 72 additions and 19 deletions
+14 -9
View File
@@ -7,8 +7,7 @@
# needs more than ~1s alone either gets fixed or moves into a `slow`/`stress`
# module, and the 5s slow-timeout below surfaces new offenders in every run's
# summary. Two independent things run the skipped tier anyway: the pre-push
# hook (`scripts/install-git-hooks.sh`) and CI, which uses `cargo test` and
# never reads this file.
# hook (`scripts/install-git-hooks.sh`) and CI, which runs `-P ci` below.
[profile.default]
default-filter = "not test(/(^|::)(slow|stress)[a-z0-9_]*::/)"
@@ -31,15 +30,21 @@ failure-output = "immediate-final"
status-level = "fail"
# The slow tier legitimately sits in the 10-20s range.
slow-timeout = { period = "30s", terminate-after = 8 }
# No retries: no genuinely flaky test has been found in this suite, and a
# local gate should tell the truth. Retries belong in profile.ci.
# No retries: a gate should tell the truth (profile.ci follows the same rule).
[profile.ci]
# CI runs every tier, like `cargo tf`: `all()` overrides the everyday skip it
# would otherwise inherit from profile.default.
default-filter = "all()"
# Runner contention is real and separate from flakiness.
retries = 2
# No retries: a retry turns a flaky test green, and the suite's real flakes
# were found because CI showed them failing.
retries = 0
failure-output = "immediate-final"
fail-fast = false
[profile.ci.junit]
path = "junit.xml"
status-level = "fail"
final-status-level = "slow"
# Hosted runners (Windows especially) are several times slower than a
# workstation, so the everyday 5s marker would flag most of the suite. Kill a
# test only after 10 minutes: long enough for a slow runner, short enough that
# a hang fails the job instead of eating its whole timeout.
slow-timeout = { period = "60s", terminate-after = 10 }
+15 -1
View File
@@ -16,6 +16,13 @@ env:
permissions:
contents: read
# A newer push to a pull request supersedes its in-flight run, so cancel it.
# Pushes to main and manual dispatches are never cancelled: each is the record
# for its commit (a dispatch is the pre-release matrix on an exact SHA).
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
fmt:
name: rustfmt
@@ -70,7 +77,14 @@ jobs:
- name: Regenerate the Tailwind bundle
if: matrix.os == 'ubuntu-latest'
run: echo "TAILWIND_BUILD=1" >> "$GITHUB_ENV"
- run: cargo test --workspace --all-targets
# nextest runs every test binary's tests in parallel across the runner's
# cores; `cargo test` ran the binaries one after another, which was most
# of this job's wall clock. Profile `ci` (.config/nextest.toml) runs every
# tier, including slow::/stress::, with no retries.
- uses: taiki-e/install-action@f7e5d7c961414b23f5b25b2da9294395d08513ad # v2.87.26
with:
tool: cargo-nextest
- run: cargo nextest run --workspace --all-targets --profile ci
# `--all-targets` deliberately excludes doctests, so a doc-comment code
# block that fails to compile (e.g. an indented shell example rustdoc
# reads as Rust) shipped unnoticed. Run them explicitly.
+6
View File
@@ -8,6 +8,12 @@ on:
permissions:
contents: read
# Same rule as ci.yml: a newer push to a pull request cancels its in-flight
# run; pushes, schedules and dispatches each run to completion.
concurrency:
group: macos-app-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
swift-test:
name: swift test (ai-memory-macos)
+6
View File
@@ -51,6 +51,12 @@ on:
permissions:
contents: read
# Same rule as ci.yml: a newer push to a pull request cancels its in-flight
# run; pushes, schedules and dispatches each run to completion.
concurrency:
group: nix-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build-linux:
name: nix build (x86_64-linux)
+12 -7
View File
@@ -243,9 +243,10 @@ cargo deny check # dependency policy (if installed)
`.config/nextest.toml`. Run them from the repo root. `cargo t` builds the
workspace's default members, which is every shipped crate; the evals harness
is two more test binaries that only `cargo tf`, the pre-push hook, and CI
build (`--workspace`). Without nextest,
`cargo test --workspace --all-targets` is what CI runs: everything, slower,
no tiers.
build (`--workspace`). Linux and macOS CI run `cargo nextest run --workspace
--all-targets --profile ci`: every tier like `cargo tf`, no retries, a
10-minute hang kill. Without nextest, `cargo test --workspace --all-targets`
covers the same tests, slower, no tiers.
- **Slow tier.** A test whose module path has a segment starting with `slow`
or `stress` (`packaging::slow::*`, `stress_autoscope::*`) runs only under
@@ -332,7 +333,9 @@ no tiers.
locking, git plumbing, or the hook bundle, so the corresponding Windows
jobs run before the merge rather than only on the nightly schedule. Both
the Rust test job and the hook-bundle job use this label gate on pull
requests; they also run on manual dispatch.
requests; they also run on manual dispatch. That job stays on plain
`cargo test`: nextest, partitioning across runners, and a Dev Drive were
each measured there and none beat it.
## Code style guidelines
@@ -484,9 +487,11 @@ Additional boundary rules:
- New disk+SQL mutations need recovery/rollback tests.
- The recall-eval framework lives at
`crates/ai-memory-consolidate/tests/recall_eval.rs`.
- Tests run with `cargo t` locally and `cargo test --workspace --all-targets`
in CI (plus `cargo test --workspace --doc`, which `--all-targets` excludes,
for doc-comment code blocks).
- Tests run with `cargo t` locally,
`cargo nextest run --workspace --all-targets --profile ci` in Linux/macOS
CI, and `cargo test --workspace --all-targets` on Windows (plus
`cargo test --workspace --doc`, since neither runs doctests, for
doc-comment code blocks).
## Security considerations
+2 -1
View File
@@ -53,7 +53,8 @@ cargo deny check # dependency policy
```
`cargo tf` needs nextest (`cargo install cargo-nextest --locked`); without it,
`cargo test --workspace --all-targets` is the equivalent and is what CI runs.
`cargo test --workspace --all-targets` is the equivalent. Linux and macOS CI run the
same tests with `cargo nextest run --workspace --all-targets --profile ci`.
If `cargo-deny` or `cargo-audit` are not installed:
```bash
+17 -1
View File
@@ -759,7 +759,23 @@ fn workflows_keep_fixed_rust_jobs_on_the_fixed_toolchain() {
fn dev_loop_is_wired_consistently() {
// One gate, written down in four places; keep them from drifting apart.
let ci = read_repo(".github/workflows/ci.yml");
assert!(ci.contains("cargo test --workspace --all-targets"));
assert!(ci.contains("cargo nextest run --workspace --all-targets --profile ci"));
// nextest does not run doctests.
assert!(ci.contains("cargo test --workspace --doc"));
// Windows stays on plain `cargo test`: nextest's per-test processes cost
// more there than its parallelism saves.
let windows = read_repo(".github/workflows/windows.yml");
assert!(windows.contains("cargo test --workspace --all-targets"));
// The `ci` profile is what makes CI a full gate: every tier, nothing
// turned green by a retry.
let nextest = read_repo(".config/nextest.toml");
let ci_profile = nextest
.split("[profile.ci]")
.nth(1)
.expect("nextest.toml defines [profile.ci]");
let ci_profile = ci_profile.split("\n[").next().unwrap_or(ci_profile);
assert!(ci_profile.contains("default-filter = \"all()\""));
assert!(ci_profile.contains("retries = 0"));
assert!(
ci.contains("TAILWIND_BUILD=1"),
"CI must regenerate the stylesheet somewhere"