Recipe
Set up CI for a Rust project
Use when: a Rust repository has more than one contributor, or ships to more than one platform
Goal: a pipeline that fails any change that is unformatted, warns, fails a test on any target OS, uses an unreviewed dependency version, or has a known advisory
Taught in CI pipelines
Quick recipe
-
1
Make sure the lock file is in the repository, for every binary crate.
Example
dockrulesaddsanyhow = "1"toCargo.toml, but the lock file was made before.cargo test --lockedstops witherror: cannot update the lock file ... because --locked was passed. Runcargo buildon your own machine, then commitCargo.tomlandCargo.locktogether.Why:
--lockedcan only protect versions that were committed. -
2
cargo fmt --all --check, thencargo clippy --all-targets --locked -- -D warnings, in one job.Example
A push has
pub fn is_full(bikes:u32,capacity:u32)->bool{ bikes==capacity }. Thecheckjob'scargo fmt --all --checkprints the diff and fails. Because thetestjob saysneeds: check, the three test runs never start.Why: formatting and lints fail in seconds and stop the expensive jobs from running on a doomed change.
-
3
A matrix running
cargo test --lockedon Linux, Windows and macOS, after the checks pass.Example
The
testjob listsos: [ubuntu-latest, windows-latest, macos-latest]undermatrix:, socargo test --lockedruns three times. A test of a path built withformat!("{dir}/{name}")can pass on Linux and fail on the Windows run.Why: path separators, line endings and C runtimes differ, and only the real OS reveals it.
-
4
cargo audit(orcargo deny check) in its own job.Example
The
auditjob runscargo install cargo-audit --locked, thencargo audit. It has noneeds:, so it runs alongside the others. If an advisory is published for a crate version already in the lock, the next push turns this job red, even if that push changed no dependency.Why: an advisory can appear for a version you already use, without any change to your code.
-
5
Add a Cargo cache keyed on
Cargo.lock, and a separate workflow onv*tags that builds--release --lockedper target.Example
The
checkandtestjobs each have- uses: Swatinem/rust-cache@v2right after the toolchain step. The release workflow starts withon: push: tags: ['v*']. Runninggit tag v0.4.0andgit push origin v0.4.0starts it, and it runscargo build --release --lockedonce per target.Why: caching keeps runs fast; building releases from the same locked versions means users get what was tested.