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
name: CI
on:
push:
branches:
pull_request:
workflow_call: # so release.yml can gate on exactly this
env:
CARGO_TERM_COLOR: always
# Action versions, and why these particular majors (0.8.0, A4, D-112).
#
# `actions/checkout@v4`, `setup-python@v5`, `upload-artifact@v4` and
# `download-artifact@v4` all declare `runs.using: node20` — verified by reading
# each action's own `action.yml`, not inferred from the deprecation banner. Node
# 20 is deprecated and force-migrated to Node 24, which annotated every job of
# every run across all four workflow files.
#
# Each is bumped to the FIRST major that declares `node24`, which is a different
# number for each of them: checkout v5, setup-python v6, upload-artifact v6,
# download-artifact v7. Not to the newest (v7 / v7 / v7 / v8) — that would also
# take an ESM migration, `download-artifact`'s new hash-mismatch enforcement,
# `upload-artifact`'s direct-upload semantics and `checkout`'s fork-PR blocking,
# none of which this repository needs and none of which can be tested anywhere
# but on CI itself. The goal is to stop running a deprecated runtime, and that
# is exactly what these four buy.
#
# `download-artifact` v5's one real breaking change — the output path for
# single-artifact downloads **by ID** — does not apply here: `wheels.yml` names
# no artifact and uses `merge-multiple: true` across all of them.
#
# Not bumped, because they are already fine: `Swatinem/rust-cache@v2` and
# `PyO3/maturin-action@v1` both declare `node24` on their current major, and
# `pypa/gh-action-pypi-publish@release/v1` is a composite action with no Node
# runtime at all. `dtolnay/rust-toolchain` is a branch ref by design.
jobs:
lint:
name: clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
# `--all-targets` covers benches and examples, which is where several of
# this project's diagnostics live and where clippy has caught real defects
# (D-075's oversized DbError came out of a clippy warning). Verified clean
# under `-D warnings` at 0.6.0, so this is a gate that currently passes.
#
# `-D warnings` is scoped to this step rather than set for the workflow:
# as a global RUSTFLAGS it also applies to the `cargo publish --dry-run`
# verification build, where a warning from a dependency's build would fail
# a packaging check that has nothing to do with warnings.
#
# **`property-tests` is in the feature list, and was not until 0.8.0.**
# `--all-targets` only reaches targets that the selected features compile,
# so the three quarantined generated-history binaries — which are gated
# behind `property-tests` — were never linted by this gate at all. That
# was found by accident in 0.8.0 (D-124), and what it was hiding was an
# unused import in `graph_property_tests` that had survived every green
# run. Linting a binary costs nothing here whether or not it is *run*
# under a feature gate, so the two lists have no reason to differ.
- name: clippy
run: cargo clippy --all-targets --features "metrics property-tests"
env:
RUSTFLAGS: -D warnings
# `metrics` is a default feature since 0.12.11 (D-154), and turning it off
# is a configuration this crate supports: `ActorMetrics` becomes an empty
# ZST and `record_hold` a no-op. Nothing here ever built that arrangement,
# so it was verified only by hand — and in 0.12.23 an example was added
# that calls `db.metrics()` with no `required-features`, which broke
# `cargo test --no-default-features` for two releases with every gate
# green (D-169).
#
# `--all-targets` is the point rather than a flourish: the break was in an
# example, and a plain `cargo check --no-default-features` does not build
# examples. Check rather than test, because what is at stake is whether
# the feature-off configuration still *compiles* — the behaviour it gates
# is covered by the suite above.
- name: check with metrics off
run: cargo check --no-default-features --all-targets
env:
RUSTFLAGS: -D warnings
# Rustdoc has its own warning set that `cargo clippy` never sees —
# unresolved intra-doc links, public docs linking to private items,
# unclosed HTML tags — and nothing in this workflow built the docs, so
# they accumulated silently: 18 of them by 0.8.0, every one pre-existing
# and none noticed at the commit that introduced it. A broken
# `[`Foo::bar`]` renders as literal brackets on docs.rs, which is a
# published defect in the crate's own documentation.
#
# `RUSTDOCFLAGS` rather than `RUSTFLAGS`, and scoped to this step for the
# same reason clippy's is (see above): as a workflow-level env it would
# also apply to the `cargo publish --dry-run` job, where a doc warning
# from a dependency would fail a packaging check for something the
# release does not control.
#
# `--no-deps` so only this crate's docs are built and only this crate's
# warnings can fail the gate. `--features metrics` matches what the test
# job documents; `property-tests` adds no library items, so unlike clippy
# there is nothing extra for it to reach here.
- name: rustdoc (no broken links)
run: cargo doc --no-deps --features metrics
env:
RUSTDOCFLAGS: -D warnings
# **Report-only, deliberately.** This repo has never been rustfmt-clean —
# 246 diffs at 0.6.0, essentially all in tests and benches — so a blocking
# gate would go red on its first run for a pre-existing condition, and the
# only way to green it is a repo-wide reformat nobody reviewed. Adopting
# rustfmt is a decision worth taking on purpose: run `cargo fmt --all` in
# its own commit, then delete `continue-on-error` here.
- name: rustfmt (advisory)
run: cargo fmt --all -- --check
continue-on-error: true
# The README carries an MSRV badge and `Cargo.toml` a `rust-version`, and
# both are claims. Nothing else in the build would notice them going stale:
# the crate is developed on stable, so a newly used std method raises the real
# floor silently and the declared one stays where it was.
#
# `--all-targets` deliberately: tests and benches use `OnceLock` and other
# items the library does not, and a contributor hitting a build failure the
# badge said would not happen is the same defect as a wrong badge.
msrv:
name: MSRV (rust-version in Cargo.toml)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- id: msrv
run: echo "v=$(grep -m1 '^rust-version' Cargo.toml | sed 's/.*= *//; s/\"//g')" >> "$GITHUB_OUTPUT"
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.msrv.outputs.v }}
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-features --all-targets
# `README.md` promises "Windows desktop, Linux, or macOS", and until 0.8.0 the
# only macOS evidence this project held arrived *through pyo3* — `python.yml`
# added `macos-latest` at P7, so the crate that is the actual product was
# tested on two of the three platforms it claims. That is a strange shape for
# a README claim to rest on, and the fix is four characters.
#
# It also buys a measurement worth having on its own: **the R15 rate on Apple
# silicon is unknown.** Every figure in `.cargo/config.toml` and in the R15
# risk row is from this Windows machine, and the fault has never been observed
# on a non-Windows runner — which is not the same as knowing it is absent.
# A1's classifier is what makes that observable rather than merely noisy: a
# macOS crash will now say `CRASH` and name its target instead of arriving as
# a smaller green.
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# For `scripts/run_rust_suite.py`. Pinned rather than relying on the
# runner's default: `python` and `python3` are not the same name on both
# images, and a gate that only exists on one OS is not a gate.
- uses: actions/setup-python@v6
with:
python-version: "3.12"
# `.cargo/config.toml` sets RUST_TEST_THREADS = "1" for R15 and applies
# here automatically. Read that file before changing anything below.
#
# R15 is an intermittent libSQL access violation (0xC0000005) triggered by
# concurrent database opens. It kills the process, so the affected target
# never prints its `test result:` line while every other target still
# prints its own — the run comes back with a SMALLER PASS COUNT AND ZERO
# FAILURES, which reads as green to anything summing passes, and red to
# cargo's exit code, for a reason that has nothing to do with the change
# under test.
#
# Both steps below used to be `for attempt in 1 2 3`, which counts
# failures without reading them: three failures produced four lines of log
# and none of them said whether libSQL died or a property found a real
# defect. `scripts/run_rust_suite.py` classifies instead — CRASH, FAILED,
# INCOMPLETE, TEARDOWN, BUILD — and retries only CRASH. A genuine failure
# is now reported on attempt 1 with the test named. D-107 did this for the
# Python suite; D-110 brings it back to Rust.
# The classifier is verified before it is trusted, and this step exists
# because verifying it by injection was not enough. Both injections were
# run locally and both were correct; the first CI run then reported
# `BUILD` on a suite in which all 27 targets passed, because
# `CARGO_TERM_COLOR: always` (set above, and not set locally) wraps every
# cargo status line in SGR escapes and the stderr parse matched nothing.
#
# Injection proves the classifier reads a real cargo run. It cannot prove
# it reads a run shaped the way *this workflow* shapes one. The fixtures
# include the coloured shape, cost no compile, and run first so a broken
# classifier is reported as a broken classifier rather than as a broken
# suite.
- name: suite gate self-test
shell: bash
run: python scripts/run_rust_suite.py --self-test
- name: test (with R15 retry)
shell: bash
run: python scripts/run_rust_suite.py --features metrics --attempts 3
# The generated-history binaries are quarantined behind a feature (see
# .cargo/config.toml): a property case opens a database of its own, which
# is exactly R15's trigger, so they fault far more often than the rest.
# Run as their own step so their noise cannot be mistaken for the suite's.
#
# Six attempts here, not three. The old budget was calibrated on the main
# suite — "R15 has always passed on re-run" is true where
# RUST_TEST_THREADS = "1" helps, and these targets are quarantined
# precisely because it does not: doctrine_property_tests alone measured
# 9 crashes in 15 runs at 0.7.0. Against p≈0.6 per attempt, three attempts
# go red about 22% of the time and six about 5% — which matches the 2-in-9
# observed on `main` in runs 30706318073 and 30706474231, both doc-only
# commits.
#
# p≈0.6 was one binary, one session, n=15. Measured on this step as a
# whole at 0.12.0 — 93% per attempt, n=100, on a developer Windows box
# under sustained load (.cargo/config.toml, D-147). Six attempts at that
# rate is 65%, and this step plainly does not go red 65% of the time
# here, so the runner is NOT that machine and the two figures bracket it
# rather than contradict each other: the 2-in-9 observed above implies
# p≈0.78 on the runner, between 0.6 and 0.93.
#
# The honest summary is that this rate is a property of the machine and
# the load, not a constant of the crate, and no measurement taken
# anywhere else predicts the runner. The budget is deliberately NOT
# raised: at any of these rates no budget makes the step a gate, and more
# attempts only buy more chances to launder a real failure. What the step
# is for is running these binaries at all and reporting honestly when the
# engine wins.
#
# Raising the budget was rejected while the loop existed, because more
# attempts also meant more chances to launder a real failure into a pass.
# The classifier removes that: a FAILED result returns on attempt 1 and is
# never retried, so the only thing six attempts buys is more tolerance for
# the one outcome that is known to be noise.
- name: property tests (quarantined, R15-prone)
shell: bash
run: python scripts/run_rust_suite.py --features property-tests --attempts 6
# A `cargo publish` failure after a tag is pushed is expensive: the tag is
# already public and the version number is spent. This runs the same
# packaging and verification step on every PR, so the release job's only new
# variable is the upload itself.
package:
name: cargo publish --dry-run
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo publish --dry-run