export: public tree rebuilt from f9e7dd1 #150
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| # Two reasons, and the second one was an accident that earned its keep. | |
| # | |
| # The stated one: the advisory database moves without anyone committing here, | |
| # so `deny` has to run on a clock -- otherwise a fresh RUSTSEC entry stays | |
| # unseen until the next unrelated pull request. | |
| # | |
| # The one found in practice (2026-08-17): the schedule runs the WHOLE | |
| # workflow, so the suite is executed weekly against a commit nobody touched. | |
| # That is the only thing in this project that can catch a flaky test after | |
| # the fact -- a push-triggered run tests a change, and a change that passes | |
| # once looks settled. The first scheduled run ever found a repair from the | |
| # night before that had swapped one race for a subtler one. Keep it whole; | |
| # narrowing this to the `deny` job would save minutes and lose that. | |
| # | |
| # This is also why the `test` job's condition below asks for the event name: | |
| # only a push may skip the shards. A scheduled or dispatched run tests | |
| # everything, whatever the diff of the day happens to look like. | |
| schedule: | |
| - cron: "17 5 * * 1" | |
| # A red scheduled run needs a second opinion, and asking for one should not | |
| # require an empty commit. Without this, the only way to re-test the exact | |
| # commit the cron tripped on is to push something -- which changes the thing | |
| # under test. (Found the hard way on 2026-08-17.) | |
| workflow_dispatch: | |
| env: | |
| CARGO_TERM_COLOR: always | |
| # Incremental compilation pays for itself when the same target directory is | |
| # compiled twice. A runner compiles it once and throws it away, so all the | |
| # scheme does here is write incremental state nobody reads -- and this | |
| # workspace has a disk ceiling to respect: the public test suite compiles | |
| # 400+ test binaries, and with debuginfo the target tree outgrew the 14 GB a | |
| # hosted runner has free (the run died on ENOSPC in the middle of the | |
| # build). The debuginfo half of that is no longer set here: | |
| # `.cargo/config.toml` builds dev and test with `debug = 0` for every | |
| # tree, this one included. | |
| CARGO_INCREMENTAL: 0 | |
| jobs: | |
| # --------------------------------------------------------------------- | |
| # Which stations does this diff need? The same question the maintainer's | |
| # local gate asks, answered by the same resolver -- `scripts/gate_plan.py`, | |
| # via `scripts/gate.sh`. Nothing in this file decides what a change is | |
| # worth testing; a trigger hard-coded here is exactly the drift the single | |
| # resolver exists to end. | |
| # | |
| # The one output is `tests=true|false`: whether this diff can reach a Rust | |
| # test at all. A push that only moves documentation cannot, and then the | |
| # three shards below do not start. | |
| # | |
| # `fetch-depth: 0` because the base of the diff is the push event's | |
| # `before`, and a shallow clone does not have that commit. An unknown or | |
| # all-zero base (a branch's first push, a force-push) makes the resolver | |
| # fall back to the full tree, which is the safe direction. | |
| # --------------------------------------------------------------------- | |
| plan: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tests: ${{ steps.plan.outputs.tests }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: plan | |
| # `shell: bash` for the pipefail it brings: without it a resolver that | |
| # refuses the diff would leave the output empty, and an empty output | |
| # reads as "no tests needed" -- the whole suite skipped in silence. | |
| shell: bash | |
| run: | | |
| scripts/gate.sh ci --plan-only --base "${{ github.event.before }}" \ | |
| | tee /dev/stderr \ | |
| | grep '^tests=' >> "$GITHUB_OUTPUT" | |
| # --------------------------------------------------------------------- | |
| # Everything that is not the test suite: fmt, clippy, the unwrap/expect | |
| # budget, the frozen corridor bodies, the anchor and claim gates, the | |
| # shell lint, the resolver's own self-test. One script decides which of | |
| # them this diff needs and runs them; each reports a `GATE <name> ...` | |
| # line. What a station is and when it fires is documented where it is | |
| # decided, in `scripts/gate_plan.py` -- not here, and not twice. | |
| # | |
| # Fast fail is the point. fmt and clippy used to sit in front of the test | |
| # step in one long job, so a formatting slip cost the full suite before it | |
| # was reported (measured 2026-08-27: fmt 3s, clippy 47s, test 10m43s -- | |
| # the 50s verdict arrived eleven minutes late). | |
| # | |
| # It is deliberately NOT a `needs:` of the test job. Chaining would put | |
| # those 50s in front of every shard's build and lengthen the critical path | |
| # to save runner minutes we are not paying for. Parallel means the verdict | |
| # lands about a minute in, while the suite keeps running. | |
| # | |
| # ONE NOTE ON THE PUBLIC TREE, because a green run here says less than it | |
| # looks like it does. Three of these gates keep their corpus in a tree that | |
| # does not travel, so a copy travels instead: the spec-claims registry as | |
| # .github/gates/claims.tsv, the ADR anchors as a derived | |
| # .github/gates/adr-anchors.tsv (one row per anchor, resolved against | |
| # crates/ -- which is where the deletion the gate exists against would be | |
| # visible), and the roadmap's register half, which is absent here and | |
| # counted as a named skip. The published documents also carry the English | |
| # text under the plain name, so the German half of every claim row is | |
| # skipped here too. The strict runs -- both languages, and the copies | |
| # against their originals -- live in the maintainer's tree and ride the | |
| # suite as drift-lock tests. | |
| # | |
| # For the same reason a gate that USED to stand here does not (GH #234): | |
| # `build_librarian_seed.py --check`, guarding a committed tree that is | |
| # generated. Do not put it back -- and do not teach the resolver to plan | |
| # its station for this mode either. The generator lives under workshop/, | |
| # which is not part of this tree and is not meant to be, so the step could | |
| # not pass here -- it failed on a missing file, which says nothing about | |
| # whether the artifact drifted. It runs where its sources are: in the | |
| # maintainer's strand and release gates, and as a test in the suite that | |
| # ships (that twin skips when no generator is present, which here is | |
| # always). | |
| # --------------------------------------------------------------------- | |
| gate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| # Its own key: clippy builds dependencies as `check` artifacts | |
| # (.rmeta), the test job needs them as real rlibs. Sharing one key | |
| # would hand the test shards a cache "hit" that still forces a full | |
| # dependency rebuild. The predecessor of this job ran without a | |
| # cache at all, on the grounds that its steps were bash and python | |
| # over text -- no longer true, the unwrap budget rides clippy. | |
| shared-key: lint | |
| - run: scripts/gate.sh ci | |
| # --------------------------------------------------------------------- | |
| # The suite, cut into shards. Two changes over the old single `cargo test` | |
| # step, and they attack the two halves of the 10m43s it took: | |
| # | |
| # 1. cargo-nextest instead of cargo test. `cargo test` runs the ~650 test | |
| # BINARIES one after another and only threads within each one; nextest | |
| # runs tests from different binaries at the same time. The measured | |
| # sum of per-binary wall time was 404s, with the longest single binary | |
| # at 30s -- i.e. the old run was mostly waiting. | |
| # 2. `--partition count:N/3`, so the run time is divided again across | |
| # three runners. | |
| # | |
| # Each shard builds its own test binaries rather than downloading a | |
| # `nextest archive` from a build job. That is deliberate: the build is | |
| # ~3m50s and would sit in FRONT of every shard as a serial stage, plus the | |
| # transfer of a multi-hundred-megabyte archive. Built per shard, the three | |
| # builds run at the same time, and the dependency half of the build comes | |
| # out of the shared cache below -- no shard compiles a dependency twice. | |
| # It costs runner minutes, not wall clock, and runner minutes are free on | |
| # a public repository. | |
| # --------------------------------------------------------------------- | |
| test: | |
| runs-on: ubuntu-latest | |
| needs: plan | |
| # A push whose diff cannot reach a Rust test does not build one. Every | |
| # other event runs the whole suite regardless of the diff: a pull request | |
| # is judged as a whole, and the weekly run above is the flake catcher and | |
| # would be worthless if it inherited a push's narrowing. | |
| # Only an explicit `false` skips: an empty output (a resolver that refused | |
| # the diff, a step that died before the grep) must run the suite, not hide it. | |
| if: needs.plan.outputs.tests != 'false' || github.event_name != 'push' | |
| strategy: | |
| # Same reasoning as `--no-fail-fast` below, one level up: if shard 2 | |
| # fails, shards 1 and 3 must still report. A cancelled shard hides | |
| # every defect it was carrying. | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| # All three shards compile the identical dependency graph, so they | |
| # share one cache entry. | |
| shared-key: test | |
| - uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cargo-nextest | |
| # t2 is the whole suite in the vocabulary of scripts/test-tier.sh, which | |
| # is also what the maintainer runs before a tag -- one definition of the | |
| # tiers, in one file, invoked the same way from both sides. The script | |
| # normally wraps a run in the nice/ionice/flock/TMPDIR hygiene that a | |
| # shared build machine needs; it drops all of it when `CI` is set, which | |
| # Actions sets for every step. Extra arguments go through to nextest. | |
| # | |
| # `--no-fail-fast` because the default stops at the first failing test, | |
| # and this suite has 650+ binaries. A red run then reports one defect | |
| # and hides every other -- W3's export CI showed exactly one of two | |
| # identically-caused vacuity-floor failures, so the fix session repaired | |
| # one, pushed, and found the second on the next run (GH #356). One run | |
| # should cost one round, not one defect. (The `ci` profile in | |
| # .config/nextest.toml sets `fail-fast = false` as well; the flag is | |
| # repeated here so that the property survives a profile edit.) | |
| - name: test (shard ${{ matrix.shard }}/3) | |
| env: | |
| MECLAW_TIER_PROFILE: ci | |
| run: > | |
| scripts/test-tier.sh t2 | |
| --no-fail-fast | |
| --partition count:${{ matrix.shard }}/3 | |
| # nextest does not run doctests -- it has no way to, they are rustdoc's. | |
| # `cargo test --doc` keeps them in the run; it is cheap here because the | |
| # libraries are already built by the step above (measured: ~2s for all | |
| # seven crates). One shard is enough, doctests do not partition. | |
| - name: doctests | |
| if: matrix.shard == 1 | |
| run: cargo test --workspace --doc | |
| # The `ci` profile writes JUnit XML. Keeping it makes a red shard | |
| # readable without scrolling a log. | |
| - name: junit | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: junit-shard-${{ matrix.shard }} | |
| path: target/nextest/ci/junit.xml | |
| if-no-files-found: ignore | |
| windows: | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: check | |
| run: cargo check --workspace | |
| deny: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Deterministic half: a verdict that depends only on this commit's | |
| # Cargo.lock and deny.toml. Blocking. | |
| - name: bans, licenses, sources | |
| uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| command: check bans licenses sources | |
| # Time-dependent half: RUSTSEC advisories and crates.io yank state change | |
| # under a commit that never moved, so a red here is a report, not a | |
| # verdict on the pull request. It is deliberately not blocking; the | |
| # advisories standing open today are tracked in GH #127 (GH #115 is | |
| # where the split was decided). | |
| # | |
| # It stays a job of its own rather than a station of the gate above: | |
| # cargo-deny is not on the runner image, and the action that installs it | |
| # also caches its advisory database. The resolver never plans `deny` or | |
| # `deny-advisories` in ci mode at all -- both are in its `CI_EXCLUDED` | |
| # set for exactly this reason -- so this job is the only place either | |
| # check runs for a push. | |
| - name: advisories (reporting only) | |
| continue-on-error: true | |
| uses: EmbarkStudios/cargo-deny-action@v2 | |
| with: | |
| command: check advisories |