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
# Named for its README badge, as are miri.yml, sanitizer.yml, shuttle.yml,
# fuzz.yml and verus.yml.
name: test
on:
push:
branches:
pull_request:
# Cancel a pull request's superseded runs. Every commit on main finishes its
# run, so the README badge reports it.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
permissions:
contents: read
# For the release fuzzing to fetch the corpora that earlier runs saved.
actions: read
jobs:
# Whether this pull request is a release: one that changes the package
# version (.github/actions/version-change). A release must also pass the
# `release-` jobs.
plan:
name: plan
runs-on: ubuntu-latest
outputs:
release: ${{ steps.version.outputs.changed || 'false' }}
steps:
- uses: actions/checkout@v4
- id: version
if: github.event_name == 'pull_request'
uses: ./.github/actions/version-change
with:
base: ${{ github.event.pull_request.base.sha }}
check:
name: fmt + clippy + test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Test
run: cargo test --workspace --all-features
bench:
name: bench (fmt + clippy + test + quick run)
runs-on: ubuntu-latest
defaults:
run:
working-directory: bench
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
components: rustfmt, clippy
# The bench builds for the runner's CPU (bench/.cargo/config.toml), and
# runners differ: a build cached on one with AVX-512 dies of SIGILL on
# one without. So each instruction set gets its own cache.
- name: Identify the CPU's instruction set
id: cpu
run: echo "flags=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
- uses: Swatinem/rust-cache@v2
with:
workspaces: bench
key: cpu-${{ steps.cpu.outputs.flags }}
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Test
run: cargo test
# Keeps the suite building and running as the library's API changes;
# timings on a shared runner mean nothing.
- name: Quick run
run: |
cargo run --release --bin bbtree-bench -- small --quick
cargo run --release --bin bbtree-bench-memory -- small --quick
# CI has no datasets: their cells must be reported as missing data
# while the rest of the suite runs.
- name: Quick run without datasets
env:
BBTREE_BENCH_DATA: ${{ runner.temp }}/no-bench-data
run: cargo run --release --bin bbtree-bench -- realkeys tiny --quick --map bbtree-unsync
fuzz:
name: fuzz harness (fmt + clippy + test)
runs-on: ubuntu-latest
defaults:
run:
working-directory: fuzz
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: fuzz
# The structural checks that the fuzz targets call, which only fuzzing
# builds (`--cfg fuzzing`) compile.
- name: Clippy (library, fuzzing build)
working-directory: .
run: cargo clippy --all-features -- -D warnings
env:
RUSTFLAGS: --cfg fuzzing
- name: Format
run: cargo fmt --all -- --check
# `fuzz/.cargo/config.toml` builds with `--cfg fuzzing`, as cargo-fuzz
# does.
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
# The harnesses on pseudo-random inputs, and every crash kept in
# `regressions/`. The fuzzing itself runs weekly and for every release
# (fuzz.yml).
- name: Test
run: cargo test
# The library's tests on other platforms. On ARM, the portable code runs
# where x86 has SIMD. On macOS, Apple's chips run it, with their 128-byte
# cache lines and 16 KiB pages, and macOS's own allocator. On wasm, pointers
# are 32 bits, a panic aborts and there are no threads, so tests that
# assume 64-bit pointers, need unwinding or spawn threads are ignored
# there.
test-aarch64:
name: test (aarch64)
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
- uses: Swatinem/rust-cache@v2
- name: Test
run: cargo test --lib --all-features
test-macos:
name: test (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
- uses: Swatinem/rust-cache@v2
- name: Test
run: cargo test --workspace --all-features
test-wasm:
name: test (wasm32-wasip1)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
targets: wasm32-wasip1
- name: Install wasmtime
uses: taiki-e/install-action@v2
with:
tool: wasmtime
- uses: Swatinem/rust-cache@v2
- name: Test
env:
CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime
# The tests run on the main thread, whose stack wasm-ld makes 1 MiB,
# and an overflow there faults far from the cause. Debug builds of
# walks over page-aligned values need about 1.25 MiB; give the 8 MiB
# of a native main thread.
CARGO_TARGET_WASM32_WASIP1_RUSTFLAGS: -C link-arg=-zstack-size=8388608
run: cargo test --target wasm32-wasip1 --lib --all-features
# The release checks: the fuzzing takes half an hour on the shared runner,
# so it runs for releases (and weekly, in fuzz.yml) rather than for every
# change.
release-fuzz:
name: release fuzz
needs: plan
if: needs.plan.outputs.release == 'true'
uses: ./.github/workflows/fuzz.yml
with:
minutes: 30
release-package:
name: release package
needs: plan
if: needs.plan.outputs.release == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Keep this exact nightly aligned with flake.nix.
- name: Install Rust (nightly-2026-10-07)
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-10-07
# What `cargo publish` will upload, built from those files alone, so
# that publishing after the merge doesn't fail on the package.
- name: Package
run: cargo publish --dry-run --locked
# This workflow's one required check (.github/rulesets/main.json), so the
# jobs above can change without touching the ruleset. It always runs: a
# required check that is skipped counts as passed. Miri, the sanitizer and
# Shuttle are required checks of their own (miri.yml, sanitizer.yml,
# shuttle.yml), as Verus will be.
merge-gate:
name: merge gate
if: always()
needs:
runs-on: ubuntu-latest
steps:
- name: Check that every job passed
env:
RELEASE: ${{ needs.plan.outputs.release }}
RESULTS: ${{ toJSON(needs) }}
run: |
# A change that isn't a release skips the `release-` jobs; every
# other result must be a pass.
not_passed=$(jq -r --arg release "$RELEASE" '
[ to_entries[]
| select(.value.result != "success")
| select(($release == "false"
and (.key | startswith("release-"))
and .value.result == "skipped") | not)
| "\(.key): \(.value.result)" ]
| join(", ")' <<< "$RESULTS")
if [ -n "$not_passed" ]; then
echo "::error::Not passed: $not_passed"
exit 1
fi