Skip to content

Commit e41d8ac

Browse files
lwshangclaude
andauthored
ci: track stable in rust-toolchain.toml, enforce MSRV in its own job (#741)
* ci: track stable in rust-toolchain.toml, enforce MSRV in its own job rust-toolchain.toml was pinned to the MSRV (1.88.0), so every local build, rust-analyzer, clippy and every CI job used a compiler nine releases behind stable. That makes the repo progressively harder to work in as the MSRV ages (rust-analyzer now refuses sufficiently old toolchains) and hides new rustc/clippy diagnostics until the MSRV is bumped. Split the two concerns, matching the setup in dfinity/cdk-rs: - rust-toolchain.toml tracks `stable`. - `rust-version` in the workspace Cargo.toml stays at 1.88.0 and becomes the single source of truth for the MSRV. - A new `msrv` job in test.yml parses `rust-version` out of Cargo.toml and builds with that toolchain, so bumping the MSRV is a one-line change and cannot drift out of sync with CI. The msrv job covers the published crates only (ref-tests and ic-utils-bindgen-tests depend on pocket-ic from the IC monorepo, whose MSRV runs far ahead of ours) and builds without --all-targets: the promise is that consumers can build the libraries at the MSRV, not that our test suite runs there. It gates on the existing `changes` filter and reports through the existing `test:required` aggregate, so no branch-protection changes are needed. Verified before landing: cargo fmt, `cargo hack clippy --each-feature`, `clippy --all-targets --all-features -D warnings`, the wasm clippy target and `RUSTDOCFLAGS=-Dwarnings cargo doc` all pass on stable 1.97, and the new msrv commands pass on 1.88.0. No source changes were needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ic-agent): make the wasm test mock hit-counter atomic, skip wasm doctests Moving rust-toolchain.toml to `stable` surfaced two latent problems in the WASM job. Both are test-harness issues; no library code changes. 1. `agent::agent_test::no_cert` failed with "some mocked routes were never hit". The service-worker mock counts hits with a read-modify-write across two IndexedDB transactions: getMock reads the whole record, the handler mutates its copy, setMock writes it back. `Agent::query` issues its `query` and its `read_state` concurrently via `try_join!`, so both handlers read the pre-state and the second write discards the first one's increment. `no_cert` is the only test that combines concurrent requests with assert_mock (which requires *every* route to be hit), so it is the only one that noticed. Confirmed rather than inferred: the counter for the `query` route reads 0 even though the test's MissingSignature assertion — which requires the query response to have been served — passes. The request happened; only its increment was lost. Fixed by serializing the service worker's handlers; the responses are canned data, so there is nothing to gain from overlapping them. Locally: fails 2/2 on stable 1.97 and passes 2/2 on 1.88 before the fix, passes 3/3 on both after. assert_mock now prints the hit map on failure. The original assertion gave no indication of which route was missed. 2. Newer toolchains run doctests for wasm targets, which 1.88 skipped. ic-agent's doctests use `#[tokio::main]` and tokio is deliberately a dev-dependency only under cfg(not(target_family = "wasm")), so they cannot compile there: "cannot find module or crate `tokio`". Pass --lib so the browser run covers the #[wasm_bindgen_test] tests it is meant to cover; the host `test` job already runs the doctests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 403dc15 commit e41d8ac

6 files changed

Lines changed: 149 additions & 12 deletions

File tree

.github/workflows/test.yml

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,70 @@ jobs:
4848
- '**'
4949
- '!**.md'
5050
51+
# Everything else in CI runs on `stable` (see rust-toolchain.toml), so the MSRV
52+
# promised to downstream users needs its own gate. This job is the only place
53+
# the MSRV toolchain is used; it reads the version from the workspace
54+
# Cargo.toml so `rust-version` stays the single source of truth.
55+
#
56+
# Scope: build only, no `--all-targets`. The promise is that consumers can
57+
# *build* the published crates with the MSRV, not that our test suite runs
58+
# there — dev-dependencies are free to require a newer compiler.
59+
#
60+
# Linux-only: MSRV regressions almost always come from a dependency raising its
61+
# own `rust-version`, which is platform-independent. Running the full OS matrix
62+
# would triple the cost to catch only platform-gated regressions (e.g. a
63+
# windows-sys bump), which the stable `test` matrix would surface anyway once
64+
# the dependency reaches a release we build.
65+
msrv:
66+
name: MSRV build
67+
needs: changes
68+
if: needs.changes.outputs.src == 'true'
69+
runs-on: ubuntu-latest
70+
71+
steps:
72+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
73+
74+
- name: Read MSRV from Cargo.toml
75+
id: msrv
76+
run: |
77+
set -euo pipefail
78+
version=$(awk -F'"' '/^rust-version *=/ { print $2; exit }' Cargo.toml)
79+
if [ -z "$version" ]; then
80+
echo "::error file=Cargo.toml::could not parse rust-version"
81+
exit 1
82+
fi
83+
echo "Detected MSRV: $version"
84+
echo "version=$version" >> "$GITHUB_OUTPUT"
85+
86+
- name: Remove the runner's bundled Rust toolchain
87+
run: rustup toolchain remove stable 2>/dev/null || true
88+
89+
- uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
90+
with:
91+
toolchain: ${{ steps.msrv.outputs.version }}
92+
target: wasm32-unknown-unknown
93+
cache-shared-key: ${{ runner.os }}-msrv
94+
cache-bin: false
95+
rustflags: ""
96+
97+
# ref-tests and ic-utils-bindgen-tests are the only `publish = false`
98+
# members; they depend on pocket-ic from the IC monorepo, whose MSRV runs
99+
# far ahead of ours. Excluding (rather than listing the published crates)
100+
# keeps a newly added published crate covered by default.
101+
- name: Build with MSRV
102+
run: |
103+
cargo build --locked --workspace \
104+
--exclude ref-tests --exclude ic-utils-bindgen-tests
105+
cargo build --locked --workspace \
106+
--exclude ref-tests --exclude ic-utils-bindgen-tests --all-features
107+
108+
# Browser consumers build ic-agent for wasm at the MSRV too; mirrors the
109+
# WASM step in lint.yml.
110+
- name: Build with MSRV (WASM)
111+
run: |
112+
CARGO_TARGET_DIR=target/wasm cargo build --locked --target wasm32-unknown-unknown \
113+
-p ic-agent --features wasm-bindgen -p ic-utils
114+
51115
# Workspace tests for every crate except ref-tests, on all three OSes. Because
52116
# each crate is tested from its own directory, the heavy pocket-ic dependency
53117
# (only used by ref-tests) never compiles here, keeping this job's cache small.
@@ -199,17 +263,27 @@ jobs:
199263
200264
# CARGO_TARGET_DIR=target/wasm keeps wasm artifacts under ./target, so
201265
# rust-cache (which caches ./target) still picks them up.
266+
#
267+
# --lib restricts this to the lib target's #[wasm_bindgen_test] tests. Newer
268+
# toolchains also run doctests for wasm targets (1.88 skipped them), and
269+
# ic-agent's doctests cannot compile there: they use #[tokio::main], and
270+
# tokio is deliberately a dev-dependency only under
271+
# cfg(not(target_family = "wasm")). Doctests are covered on the host by the
272+
# `test` job above; running them inside a headless browser adds nothing.
202273
- name: Run Tests (WASM)
203-
run: CARGO_TARGET_DIR=target/wasm wasm-pack test --chrome --headless ic-agent --features wasm-bindgen
274+
run: CARGO_TARGET_DIR=target/wasm wasm-pack test --chrome --headless ic-agent --features wasm-bindgen --lib
204275

205276
aggregate:
206277
name: test:required
207278
# Runs only when the test jobs ran; on docs-only changes this skips, and a
208279
# skipped required check counts as passing for branch protection.
209280
if: always() && needs.changes.outputs.src == 'true'
210281
runs-on: ubuntu-latest
211-
needs: [changes, test, ref_tests, wasm]
282+
needs: [changes, msrv, test, ref_tests, wasm]
212283
steps:
284+
- name: Check MSRV result
285+
if: ${{ needs.msrv.result != 'success' }}
286+
run: exit 1
213287
- name: Check test result
214288
if: ${{ needs.test.result != 'success' }}
215289
run: exit 1

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ version = "0.49.2"
1717
authors = ["DFINITY Stiftung <sdk@dfinity.org>"]
1818
edition = "2021"
1919
repository = "https://github.com/dfinity/agent-rs"
20-
# MSRV
21-
# Avoid updating this field unless we use new Rust features
22-
# Sync rust-version in rust-toolchain.toml
20+
# MSRV — the single source of truth. The `msrv` job in
21+
# .github/workflows/test.yml parses this line to pick its toolchain, so bumping
22+
# the MSRV is a one-line change here.
23+
#
24+
# Avoid updating this field unless we need new Rust features, or a dependency
25+
# forces it. It is a compatibility promise to downstream users: raise it only in
26+
# a minor (not patch) release, and note it in CHANGELOG.md.
27+
#
28+
# rust-toolchain.toml is intentionally NOT pinned to this version; it tracks
29+
# stable.
2330
rust-version = "1.88.0"
2431
license = "Apache-2.0"
2532

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,31 @@ We use `cargo` to build this repo. Make sure you have rust stable installed. To
1313
cargo build
1414
```
1515

16+
### Rust toolchain and MSRV
17+
Two different Rust versions are in play, and they are deliberately decoupled:
18+
19+
| | Where | Value |
20+
|---|---|---|
21+
| Development toolchain | `rust-toolchain.toml` | `stable` |
22+
| MSRV | `rust-version` in the workspace `Cargo.toml` | pinned |
23+
24+
Development, `cargo fmt`, `cargo clippy` and all CI jobs except one use the
25+
latest **stable** release. The **MSRV** is the oldest compiler the published
26+
crates are guaranteed to build with, and is enforced by the `msrv` job in
27+
[`.github/workflows/test.yml`](.github/workflows/test.yml), which reads the
28+
version out of `Cargo.toml`. That job builds the published crates only — the
29+
MSRV covers building the libraries, not running our test suite, so
30+
dev-dependencies may require a newer compiler.
31+
32+
Consequences worth knowing:
33+
34+
* Bumping the MSRV is a one-line change to `rust-version`. Do it only when we
35+
need newer Rust features or a dependency forces it, in a minor (not patch)
36+
release, with a CHANGELOG entry.
37+
* Because clippy tracks stable, a new Rust release can introduce lints that fail
38+
CI on an otherwise untouched branch. The fix is a small lint-cleanup PR, not an
39+
MSRV or toolchain change.
40+
1641
## Testing
1742
There are two suites of tests that can be executed from this repo; the regular cargo tests and
1843
the ic-ref tests. In order to run the ic-ref tests, you will need a running local reference

ic-agent/http_mock_service_worker.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,31 @@ async function getMock(nonce) {
3838
});
3939
}
4040

41+
// Handle one request at a time.
42+
//
43+
// Both the `hits` counter and the route list are read-modify-write cycles over a
44+
// single IndexedDB record: `getMock` reads the whole record in one transaction,
45+
// the handler mutates its copy, and `setMock` writes the whole record back in
46+
// another. Two requests in flight against the same mock therefore both read the
47+
// pre-state and the second write silently discards the first one's mutation.
48+
//
49+
// That is not hypothetical: `Agent::query` issues its `query` and its
50+
// `read_state` concurrently via `try_join!`, so a certifying-agent test reliably
51+
// has two overlapping requests and can lose one of the two hit increments. There
52+
// is nothing to gain from serving these concurrently — the responses come from
53+
// canned data — so serialize the handlers and keep each cycle atomic.
54+
let tail = Promise.resolve();
55+
function serialized(fn) {
56+
const run = tail.then(fn);
57+
// Keep the chain alive regardless of how this handler settles.
58+
tail = run.then(() => {}, () => {});
59+
return run;
60+
}
61+
4162
// Status codes are chosen to avoid being picked up as successes by tests expecting a 404 or 500.
4263

4364
self.addEventListener("fetch", (event) => {
44-
event.respondWith((async () => {
65+
event.respondWith(serialized(async () => {
4566
try {
4667
const request = event.request;
4768
const url = new URL(request.url);
@@ -83,7 +104,7 @@ self.addEventListener("fetch", (event) => {
83104
} catch (e) {
84105
return new Response(e.toString(), { status: 503 });
85106
}
86-
})())
107+
}));
87108
});
88109

89110
self.addEventListener("activate", (event) => {

ic-agent/src/agent/agent_test.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,10 @@ mod mock {
878878

879879
pub async fn assert_mock(nonce: String) {
880880
let hits = get_hits(&nonce).await;
881-
assert!(hits.values().all(|x| *x > 0));
881+
assert!(
882+
hits.values().all(|x| *x > 0),
883+
"some mocked routes were never hit: {hits:?}"
884+
);
882885
}
883886

884887
pub async fn assert_single_mock(method: &str, path: &str, nonce: &String) {

rust-toolchain.toml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
[toolchain]
2-
# MSRV
3-
# Avoid updating this field unless we use new Rust features
4-
# Sync rust-version in workspace Cargo.toml
5-
channel = "1.88.0"
2+
# Development toolchain — NOT the MSRV.
3+
#
4+
# We deliberately track the latest stable release here so that local builds,
5+
# rust-analyzer, clippy and rustfmt all use a current compiler. Pinning this to
6+
# the MSRV instead makes the repo progressively harder to work in as the MSRV
7+
# ages (rust-analyzer refuses to work with sufficiently old toolchains) and
8+
# hides new clippy/rustc diagnostics until the MSRV is bumped.
9+
#
10+
# The MSRV is `rust-version` in the workspace Cargo.toml, and is enforced by the
11+
# `msrv` job in .github/workflows/test.yml, which reads it from there.
12+
channel = "stable"
613
components = ["rustfmt", "clippy"]
714
targets = ["wasm32-unknown-unknown"]

0 commit comments

Comments
 (0)