Skip to content
compiler.dev

Rust GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a Cargo project. It installs the stable toolchain, caches dependencies and build artifacts with Swatinem/rust-cache, then checks formatting, runs clippy and executes the tests.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: "0"
  RUSTFLAGS: "-D warnings"

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - uses: Swatinem/rust-cache@v2

      - run: cargo fmt --all -- --check
      - run: cargo clippy --all-targets --all-features
      - run: cargo test --all-features

What the non-obvious parts do

  • dtolnay/rust-toolchain@stable installs a toolchain by the ref you choose. @stable tracks the stable release; use @1.85.0 style refs to pin an exact version.
  • Swatinem/rust-cache@v2 caches ~/.cargo and the target directory, keyed on Cargo.lock, the toolchain and manifest files. It also cleans out stale artifacts before saving so the cache does not grow forever. See Cargo caching in CI.
  • CARGO_INCREMENTAL: "0" turns off incremental compilation. It helps local rebuilds but makes CI caches larger and slower to restore.
  • RUSTFLAGS: "-D warnings" makes any compiler warning an error in CI. It also changes the compile flags, so cache entries from a build without it are not reused. Keep it set the same way everywhere.
  • cargo fmt --all -- --check fails if code is not formatted, without changing files.
  • --all-targets makes clippy also lint tests, examples and benches.
  • 30 minute timeout. Cold Rust builds are slow, so the cap is higher than in other templates.

Variants

Build matrix

For libraries, add a job with matrix.toolchain: [stable, beta, "1.80.0"] to check the minimum supported Rust version. See matrix builds.

Workspaces

Add --workspace to the clippy and test commands to cover every member crate. Use rust-cache's workspaces: input when the workspace root is not the repository root.

Database tests

Add a services: block as shown in the Django template, and pass DATABASE_URL.

Release builds

Add a job that runs cargo build --release and upload the binary as an artifact. Cross-compiling is easier with cross or cargo-zigbuild.

macOS or Windows

Add them to the matrix if you ship binaries for them.

Run it on faster machines

Rust compile times scale with core count, so this is one of the stacks that gains most. Change one line:

    runs-on: [self-hosted, Linux, compiler-ci]

The job runs on a fresh 8 vCPU machine from compiler.dev, with automatic fallback. In compiler.dev's benchmark a React and Rust (Axum) app went from 2m37s to 1m45s; see benchmarks. Tuning tips are in speed up Rust and Cargo on GitHub Actions, and prices are on pricing.

FAQ

Do I need sccache as well?

Not at first. rust-cache handles dependencies. sccache helps when many projects share compiled code.

Why does my cache keep growing?

rust-cache prunes old artifacts, but only for its own cache. Check cache limits if you hit the repository cap.

Should clippy fail the build?

Yes on main. On pull requests, it keeps style debt from piling up.

Can I cache across branches?

Branches can restore caches from the default branch. Make sure main runs the workflow so it saves one.