1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
name: main
on:
push:
branches:
pull_request:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
RUSTDOCFLAGS: -Dwarnings
jobs:
# Which expensive jobs a change can possibly affect.
#
# `merge_group` is the one event that never filters. The queue validates a *combination* no single
# pull request tested, so skipping there could let a broken pair through -- and `paths-filter` has
# no base to diff against on that event anyway, so it is not even asked. Pull requests and pushes
# both diff cleanly and are both filtered.
#
# The filter never lives in `on.*.paths`: a job skipped by `if:` reports **success** to branch
# protection, whereas a workflow filtered out by `on.*.paths` leaves its check pending forever and
# blocks the merge. That distinction is why `ci-success` below can be the single required check.
changes:
runs-on: ubuntu-latest
outputs:
rust: ${{ steps.filter.outputs.rust }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v3
id: filter
if: github.event_name != 'merge_group'
with:
filters: |
rust:
- '**/*.rs'
- '**/Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain*'
- '.github/workflows/main.yml'
deps:
- '**/Cargo.toml'
- 'Cargo.lock'
- 'deny.toml'
- '.github/workflows/main.yml'
# `cargo fmt --check` only ever inspects `**/*.rs`, so it can't produce a different verdict on
# a change the `rust` filter doesn't cover -- same reasoning as the compile jobs below, gated
# the same way for the first time here (`changes` job's header comment).
fmt:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: cargo fmt --check
# This repository's own invariants, none of which compiles anything (~2 s all told) -- hence no
# path filter: gating a free check can only ever skip it wrongly. Two of them are not
# documentation-only anyway. `check-domain-purity.sh` reads the manifests and `lww-register/src`;
# `check-doc-structure.sh` greps rustdoc for `SOTA.md` citations. A `**/*.md` filter would have
# been wrong on its own terms.
#
# `check-mutant-count.sh` needs `cargo-mutants` on `PATH` (unlike the four scripts above, which
# only need plain `cargo`), but the check itself is still sub-second -- `--list` is syntactic
# mutant-site discovery, no build (script's own header comment has the measurement).
repo-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: taiki-e/install-action@v2
with:
# Pin matches mutants.yml -- keep both in sync.
tool: cargo-mutants@27.1.0
- name: Doc budget (AGENTS.md + CLAUDE.md, and SOTA.md, line caps)
run: ./scripts/check-doc-budget.sh
- name: Domain purity (hexagonal boundary + ARCHITECTURE.md §2's graph vs the manifests)
run: ./scripts/check-domain-purity.sh
- name: Doc structure (link/anchor/path resolution, SOTA.md §4.2 entry format)
run: ./scripts/check-doc-structure.sh
- name: Test-file naming (split #[cfg(test)] modules are named tests.rs, not <name>_tests.rs)
run: ./scripts/check-test-file-naming.sh
- name: File size (prod/test line-count budgets, warning + hard-fail tiers)
run: ./scripts/check-file-size.sh
- name: Mutant count (.cargo/mutants.toml's header comment vs cargo-mutants --list)
run: ./scripts/check-mutant-count.sh
# Both feature axes in one job: they are 20 s and 11 s, and the second reuses the first's
# dependency build. Splitting them would add a runner start to save nothing.
clippy:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
# `--features internal-testing` exposes the `reconcile::testing` seam so the external
# integration oracles (`tests/diff.rs`, `tests/proptest_fingerprint_tree_map.rs`) and the
# benchmarks, which reach now-`pub(crate)` reconciliation internals, still compile.
- run: cargo clippy --workspace --features internal-testing --all-targets
- run: cargo clippy --workspace --all-features --all-targets
test:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-nextest
- run: cargo build --workspace --verbose
- run: cargo nextest run --workspace --features internal-testing --retries 4 --flaky-result fail
- run: cargo test --doc --workspace --features internal-testing --verbose
# AGENTS.md §6 gates the two feature variants separately on purpose: feature interactions hide
# bugs. A different feature set is a different `target/` fingerprint, so this shares no compiled
# artifact with `test` and loses nothing by running beside it.
test-all-features:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-nextest
- run: cargo nextest run --workspace --all-features --retries 4 --flaky-result fail
# #203: the default feature set and `--all-features` both resolve to `mac-blake3` (it takes
# precedence when both MAC backends are enabled, per Cargo.toml), so `mac-hmac` — a published,
# user-selectable crypto backend — was compiled by no CI job at all. This is the one job that
# selects it explicitly.
test-mac-hmac:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- uses: taiki-e/install-action@cargo-nextest
- run: cargo clippy --workspace --no-default-features --features mac-hmac,internal-testing --all-targets
- run: cargo build --workspace --no-default-features --features mac-hmac --verbose
- run: cargo nextest run --workspace --no-default-features --features mac-hmac,internal-testing --retries 4 --flaky-result fail
# Both variants matter: an intra-doc link to a feature-gated item dangles in only one of them.
docs:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- run: cargo doc --workspace --verbose
- run: cargo doc --workspace --all-features --verbose
# Its own job because it is the only **release**-profile build in the pipeline, so it shared
# nothing with the debug steps it used to sit among and was simply 70 s added to their critical
# path -- the largest single step in the pre-split job.
bench:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Swatinem/rust-cache@v2
- run: cargo bench --no-run --features internal-testing
# Packaging check for the whole workspace -- all five members, the release path end to end (#204,
# AGENTS.md §11). `--workspace` is what makes that possible: cargo strips `path` at publish time
# and resolves each intra-workspace dependency from crates.io, so a *per-crate* `cargo package -p
# rbsr` still fails with "no matching package named `rsos`" until `rsos` is actually published --
# only `rsos`, `lww-register` and `reconcile-gossip` (the members with no intra-workspace
# dependency) can be checked one at a time today. `--workspace` packages them together and
# resolves the `path` + `version` pairs against a local temporary registry instead, which covers
# `rbsr` and `reconcile` too. Keep it as one invocation for that reason; splitting it back into
# per-crate steps silently drops two crates from the check.
#
# `cargo package` (not `publish --dry-run`): it packages *and* compiles the packaged crate, which
# is the breakage this check exists to catch, without contacting the registry to upload.
#
# The one job with no `target/` cache, deliberately. CONTRIBUTING.md documents a measured incident
# where a stale sibling rlib in `target/` made exactly this command fail on an unresolved import
# that did not exist in the tree -- a false negative on the release gate is worth more than the
# ~14 s a cache would save. Registry only.
package:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Enable caching (registry only -- see above)
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# The same staleness class as the target/ incident above, one layer over: this job's own
# `cargo package --workspace` extracts each just-packaged workspace crate into
# ~/.cargo/registry/src/.../<name>-<version>/ to verify it, and cargo treats registry content
# as immutable per name@version -- so a restored cache from an earlier run (same Cargo.lock
# hash, since these are path deps with no lockfile-visible content change) keeps serving that
# earlier run's source for the four unpublished, version-frozen workspace crates
# (rsos/rbsr/lww-register/reconcile-gossip stay at 0.1.0 across ordinary commits), so a later
# commit's edits verify against stale registry sources instead of themselves. Purge just those
# four names before packaging; everything else in the restored cache -- real crates.io deps --
# is exactly what the cache exists to keep.
- run: rm -rf ~/.cargo/registry/src/*/{rsos,rbsr,lww-register,reconcile-gossip}-* ~/.cargo/registry/cache/*/{rsos,rbsr,lww-register,reconcile-gossip}-*
- run: cargo package --workspace --allow-dirty
# `pr-issue-references` used to live here. It reads the PR *description*, which changes without a
# push, so it needs the `edited` event type -- and `on:` is per workflow, not per job, so carrying
# it here would re-run every build job on a typo fix, cancelling any in flight. It is now
# `.github/workflows/pr-issue-references.yml`; that file's header has the full reasoning.
# #312: advisories (RustSec), license compliance, and a dependency-source allowlist. Config in
# `deny.toml`. `checks: advisories` alone would silently skip a license/bans/sources regression;
# the action's default `all` is deliberate here.
#
# Gated on `deps` rather than `rust`: this reads the dependency graph and `deny.toml`, so a pure
# `.rs` edit cannot change its verdict, while a manifest edit touching no `.rs` file can.
deny:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.deps == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: EmbarkStudios/cargo-deny-action@v2
# #311 rule 2/3: a committed public-API snapshot per crate (`public-api/*.txt`), diffed against
# `cargo public-api`'s live render, plus a check that `reconcile`'s public API names no `rbsr`
# symbol (AGENTS.md §11, #308 — `rbsr` is deliberately held at 0.x). Rule 1
# (`cargo-semver-checks` against a published baseline) is sequenced separately: it needs a
# registry baseline, and 4 of the 5 crates have never published one (AGENTS.md §11).
#
# CI-only tier (AGENTS.md §3): a full-workspace rustdoc-json build per crate is well over
# `pre-push`'s ~20 s budget. Needs a `nightly` toolchain (rustdoc's unstable
# `--output-format json`, which the stable channel does not expose) in addition to the `stable`
# default every other job uses, so it is the one job that installs a second toolchain.
#
# `cargo install` rather than `taiki-e/install-action`: that action has no `cargo-public-api`
# entry in its tool manifest (unlike `cargo-llvm-cov` below), so `@cargo-public-api` fails to
# resolve as a ref.
public-api:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-public-api --locked
- run: ./scripts/check-public-api.sh
coverage:
needs: changes
if: github.event_name == 'merge_group' || needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install llvm-tools-preview
run: rustup component add llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: Swatinem/rust-cache@v2
# `--all-features`: `internal-testing`-gated seams (e.g. `just_insert`) are what the
# integration-test oracles build against; without it this job fails to compile.
- name: Generate code coverage
run: cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
fail_ci_if_error: false
# **The single required status check.** Branch protection pins required checks by job name, so a
# list of a dozen names has to be re-edited in repository settings every time a job is added,
# renamed or split -- and nothing fails loudly when someone forgets, because a check that is not
# in the list simply is not enforced. Requiring one aggregate instead makes that class of mistake
# impossible; it is the convention `hyper`, `tokio` and `clap` all converged on.
#
# `if: always()` is load-bearing. Without it this job inherits the default `needs` behaviour and
# is *skipped* whenever any dependency is skipped -- which, with the path filters above, is most
# pull requests. A skipped required check passes, so the gate would silently stop gating.
#
# The condition treats `skipped` as acceptable (that is the filter working) and `failure` or
# `cancelled` as fatal. `success` is not tested for, because a job that neither succeeded, failed,
# nor was cancelled cannot exist.
ci-success:
name: ci-success
if: always()
needs:
- changes
- fmt
- repo-gates
- clippy
- test
- test-all-features
- test-mac-hmac
- docs
- bench
- package
- deny
- public-api
- coverage
runs-on: ubuntu-latest
steps:
- name: Fail if any job failed or was cancelled
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: |
echo "::error::one or more CI jobs failed or were cancelled"
exit 1