The Cargo Guide

CI/CD with Cargo

Why CI/CD with Cargo Deserves Its Own Design

Cargo works well for local development out of the box, but CI/CD adds different pressures: speed, repeatability, matrix coverage, artifact retention, publishing discipline, and supply-chain control. A useful mental model is:

  • local development optimizes for fast iteration
  • CI optimizes for trustworthy signal
  • CD and release pipelines optimize for reproducible delivery

That means a good Cargo CI/CD setup is not just cargo test in a shell script. It is a policy about what gets cached, what gets verified, what gets published, and under which constraints.

The Core Goals of a Cargo CI/CD Pipeline

Most Cargo CI/CD systems are trying to balance several goals at once:

  • fast feedback on ordinary changes
  • deterministic dependency resolution
  • good coverage across features, targets, and toolchains
  • reproducible release artifacts
  • safe publishing automation
  • strong dependency provenance and supply-chain hygiene

A useful mental model is:

  • CI is not one job
  • it is usually a layered set of jobs with different scopes and costs

A Small Example Project

Suppose you start with a small package:

cargo new ci_demo --lib
cd ci_demo

Manifest:

[package]
name = "ci_demo"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
 
[dependencies]
serde = { version = "1", optional = true, features = ["derive"] }
 
[features]
default = ["text"]
text = []
json = []
serde_support = ["dep:serde"]

Library code:

#[cfg(feature = "serde_support")]
use serde::Serialize;
 
#[cfg_attr(feature = "serde_support", derive(Serialize))]
pub struct Report {
    pub title: String
}
 
pub fn output_mode() -> &'static str {
    #[cfg(feature = "json")]
    {
        return "json";
    }
 
    #[cfg(not(feature = "json"))]
    {
        return "text";
    }
}

This crate is small, but it is enough to demonstrate feature matrices, docs, caching, and release checks.

The Fastest Useful CI Layer

A good first CI layer is usually a fast validation pass.

Typical commands:

cargo check --locked
cargo test --locked

A useful mental model is:

  • cargo check gives a fast structural correctness signal
  • cargo test gives behavioral confidence
  • --locked prevents dependency drift from silently changing CI meaning

Why Locked Builds Matter in CI

In CI, --locked is usually one of the most important flags.

Examples:

cargo build --locked
cargo test --locked
cargo doc --locked

A useful mental model is:

  • Cargo.toml defines allowed dependency ranges
  • Cargo.lock defines the exact graph CI is expected to use
  • --locked enforces that the CI run does not silently mutate that graph

Frozen Builds for Stricter Pipelines

For stricter environments, --frozen is often used.

Examples:

cargo build --frozen
cargo test --frozen

A useful mental model is:

  • --locked means do not change the lockfile
  • --offline means do not use the network
  • --frozen means both at once

This is especially useful in controlled release pipelines and hermetic build environments.

Caching Strategy at a High Level

Cargo CI performance depends heavily on caching. There are two especially important cache realms:

  • Cargo home content such as registry, git, and downloaded package caches
  • build artifacts in target/

A useful mental model is:

  • Cargo home caching reduces download and source-fetch cost
  • target caching reduces recompilation cost

The right balance depends on your CI platform, cache size limits, and how stable the project graph is.

Caching Cargo Home

Caching Cargo home is often one of the most effective ways to improve CI performance.

A shell-level pattern might look like this conceptually:

export CARGO_HOME="$CI_CACHE_DIR/cargo-home"
cargo fetch
cargo test --locked

This helps avoid repeated registry downloads and git dependency fetches across CI runs.

A useful mental model is:

  • Cargo home caches dependency source material
  • warm Cargo home means faster and more resilient CI jobs

Caching the Target Directory

Caching the target directory can also help, especially for repeated builds on similar keys.

Example:

export CARGO_TARGET_DIR="$CI_CACHE_DIR/target"
cargo check --locked
cargo test --locked

A useful mental model is:

  • target caching helps most when compiler version, target, feature set, and dependency graph are stable enough for reuse
  • unstable cache keys can make target caching large and noisy rather than helpful

A Practical Cache Key Mindset

A healthy cache-key strategy usually depends on a few important build inputs such as:

  • toolchain channel or exact version
  • target triple
  • Cargo.lock
  • sometimes feature set or build profile

A useful mental model is:

  • cache keys should change when reuse would become unsafe or misleading
  • cache keys should stay stable when reuse is genuinely useful

Why Cargo.lock Is a Strong Cache Input

Because Cargo.lock represents the resolved dependency graph, it is often one of the best cache-key inputs. If the lockfile changes, many cached dependency artifacts may no longer match the build meaningfully.

A practical CI principle is:

  • use the lockfile as part of cache identity
  • do not treat dependency graph drift as cache-irrelevant

cargo fetch as a CI Primitive

cargo fetch is a useful command in CI because it separates dependency acquisition from later build stages.

Example:

cargo fetch

A useful workflow is:

cargo fetch
cargo build --locked
cargo test --locked

This makes dependency availability a distinct step and helps with both performance and offline preparation.

Feature Matrix Testing

Once a crate has features, CI should usually stop pretending that the default feature set is the whole product.

Example commands:

cargo test --locked
cargo test --locked --no-default-features
cargo test --locked --features json
cargo test --locked --features "json serde_support"
cargo test --locked --all-features

A useful mental model is:

  • each supported feature combination is part of the supported build surface
  • CI should cover representative combinations, not only the default

Keeping Feature Matrices Tractable

Not every crate should test every possible feature subset in CI. The matrix can grow too quickly. A healthier strategy is to choose a small set of meaningful combinations.

A practical pattern is:

cargo test --locked
cargo test --locked --no-default-features
cargo test --locked --all-features

And then add a few important intermediate combinations when the crate's feature model really needs them.

A useful mental model is:

  • cover the edges and the main supported paths
  • do not let the matrix explode combinatorially without reason

Target Matrix Testing

Many crates also need CI coverage across targets.

Examples:

cargo build --locked --target x86_64-unknown-linux-gnu
cargo build --locked --target aarch64-unknown-linux-gnu
cargo test --locked --target x86_64-unknown-linux-gnu

A useful mental model is:

  • some targets may only need build validation
  • some targets also need execution validation through native hosts, runners, or emulators

Cross Testing and Emulation

Cross-building and cross-testing are different CI concerns.

A cross-build job might do:

cargo build --locked --target aarch64-unknown-linux-gnu

A cross-test job may also need a configured runner or emulator:

cargo test --locked --target aarch64-unknown-linux-gnu

A useful mental model is:

  • build matrices validate portability of compilation and linking
  • test matrices validate behavior under execution, which may require extra infrastructure

Toolchain Matrix Testing

CI often needs more than one toolchain role.

Examples:

cargo +stable test --locked
cargo +beta test --locked
cargo +nightly test --locked

A useful mental model is:

  • stable validates the mainstream supported path
  • beta gives early warning about upcoming changes
  • nightly is useful when the project intentionally relies on nightly-only capabilities or wants early ecosystem signal

MSRV Validation in CI

If the crate declares a minimum supported Rust version, CI may also validate that floor explicitly.

Manifest:

[package]
name = "ci_demo"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"

A practical CI idea is to ensure that at least one job tests the declared floor or a policy-defined minimum-compatible toolchain.

A useful mental model is:

  • rust-version is a support claim
  • CI is where that claim should be checked

Workspace-Wide CI Strategy

In workspaces, CI usually needs package selection strategy rather than a single universal command.

Examples:

cargo test --workspace --locked
cargo build --workspace --locked
cargo test -p core --locked
cargo test --workspace --exclude tools --locked

A useful mental model is:

  • full-workspace jobs are useful but expensive
  • targeted package jobs are often better for fast feedback and ownership isolation

A Layered Workspace Pattern

A practical workspace CI pattern often looks like this:

  • one fast job for common default members
  • targeted jobs for core libraries or critical binaries
  • a slower full-workspace job
  • release-oriented jobs only on versioned or tagged events

Illustrative commands:

cargo test --locked
cargo test -p core --all-features --locked
cargo test --workspace --locked

Documentation Builds in CI

Documentation should usually be part of CI, especially for published crates or public internal APIs.

Examples:

cargo doc --locked
cargo doc --workspace --locked
cargo test --doc --locked

A useful mental model is:

  • docs are part of the public interface
  • doctests validate examples shown to users
  • CI should catch documentation drift before release

Doctests vs Doc Generation

A useful distinction in CI is:

  • cargo doc checks whether docs build as artifacts
  • cargo test --doc checks whether documentation examples behave like valid code

A healthy documentation-oriented pipeline often uses both.

Linting and Maintenance Checks in CI

CI often includes formatting and lint jobs alongside build and test jobs.

Examples:

cargo fmt --check
cargo clippy --locked --all-targets --all-features

A useful mental model is:

  • formatting jobs keep code shape predictable
  • lint jobs keep style and correctness guidance visible
  • build and test jobs verify compilation and behavior

Artifact Management

CI/CD pipelines often need to preserve outputs from Cargo jobs rather than only their pass/fail status.

Common artifacts include:

  • release binaries
  • documentation output
  • test reports or converted diagnostic output
  • packaged crate archives

Useful commands may include:

cargo build --release --locked
cargo doc --locked
cargo package

A useful mental model is:

  • a Cargo job may produce both signal and deliverables
  • artifact retention policy should be deliberate

Release Artifacts

A release-oriented pipeline often centers around optimized artifacts.

Examples:

cargo build --release --locked
cargo test --release --locked

A useful mental model is:

  • fast CI paths validate quickly
  • release CI paths validate the actual shipping profile and artifact shape

Packaging Checks Before Publish

Before publish automation runs, the package should usually be verified as a package.

Examples:

cargo package
cargo package --list
cargo publish --dry-run

A useful mental model is:

  • publishing is built on packaging
  • package verification belongs in CI before any real registry upload

Publish Automation

Publish automation is usually safest when it is narrow, explicit, and separated from ordinary CI.

A typical publish-oriented sequence might look like this:

cargo test --locked
cargo doc --locked
cargo publish --dry-run
cargo publish

For alternate registries:

cargo publish --dry-run --registry company
cargo publish --registry company

A useful mental model is:

  • publish jobs are release jobs, not ordinary push-validation jobs
  • they should run only under intentional triggers such as tags or release branches

Registry Tokens and Publish Secrets

Publish automation usually needs registry credentials, and CI should inject those secrets at runtime rather than storing them in repository files.

Examples:

export CARGO_REGISTRY_TOKEN="$CRATES_IO_TOKEN"
cargo publish

Or for an alternate registry:

export CARGO_REGISTRIES_COMPANY_TOKEN="$COMPANY_REGISTRY_TOKEN"
cargo publish --registry company

A useful mental model is:

  • credentials are operational inputs
  • publish jobs should receive them only when needed

Reproducible Release Builds

A reproducible release pipeline usually combines several ideas:

  • explicit toolchain selection
  • locked or frozen dependency resolution
  • controlled caching or vendoring policy
  • explicit release profile builds
  • package verification before publication

A small reproducibility-oriented sequence might look like this:

cargo +stable build --release --frozen
cargo +stable test --release --frozen
cargo package

Supply-Chain Hygiene

Supply-chain hygiene in Cargo CI/CD is about reducing surprise and increasing traceability.

Common practices include:

  • committing and enforcing Cargo.lock where appropriate
  • using --locked or --frozen
  • preferring vendored or mirrored sources in controlled environments
  • limiting publish credentials to release jobs
  • reviewing dependency updates intentionally rather than incidentally

A useful mental model is:

  • every dependency and every credential is part of the delivery chain
  • CI/CD should make that chain more explicit, not less

Mirrors, Vendoring, and Controlled Environments

More controlled environments may go beyond lockfiles and use mirrors or vendoring.

Examples:

cargo vendor > .cargo/config.toml
cargo build --frozen

Or with a registry mirror configured in .cargo/config.toml:

[source.crates-io]
replace-with = "company-mirror"
 
[source.company-mirror]
registry = "sparse+https://mirror.example.com/index/"

A useful mental model is:

  • lockfiles control which versions are used
  • vendoring and mirrors control where those sources come from

A Small CI Script Example

A compact CI validation script might look like this:

#!/usr/bin/env sh
set -e
 
cargo fetch
cargo fmt --check
cargo clippy --locked --all-targets --all-features
cargo test --locked
cargo test --locked --all-features
cargo doc --locked

This is only one pattern, but it shows how caching preparation, formatting, linting, testing, and docs can be composed into one coherent CI stage.

A Small Release Script Example

A compact release-oriented script might look like this:

#!/usr/bin/env sh
set -e
 
cargo +stable test --locked
cargo +stable build --release --locked
cargo package
cargo publish --dry-run

And the final authenticated publish step can be isolated to a later job:

export CARGO_REGISTRY_TOKEN="$CRATES_IO_TOKEN"
cargo publish

A Matrix-Oriented CI Sketch

A conceptual matrix-oriented workflow might include combinations such as:

cargo +stable test --locked
cargo +beta test --locked
cargo test --locked --no-default-features
cargo test --locked --all-features
cargo build --locked --target aarch64-unknown-linux-gnu
cargo doc --locked

This pattern treats Cargo CI/CD as a set of orthogonal coverage dimensions rather than one monolithic job.

Common Beginner Mistakes

Mistake 1: running CI without --locked and then treating dependency drift as harmless.

Mistake 2: over-caching without thinking about cache-key stability or meaning.

Mistake 3: testing only the default feature set for a featureful crate.

Mistake 4: assuming cross-build coverage automatically implies cross-test coverage.

Mistake 5: publishing from ordinary CI jobs instead of narrow release jobs.

Mistake 6: forgetting that docs, package verification, and supply-chain policy are part of release quality too.

Hands-On Exercise

Take a small crate and design a two-layer CI workflow.

For a fast validation layer, use:

cargo fetch
cargo fmt --check
cargo clippy --locked --all-targets --all-features
cargo test --locked

For a broader confidence layer, use:

cargo test --locked --no-default-features
cargo test --locked --all-features
cargo doc --locked
cargo build --release --locked

Then ask which jobs truly need caching, which jobs truly need matrix expansion, and which jobs should be release-only. That exercise is one of the fastest ways to move from ad hoc Cargo CI to deliberate Cargo CI/CD design.

Mental Model Summary

A strong mental model for CI/CD with Cargo is:

  • CI is usually layered, not monolithic
  • --locked and sometimes --frozen are core tools for reproducible pipelines
  • caching strategy should distinguish Cargo home from target artifact reuse
  • feature, target, and toolchain matrices should be chosen deliberately rather than maximized blindly
  • docs, package verification, and release artifact generation are part of pipeline quality
  • publish automation should be isolated, authenticated narrowly, and preceded by dry-run verification
  • supply-chain hygiene is a build-policy concern, not just a security afterthought

Once this model is stable, Cargo CI/CD becomes much easier to treat as release engineering and operational design rather than as a pile of shell commands.