Skip to content

ci(msrv): enforce the declared rust-version = "1.86" - #302

Merged
argszero merged 1 commit into
mainfrom
ci/enforce-declared-msrv
Sep 26, 2026
Merged

argszero merged 1 commit into
mainfrom
ci/enforce-declared-msrv

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

.github/workflows/ci.yml installed dtolnay/rust-toolchain@stable and nothing else, while Cargo.toml:8 declares rust-version = "1.86" and CONTRIBUTING.md:7 states that declaration as the contract for contributors. Nothing in the repository ever checked it, so the declaration had no enforcer: every build and every test ran on the newest stable (1.98.0 today).

That is not hypothetical here. cargo clippy --all-targets -- -D warnings is a required check, and clippy's lints propose APIs that are newer than the declared MSRV. Measured on this tree: a build containing n.is_multiple_of(2) (unsigned_is_multiple_of, stable only since 1.87, rust-lang/rust#128101) is green on stable and red on 1.86 — a clippy-suggested fix can push production code past the line the manifest declares, and CI still passes.

This PR gives the declaration its first enforcer: a msrv job that installs the version the manifest declares and builds the crate with that toolchain.

Related Issue

None: no open issue covers this, and it is a self-contained change to one workflow file. (Recorded so the reviewer knows why no issue is linked and the linked-issue field stays empty.)

Changes

  • .github/workflows/ci.yml — new msrv job (a separate job, so it runs in parallel with ci and reports its own status check):
    • installs dtolnay/rust-toolchain@1.86, then runs cargo build --locked;
    • --locked pins the committed Cargo.lock: without it, a runner whose lockfile no longer matches Cargo.toml re-resolves in place and still builds, while the committed lockfile stays broken;
    • one step asserts the installed rustc really is the rust-version in Cargo.toml — bumping the declaration without bumping this job then fails loudly instead of silently re-creating the same gap.
  • No change to config or data structures; the existing ci job is untouched.

Tests

  • cargo test all pass — 396 passed / 0 failed (unchanged; this PR adds no Rust code).
  • cargo fmt --check passes (rc 0).
  • cargo clippy --all-targets -- -D warnings passes (rc 0).
  • New tests: not applicable — the change is the check; its verification is its own run, measured below.

The declared MSRV is true on this tree (measured, not assumed)

With 1.86.0 actually installed:

cargo build --locked          -> Finished `dev` profile (rc 0)
cargo check --all-targets     -> rc 0
cargo test                    -> 396 passed; 0 failed

and the two steps the new job runs:

Cargo.toml rust-version = 1.86; installed rustc = 1.86.0
guard: PASS
cargo build --locked          -> Finished `dev` profile (rc 0)

The new job has teeth

A/B in the same clone, with a temporary mutation of src/main.rs that uses <u32>::is_multiple_of (reverted afterwards; final tree is the single commit below):

toolchain job cargo build --locked
1.86 new msrv FAILED — error[E0658]: use of unstable library feature 'unsigned_is_multiple_of'
stable existing ci PASSED — Finished

The existing check is blind to exactly this class of regression; the new one is not.

The locked dependency graph is MSRV-safe

cargo metadata --locked lists 294 packages, 218 of which declare rust-version. Exactly one exceeds 1.86: wasip2 1.0.4+wasi-0.2.12 (declares 1.87.0). Its only inbound edge is target-gated — getrandom 0.3.4, cfg(all(target_arch = "wasm32", target_os = "wasi", target_env = "p2")) — so it is never built in this job. Cargo.toml has no [target.'cfg(...)'.dependencies] sections, and the versions are pinned by the committed lockfile, so the Linux runner builds the same graph that was measured here.

The action ref is valid

$ git ls-remote https://github.com/dtolnay/rust-toolchain.git 'refs/heads/1.86*'
6fd631f049d0fac46e290d1f958f752859eedcde	refs/heads/1.86
6fd631f049d0fac46e290d1f958f752859eedcde	refs/heads/1.86.0

dtolnay/rust-toolchain publishes one branch per Rust version (stable, beta, nightly are branches too); the repository has exactly one tag, v1.

Scope

This job builds production code, which is what rust-version promises downstream. cargo test also passes on 1.86 (396/0 above), so extending the job to --all-targets is a one-line change if you would rather cover test code too — say the word and I'll send it.

Checklist

  • Branch naming follows the convention (ci/, matching the existing ci(docker): commit in this repo)
  • Commit message uses Conventional Commits
  • Single responsibility, minimal change

Cargo.toml declares `rust-version = "1.86"` and CONTRIBUTING.md states it as the
contract for contributors, but the workflow only ever installed `stable`
(1.98 today) — so the declaration had no enforcer at all. This repo has already
felt the consequences: clippy suggests APIs that are newer than the declared
MSRV (e.g. `unsigned_is_multiple_of`, stable only since 1.87), and every such
suggestion is green on stable.

Add a `msrv` job that installs the toolchain the manifest declares and builds
with `--locked` against the committed Cargo.lock. A second step asserts that the
installed rustc really is the declared version, so bumping `rust-version`
without bumping this job fails loudly instead of silently re-creating the gap.

No production code changes; the existing `ci` job is untouched.
@argszero
argszero merged commit 2ce954f into main Sep 26, 2026
2 checks passed
@argszero
argszero deleted the ci/enforce-declared-msrv branch September 26, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant