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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# Inspired by https://github.com/joshka/github-workflows/blob/main/.github/workflows/rust-check.yml
name: Lint & Test
on:
pull_request:
branches:
- "**"
merge_group:
workflow_call:
# The toolchain (from rust-toolchain.toml) and the shared cargo env
# (debuginfo trim, CARGO_INCREMENTAL) are single-sourced in
# .github/actions/rust-setup — see its header. No RUST_VERSION copy exists
# anymore, in any workflow.
env:
CARGO_TERM_COLOR: always
# Ensure the workflow runs once per PR: a newer push cancels and restarts the
# in-flight run. Keyed on the PR number rather than `head_ref` because two
# forks PRing from identically-named branches share a head_ref — under the
# old key they cancelled each other's runs.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
fmt:
name: stable / fmt
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# cache: false — this job runs rustfmt and shell scripts, compiles
# nothing, and its rust-cache entry was 200MB of pure quota pressure.
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
components: rustfmt
cache: "false"
- name: Run cargo fmt
run: cargo fmt -- --check
- name: Check immutable migration guide preamble
run: bash .github/scripts/check-migrating-guide-preamble.sh
# rust-toolchain.toml is the single toolchain source: the rust-setup
# composite action resolves its channel, and no workflow carries a
# RUST_VERSION copy anymore. This guard's remaining job is to keep it
# that way — it fails on any reintroduced RUST_VERSION copy or inline
# literal `toolchain:` pin that drifts from the channel, the bypasses
# that would silently build one job on a stale toolchain.
- name: Check workflow toolchain pins match rust-toolchain.toml
run: bash .github/scripts/check-toolchain-pin.sh
# Guard against CWD-relative test fixture paths. `cargo test` runs from the
# crate root while `cargo nextest` (CI's runner) runs from the workspace
# root, so a bare `tests/data/...` path passes under one and fails under the
# other — letting a broken fixture path stay green in CI. Real test code
# must anchor fixtures to CARGO_MANIFEST_DIR (see
# crates/rig-core/src/loaders/test_fixtures.rs). Doc-comment example
# snippets (`///`, `//!`) are exempt.
- name: Check test fixture paths are CWD-independent
run: |
if grep -rnE '"(\./)?tests/data' crates/*/src crates/*/tests --include='*.rs' \
| grep -vE ':[[:space:]]*//[/!]'; then
echo "::error::Found a CWD-relative 'tests/data' path in test code. Anchor it to CARGO_MANIFEST_DIR via crate::loaders::test_fixtures (see crates/rig-core/src/loaders/test_fixtures.rs)."
exit 1
fi
- name: Test WASM chat worker runtime
run: node --test examples/candle_wasm_chat/www/worker-runtime.test.mjs
# rig-core is in the matrix below rather than in a job of its own: the body
# was character-for-character identical, and `matrix.package` reproduces the
# same `stable / check rig-core wasm target` job name, so this is one fewer
# runner and one fewer copy to keep in sync — not a coverage change.
#
# The runtime split adds wasm-sensitive code to rig-agent, feature forwarding
# in the `rig` facade, and explicit feature wiring in the Candle runtime and
# browser example, so check each on the wasm target too. The baseline checks
# use default features; rig-core also gets an all-feature pass so optional
# loaders cannot introduce browser-incompatible transitive dependencies.
# None of these checks download a model.
#
# Each package must be checked **on its own**: cargo unifies features across
# packages built in one invocation, so a workspace-wide `--all-features` build
# (the doctest job) silently supplies features an individual manifest forgot to
# declare. That is precisely how `candle_wasm_chat` came to declare
# `default-features = false` without `agent` while importing `rig::agent` —
# green in CI, broken standalone. It is also the only consumer that exercises
# the browser-wasm *facade* path, and compiling it here means a
# JavaScript-only worker test cannot hide a Rust build regression.
#
# `wasm32-unknown-unknown` (browser) is the only supported wasm target. See
# crates/rig-agent/README.md for the matrix.
check-wasm-runtimes:
name: stable / check ${{ matrix.package }} wasm target
runs-on: ubuntu-latest
strategy:
# The legs are independent packages, and the pre-fold standalone
# rig-core job always ran to completion: without `fail-fast: false`,
# one broken package would cancel the sibling legs mid-run and hide
# independent wasm breaks behind an extra CI round-trip.
fail-fast: false
matrix:
package:
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
target: wasm32-unknown-unknown
- name: Run cargo check wasm target
run: cargo check --package ${{ matrix.package }} --target wasm32-unknown-unknown
- name: Run rig-core all-features wasm check
if: matrix.package == 'rig-core'
run: cargo check --package rig-core --all-features --target wasm32-unknown-unknown
# `rmcp` is native-only: rmcp's `ClientHandler` requires `Send + Sync`
# unconditionally, which rig's wasm tool registry cannot satisfy. Asking for it
# on wasm must fail with exactly one actionable sentence rather than a wall of
# `dyn ErasedTool` trait errors, so assert both the message *and* the error
# count — a new ungated `#[cfg(feature = "rmcp")]` would leak follow-on errors
# and is the regression this guards.
check-rmcp-native-only:
name: stable / rmcp rejected on wasm
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
target: wasm32-unknown-unknown
- name: Assert the native-only diagnostic is the only error
run: |
set +e
out=$(cargo check --package rig-agent --features rmcp \
--target wasm32-unknown-unknown 2>&1)
status=$?
set -e
echo "$out"
# Parse a decolorized copy. The workflow sets CARGO_TERM_COLOR=always,
# so rustc prefixes every diagnostic with SGR escapes and an anchored
# `^error` matches nothing — the count came out 0 and the step failed
# claiming the gate had leaked. The log above keeps its colors.
plain=$(printf '%s\n' "$out" | perl -pe 's/\e\[[0-9;]*[a-zA-Z]//g')
if [ $status -eq 0 ]; then
echo "::error::expected the rmcp native-only compile_error, but the build succeeded — the gate is gone"
exit 1
fi
if ! printf '%s\n' "$plain" | grep -q 'the `rmcp` feature is native-only'; then
echo "::error::build failed for the wrong reason; the native-only compile_error did not fire"
exit 1
fi
# Exclude cargo's trailing "could not compile ..." summary, which is
# itself printed as an `error:` line.
count=$(printf '%s\n' "$plain" | grep -E '^error(\[|:)' | grep -cv 'could not compile' || true)
if [ "$count" -ne 1 ]; then
echo "::error::expected exactly 1 error, got $count — an ungated \`#[cfg(feature = \"rmcp\")]\` is leaking follow-on errors"
exit 1
fi
echo "ok: rmcp on wasm fails with exactly one actionable error"
clippy:
name: stable / clippy
runs-on: ubuntu-latest
permissions:
checks: write
steps:
- name: Checkout
uses: actions/checkout@v5
# protoc: the lance build scripts run here too (clippy compiles every
# dependency's build script). No sccache: it cannot cache
# clippy-driver invocations.
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
components: clippy
protoc: "true"
- name: Run clippy action
uses: clechasseur/rs-clippy-check@v3
with:
args: --all-features --all-targets
# The PR gate's test sweep. The full `--all-features` run (companion vector
# stores, Docker-backed integration tests, the nested-`cargo check` facade
# build guard) lives in nightly.yaml: it runs on a schedule, on demand via
# workflow_dispatch, and — via cd.yaml's `workflow_call` — on every push to
# main, so nothing is released without the full suite. `--all-features`
# TYPE-CHECK coverage stays on the PR gate regardless: the clippy job lints
# `--all-features --all-targets`, and doctest/doc build all features too.
# Two narrower classes wait for the full lane: codegen/link of the
# feature-gated test binaries clippy only type-checks, and — because
# feature-unification means no `--all-features` build can catch a facade
# feature that forgets to forward a sub-feature — the per-feature facade
# compile breaks that only nightly.yaml's nested-`cargo check` guard
# (`tests/tool_facade_features.rs`, ~4 minutes, which is exactly why it is
# not on the gate) can see. Both surface in merge_group at the latest,
# before anything lands on main.
test:
name: stable / test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# The raw lancedb dependency used by the facade's integration tests is
# feature-gated with those tests. Neither graph below enables it, so a
# cold PR run avoids the Lance/Arrow/DataFusion graph and needs no
# system protoc. The all-features jobs retain both.
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
sccache: "true"
nextest: "true"
# `nextest` below runs with `--features bedrock`, which does not compile
# the pure default-feature test graph. Check the root integration tests
# with the default feature set so feature-gated test modules cannot
# accidentally depend on APIs that only exist under optional features.
#
# `cargo check`, not `cargo test --no-run`: the guarded failure class is
# purely type-level, while `--no-run` additionally pays codegen +
# linking under a default-feature hash the `--features bedrock` step
# below never reuses. Be precise about what this trades away: the
# bedrock run below and nightly's sweep link *superset* feature
# configurations, so no lane anywhere links the pure default-feature
# binaries — a default-only codegen/link break would surface first in
# merge_group's full run. That gap is accepted as theoretical:
# `bedrock` is purely additive and the workspace has no
# `cfg(not(feature = …))` in test code, so a break that appears only
# with features *removed* has no known mechanism.
- name: Check default-feature root test targets
run: cargo check --locked -p rig --tests
# The workspace sweep over the default members' default features, plus
# the facade's `bedrock` feature. `bedrock` rides along because the
# bedrock wire-conformance suite compiles into the facade's `bedrock`
# test binary and this step is the one
# `tests/core/streaming_conformance_registry.rs` names as executing it
# (rig-bedrock itself is already a default member, so the extra compile
# cost is marginal). Everything that needs the remaining optional
# features executes in nightly.yaml's identical-command full run.
#
# No provider API keys, deliberately: the cassette suites replay
# recorded traffic with a dummy key, every live test is
# `#[ignore]`-gated (nextest never selects them), and the integration
# tests stub their embeddings — so nothing CI executes reads a real
# key. Keeping secrets in the environment of a job that runs the whole
# test suite is pure exfiltration surface; recording new cassettes is a
# local, human activity (see tests/common/cassettes.rs).
#
# A plain `run:` rather than `actions-rs/cargo@v1`: that action was
# archived in 2023 and pins Node 20, which this workflow's own log
# already reports as force-upgraded to Node 24. It added nothing over
# invoking cargo directly.
# The two nested-Cargo binaries run in `test-guards` under the stronger
# configuration they protect: identity_leak with all features and
# macro_hygiene through its dedicated downstream-consumer step. Running
# them again here made a cold test execution spend over three minutes
# recompiling scratch projects after the other ~3,600 tests had nearly
# finished.
- name: Test with latest nextest release
run: >-
cargo nextest run --locked --features bedrock --retries 2
-E 'not (binary(identity_leak) | binary(macro_hygiene))'
- name: Show sccache statistics
if: always() && env.RUSTC_WRAPPER == 'sccache'
continue-on-error: true
run: sccache --show-stats
# The cross-crate contract tripwires, split from `test` so the two halves
# run in parallel: each step deliberately compiles a package set with its
# own feature configuration (see the comments below), which serializes
# poorly behind the default-feature sweep above.
test-guards:
name: stable / test guards
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
sccache: "true"
nextest: "true"
# No system protoc: every step below names packages that do not enable
# the facade's `lancedb` feature, and rig-gemini-grpc vendors its own
# protoc via protoc-bin-vendored.
# The full rig-core and rig-agent test suites under `--all-features`.
# Before the fast/full split, the workspace-wide `--all-features` sweep
# ran these on every PR; that sweep now lives in nightly.yaml, and this
# step keeps the feature-gated tests (the rmcp tool registry, the
# pdf/epub loaders, audio generation, …) from silently demoting to
# nightly-only coverage. It reuses the compile artifacts of the
# telemetry step below (same packages, same `--all-features`), so the
# cost is execution time only. The telemetry:: tests also run here with
# retries — harmless duplication: their no-retries discipline is
# enforced by the dedicated step below, exactly as it was when the
# workspace sweep ran them with `--retries 2` alongside it.
#
# All test targets except macro_hygiene, NOT `--lib`: rig-core's
# `identity_leak` trybuild suite is an *integration* test whose
# compile-fail proof (no `Serialize` path for `PartId`/`StreamPartId`)
# is only load-bearing under `--all-features`, because trybuild
# propagates the outer binary's enabled features into its scratch
# builds. `--lib` here would demote that proof — and rig-agent's non-lib
# targets such as `runtime_model_swapping` — to nightly-only coverage,
# exactly the silent narrowing this step exists to prevent.
# macro_hygiene also shells out to Cargo, but its dedicated step below
# is the authoritative plain-`cargo test` execution.
- name: Test rig-core and rig-agent with all features
run: >-
cargo nextest run --locked -p rig-core -p rig-agent --all-features
--retries 2 -E 'not binary(macro_hygiene)'
# `cargo nextest run` (in the `test` job) selects the workspace's
# *default members* — historically just the `rig` facade, which is why a
# `crates/*` unit test executed in CI only when its package was named
# explicitly; `default-members` now lists the core crates, but that
# sweep compiles them with *default* features and `--retries 2`. This
# step re-runs the `telemetry::` and `span_safety_net::` tests under
# `--all-features` and without retries. They are the drift tripwires
# for the marker and the required `gen_ai.*` field set: unrun, the
# three forms of the contract can silently desync, which is the exact
# failure the `completion_parent_span!` macro exists to prevent.
#
# The sweep above already *executes* these tests (same packages, same
# `--all-features`, so this step reuses its compile artifacts and costs
# execution time only) — but it runs them with `--retries 2`, and a
# drift tripwire that passes on the second attempt conceals exactly the
# nondeterminism it exists to catch. The dedicated no-retries run is
# the point of this step, not redundancy.
#
# No `--retries`, deliberately: these are static-metadata assertions
# (set equality against a `const`, field counts, callsite dedup), so a
# pass on the second attempt would be concealing nondeterminism rather
# than tolerating a flaky network.
- name: Test telemetry completion-parent contract
run: cargo nextest run --locked -p rig-core -p rig-agent --all-features -E 'test(telemetry::) + test(span_safety_net::)'
# This compiles a standalone downstream consumer that renames rig-core
# and has no direct tracing dependency. Keep it outside the workspace so
# release tooling cannot discover it as a publishable package.
- name: Test telemetry macro hygiene
run: cargo test --locked -p rig-core --test macro_hygiene
# Same default-members problem as the telemetry step above, with two
# distinct casualties:
#
# * `crates/rig-core/tests/driver_adoption.rs` is a rig-core *package*
# target, so the single-policy-driver guard and the serde policy wall
# never built in the facade-only `-p rig` run. The mechanism that
# keeps "one triage policy site" true over time was itself unrun.
# * three wire-conformance families live in test binaries outside the
# `rig` facade — `openai_responses_websocket`
# (`rig-core/tests/streaming_conformance_websocket.rs`), `candle`
# (`rig-candle`) and `gemini_grpc` (`rig-gemini-grpc`) — so the
# registry reported them covered while nothing executed them.
# (`bedrock`'s suite is feature-gated inside `rig`, so the `test`
# job's `--features bedrock` sweep covers it.)
#
# `binary(...)`, not `test(...)`: nextest's `test()` predicate matches the
# test's own name (`suite_is_complete`, …), never the binary it lives in,
# so `test(streaming_conformance)` selects ZERO tests here — a filter that
# matches nothing still exits 0, which is how this gap would silently
# reopen. `binary()` also defaults to an EQUALITY match, so
# `streaming_conformance_websocket` must be named separately from
# `streaming_conformance`. `tests/core/streaming_conformance_registry.rs`
# asserts this step's name and each binary predicate still appear here.
#
# No `--retries`, deliberately, for the same reason as the telemetry step:
# these are static structural assertions (workspace source scans, compiled
# suite manifests, fixture-driven replays with no network), so a pass on
# the second attempt would conceal nondeterminism rather than tolerate a
# flaky network.
- name: Test out-of-facade streaming conformance and structural guards
run: cargo nextest run --locked -p rig-core -p rig-candle -p rig-gemini-grpc --all-features -E 'binary(streaming_conformance) + binary(streaming_conformance_websocket) + binary(driver_adoption)'
- name: Show sccache statistics
if: always() && env.RUSTC_WRAPPER == 'sccache'
continue-on-error: true
run: sccache --show-stats
# rig-derive's own test suite — 43 tests across 12 binaries, including 20
# trybuild cases and the dependency-rename fixtures. Until this job existed
# none of them ran anywhere in CI: rig-derive is deliberately outside
# `default-members` (see the note in the root Cargo.toml), so the `test`
# job's sweep never selects it, and no step named `-p rig-derive`. The
# workspace manifest asserted the coverage came from the `macro_hygiene`
# step "and rig-derive's own trybuild tests" — the first is real, the
# second ran nowhere, and that comment is corrected alongside this job.
#
# A non-compiling `tests/fixtures/*` sits on a green PR under that gap:
# the fixtures are separate cargo projects that only build when the test
# executes, so neither the workspace build nor `clippy --all-targets`
# touches them.
#
# Its own job rather than a step in `test-guards`: that job documents at
# length why it needs no protoc, and the fixtures shell out to a nested
# `cargo check` over the rig-agent and facade dependency graphs — a
# different build shape that should not silently invalidate that
# reasoning. It runs in parallel and finishes well inside the `test` job's
# critical path, so the wall-clock cost of the gate is zero.
#
# `cargo test`, not nextest: the suite is trybuild plus subprocess-driven
# fixture checks, and the sibling `macro_hygiene` step uses plain
# `cargo test` for the same reason.
test-derive:
name: stable / test rig-derive
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install Rust stable
uses: ./.github/actions/rust-setup
- name: Test rig-derive
run: cargo test --locked -p rig-derive
# `cargo nextest` (used by the `test` job) does NOT run doctests, and the
# `doc` job only builds documentation — so without this job nothing executes
# the `///` code examples and a broken doctest can sail through CI.
doctest:
name: stable / doctest
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# protoc: required because this all-features job enables rig-lancedb.
# No sccache: this job is off the critical path and rustdoc's doctest
# execution is not cacheable anyway — see the quota note on the
# sccache input in the rust-setup action.
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
protoc: "true"
# No API keys needed: the doctests that actually execute are pure (the
# provider examples are all `no_run`/compile-only), so this job stays
# hermetic and non-flaky.
- name: Run doctests
run: cargo test --locked --doc --workspace --all-features
doc:
name: stable / doc
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# protoc: required to compile rig-lancedb. No sccache: rustdoc
# invocations are not cacheable by it.
- name: Install Rust stable
uses: ./.github/actions/rust-setup
with:
components: rust-docs
protoc: "true"
- name: Run cargo doc
run: cargo doc --locked --no-deps --all-features
env:
RUSTDOCFLAGS: -D warnings