Continuous Integration for Rust With GitHub Actions
A GitHub Actions workflow for Rust that formats, lints, tests and builds, with a pinned toolchain, dependency caching, a matrix and the failure modes to avoid.

The first CI for a Rust project fails in the same three ways: the compiler version moves, nothing is cached so every run compiles the dependency tree, and warnings accumulate because nothing denies them. A short workflow fixes all three. This post gives one that runs in a few minutes for a typical service. It is the CI step of shipping Rust to production.
GitHub Actions, Rust 1.90.
The workflow
name: ci
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/[email protected]
with:
components: rustfmt
- run: cargo fmt --all -- --check
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/[email protected]
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets --all-features --locked -- -D warnings
- run: cargo test --all-features --locked
- run: cargo build --release --lockedfmt is a separate job: it needs no dependencies, so it fails in seconds. dtolnay/[email protected] installs that exact version; the ref of the action is the toolchain. RUSTFLAGS: "-D warnings" turns compiler warnings into errors for the whole run. Note that setting RUSTFLAGS changes the cache key, and builds with different flags do not share artifacts.
What the cache does
Swatinem/rust-cache stores ~/.cargo and target between runs, keyed by Cargo.lock, the compiler version and the job. The first run is cold; later runs reuse the compiled dependencies, and only your crate rebuilds. The details and the limits are in caching Cargo builds in CI. Do not cache on pull requests from forks with secrets, and prune: caches that grow without limit slow the restore.
A matrix for portability
matrix:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --lockedUse it for libraries and tools that ship on several platforms. Services that only run on Linux do not need it. fail-fast: false lets the other platforms finish, which shows whether the failure is specific to one.
Checking the minimum Rust version
A library that promises rust-version = "1.80" should test it:
- uses: dtolnay/[email protected]
- run: cargo check --locked --all-featuresSee declaring and testing the MSRV.
Extras worth adding
cargo deny checkandcargo auditon a schedule, as in Cargo Denycargo doc --no-depswithRUSTDOCFLAGS="-D warnings"to catch broken links- A scheduled run on
stableandbetaso a compiler change shows up before you need to upgrade
Pitfalls
- Using
actions-rs/*, which is unmaintained; thedtolnay/rust-toolchainaction replaces its toolchain step - Tests that need a database: start it as a service container and wait for readiness
- Secrets in pull requests from forks, which are not available, so tests that need them fail
- Floating
@stabletoolchains in the main job
Limits
CI time is dominated by compilation, so the next gains come from a faster linker and fewer features, not YAML changes. The actions are documented in their repositories: dtolnay/rust-toolchain and Swatinem/rust-cache.


