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
# 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:
env:
CARGO_TERM_COLOR: always
RUST_VERSION: 1.94.0
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
fmt:
name: stable / fmt
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
toolchain: ${{ env.RUST_VERSION }}
- name: Run cargo fmt
run: cargo fmt -- --check
# 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
# Special check to make sure rig-core is compatible with the wasm target
check-wasm:
name: stable / check rig-core wasm target
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: wasm32-unknown-unknown
toolchain: ${{ env.RUST_VERSION }}
- name: Run cargo check wasm target
run: cargo check --package rig-core --target wasm32-unknown-unknown
# 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. These are plain
# checks with no feature flags: the relaxed async bounds now follow from the
# target alone, so building for `wasm32-unknown-unknown` is the whole opt-in.
# None of them 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:
matrix:
package:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: wasm32-unknown-unknown
toolchain: ${{ env.RUST_VERSION }}
- name: Run cargo check wasm target
run: cargo check --package ${{ matrix.package }} --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@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: wasm32-unknown-unknown
toolchain: ${{ env.RUST_VERSION }}
- 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@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy
toolchain: ${{ env.RUST_VERSION }}
# Required to compile rig-lancedb
- name: Install Protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Run clippy action
uses: clechasseur/rs-clippy-check@v3
with:
args: --all-features --all-targets
test:
name: stable / test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install nextest
uses: taiki-e/install-action@v2
with:
tool: nextest
# Required to compile rig-lancedb
- name: Install Protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
# `nextest` below runs with `--all-features`, which does not compile the
# default-feature test graph. Build 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.
- name: Compile default-feature root test targets
run: cargo test -p rig --tests --no-run
- name: Test rig-agent agent unit tests with all features
run: cargo nextest run -p rig-agent --all-features -E 'test(agent::)' --retries 2
# `cargo nextest run` (below) selects the workspace's *default members*,
# and this workspace's root manifest is itself the `rig` package with no
# `default-members` — so that run covers `rig` alone and a `crates/*` unit
# test executes in CI only when its package is named explicitly. The
# rig-agent half of the completion-parent contract rides along on the
# `agent::` filter above, but rig-core's `telemetry::` tests match nothing
# and would otherwise never run. 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.
#
# rig-agent is named explicitly even though `test(agent::)` above already
# matches `span_safety_net::`, so the cross-crate tripwire keeps running
# if that filter is ever narrowed. The tests are cheap and the duplicate
# run is the price of not depending on an unrelated filter.
#
# No `--retries`, deliberately, unlike the steps around it: 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.
#
# This resolves rig-core with its own `--all-features` set, a different
# configuration from both the `-p rig-agent` step above and the root
# `-p rig` run below, so rig-core is compiled here rather than reused.
# That cost is accepted deliberately: folding these tests into the
# `agent::` step would share one build but hand them its `--retries 2`,
# and a drift tripwire that passes on the second attempt conceals exactly
# the nondeterminism it exists to catch.
- name: Test telemetry completion-parent contract
run: cargo nextest run -p rig-core -p rig-agent --all-features -E 'test(telemetry::) + test(span_safety_net::)'
- name: Test with latest nextest release
uses: actions-rs/cargo@v1
with:
command: nextest
args: run --all-features --retries 2
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
PERPLEXITY_API_KEY: ${{ secrets.PERPLEXITY_API_KEY }}
# `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@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
# Required to compile rig-lancedb
- name: Install Protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
# 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.
#
# Keep `--workspace`: it is also the only thing in CI that compiles
# `rig-core-macro-hygiene` (a bare root invocation selects just the `rig`
# package). A dedicated `cargo check -p` step would resolve rig-core with
# `default-features = false` and rebuild its whole tree; here feature
# unification makes the tripwire free.
- name: Run doctests
run: cargo test --doc --workspace --all-features
doc:
name: stable / doc
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust stable
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rust-docs
toolchain: ${{ env.RUST_VERSION }}
# Required to compile rig-lancedb
- name: Install Protoc
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
- name: Run cargo doc
run: cargo doc --no-deps --all-features
env:
RUSTDOCFLAGS: -D warnings