Putting It All Together — A Production CI/CD Pipeline 🟡
全部整合:生产级 CI/CD 流水线 🟡
What you’ll learn:
本章将学到什么:
- Structuring a multi-stage GitHub Actions CI workflow (check → test → coverage → security → cross → release)
如何组织多阶段 GitHub Actions CI 流程:check → test → coverage → security → cross → release- Caching strategies with
rust-cacheandsave-iftuning
如何用rust-cache和save-if做缓存调优- Running Miri and sanitizers on a nightly schedule
如何通过 nightly 定时任务运行 Miri 和 sanitizer- Task automation with
Makefile.tomland pre-commit hooks
如何用Makefile.toml和 pre-commit hook 自动化任务- Automated releases with
cargo-dist
如何用cargo-dist自动产出发布包Cross-references: Build Scripts · Cross-Compilation · Benchmarking · Coverage · Miri/Sanitizers · Dependencies · Release Profiles · Compile-Time Tools ·
no_std· Windows
交叉阅读: 这一章基本把前面 1 到 10 章的内容全串起来了:构建脚本、交叉编译、benchmark、覆盖率、Miri 与 sanitizer、依赖治理、发布配置、编译期工具、no_std和 Windows 支持,都会在这里汇总成一条完整流水线。
Individual tools are useful. A pipeline that orchestrates them automatically on every push is transformative. This chapter assembles the tools from chapters 1–10 into a cohesive CI/CD workflow.
单个工具当然有用,但真正产生质变的是:每次推送都能自动把这些工具串起来跑一遍的流水线。本章就是把前面 1 到 10 章的工具整合成一套完整的 CI/CD 体系。
The Complete GitHub Actions Workflow
完整的 GitHub Actions 工作流
A single workflow file that runs all verification stages in parallel:
下面是一份单文件工作流,它会把各个验证阶段拆开并行跑。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_ENCODED_RUSTFLAGS: "-Dwarnings" # Treat warnings as errors (top-level crate only)
# NOTE: Unlike RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS does not affect build scripts
# or proc-macros, which avoids false failures from third-party warnings.
# Use RUSTFLAGS="-Dwarnings" instead if you want to enforce on build scripts too.
jobs:
# ─── Stage 1: Fast feedback (< 2 min) ───
check:
name: Check + Clippy + Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 # Cache dependencies
- name: Check compilation
run: cargo check --workspace --all-targets --all-features
- name: Check Cargo.lock
run: cargo fetch --locked
- name: Check doc
run: RUSTDOCFLAGS='-Dwarnings' cargo doc --workspace --all-features --no-deps
- name: Clippy lints
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Formatting
run: cargo fmt --all -- --check
# ─── Stage 2: Tests (< 5 min) ───
test:
name: Test (${{ matrix.os }})
needs: check
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run tests
run: cargo test --workspace
- name: Run doc tests
run: cargo test --workspace --doc
# ─── Stage 3: Cross-compilation (< 10 min) ───
cross:
name: Cross (${{ matrix.target }})
needs: check
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
use_cross: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl-tools
if: contains(matrix.target, 'musl')
run: sudo apt-get install -y musl-tools
- name: Install cross
if: matrix.use_cross
uses: taiki-e/install-action@cross
- name: Build (native)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- name: Build (cross)
if: matrix.use_cross
run: cross build --release --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/diag_tool
# ─── Stage 4: Coverage (< 10 min) ───
coverage:
name: Code Coverage
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Enforce minimum coverage
run: cargo llvm-cov --workspace --fail-under-lines 75
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
# ─── Stage 5: Safety verification (< 15 min) ───
miri:
name: Miri
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- name: Run Miri
run: cargo miri test --workspace
env:
MIRIFLAGS: "-Zmiri-backtrace=full"
# ─── Stage 6: Benchmarks (PR only, < 10 min) ───
bench:
name: Benchmarks
if: github.event_name == 'pull_request'
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run benchmarks
run: cargo bench -- --output-format bencher | tee bench.txt
- name: Compare with baseline
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'cargo'
output-file-path: bench.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
alert-threshold: '115%'
comment-on-alert: true
Pipeline execution flow:
流水线执行结构:
┌─────────┐
│ check │ ← clippy + fmt + cargo check (2 min)
└────┬────┘
┌─────────┬──┴──┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌────────┐ ┌──────┐ ┌──────┐
│ test │ │cross │ │coverage│ │ miri │ │bench │
│ (2×) │ │ (2×) │ │ │ │ │ │(PR) │
└──────┘ └──────┘ └────────┘ └──────┘ └──────┘
3 min 8 min 8 min 12 min 5 min
Total wall-clock: ~14 min (parallel after check gate)
The total wall-clock time is around 14 minutes because everything after check runs in parallel.
整条流水线的总墙钟时间大约是 14 分钟,原因很简单:check 之后的阶段都在并行执行。
CI Caching Strategies
CI 缓存策略
Swatinem/rust-cache@v2 is the standard Rust CI cache action. It caches ~/.cargo and target/ between runs, but large workspaces need tuning:Swatinem/rust-cache@v2 基本就是 Rust CI 缓存的标准动作。它会缓存 ~/.cargo 和 target/,不过工程一大,参数就得认真调。
# Basic (what we use above)
- uses: Swatinem/rust-cache@v2
# Tuned for a large workspace:
- uses: Swatinem/rust-cache@v2
with:
# Separate caches per job — prevents test artifacts bloating build cache
prefix-key: "v1-rust"
key: ${{ matrix.os }}-${{ matrix.target || 'default' }}
# Only save cache on main branch (PRs read but don't write)
save-if: ${{ github.ref == 'refs/heads/main' }}
# Cache Cargo registry + git checkouts + target dir
cache-targets: true
cache-all-crates: true
Cache invalidation gotchas:
缓存失效与污染的常见坑:
| Problem 问题 | Fix 处理方式 |
|---|---|
| Cache grows unbounded (>5 GB) 缓存越滚越大,超过 5 GB | Set prefix-key: "v2-rust" to force fresh cache升级 prefix-key,强制切新缓存。 |
| Different features pollute cache 不同 feature 共用缓存,互相污染 | Use key: ${{ hashFiles('**/Cargo.lock') }}把 key 跟锁文件绑定。 |
| PR cache overwrites main PR 把主分支缓存覆盖了 | Set save-if: ${{ github.ref == 'refs/heads/main' }}只允许主分支写缓存。 |
| Cross-compilation targets bloat 交叉编译目标把缓存撑胖 | Use separate key per target triple按 target triple 拆 key。 |
Sharing cache between jobs:
多任务之间怎么共享缓存:
The check job saves the cache; downstream jobs such as test、cross、coverage read it. With save-if limited to main, PRs can consume cache without writing stale results back.check 任务负责把缓存写出来,下游的 test、cross、coverage 直接读它。再配合 save-if 只让 main 写缓存,就能避免 PR 跑出来一堆过时内容把缓存污染回去。
Measured impact on large workspace: Cold build ~4 min → cached build ~45 sec. The cache action alone can save a huge chunk of CI wall-clock time.
在大型 workspace 里的实际收益 往往很夸张:冷构建约 4 分钟,热缓存后可能缩到 45 秒左右。光缓存这一项,就足够给整条流水线省下一大截时间。
Makefile.toml with cargo-make
用 cargo-make 管理 Makefile.toml
cargo-make provides a portable task runner that works across platforms, unlike传统 make:cargo-make 提供的是一个跨平台任务运行器,不像传统 make 那么依赖系统环境。
# Install
cargo install cargo-make
# Makefile.toml — at workspace root
[config]
default_to_workspace = false
# ─── Developer workflows ───
[tasks.dev]
description = "Full local verification (same checks as CI)"
dependencies = ["check", "test", "clippy", "fmt-check"]
[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--all-targets"]
[tasks.test]
command = "cargo"
args = ["test", "--workspace"]
[tasks.clippy]
command = "cargo"
args = ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]
[tasks.fmt]
command = "cargo"
args = ["fmt", "--all"]
[tasks.fmt-check]
command = "cargo"
args = ["fmt", "--all", "--", "--check"]
# ─── Coverage ───
[tasks.coverage]
description = "Generate HTML coverage report"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--html", "--open"]
[tasks.coverage-ci]
description = "Generate LCOV for CI upload"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--lcov", "--output-path", "lcov.info"]
# ─── Benchmarks ───
[tasks.bench]
description = "Run all benchmarks"
command = "cargo"
args = ["bench"]
# ─── Cross-compilation ───
[tasks.build-musl]
description = "Build static binary (musl)"
command = "cargo"
args = ["build", "--release", "--target", "x86_64-unknown-linux-musl"]
[tasks.build-arm]
description = "Build for aarch64 (requires cross)"
command = "cross"
args = ["build", "--release", "--target", "aarch64-unknown-linux-gnu"]
[tasks.build-all]
description = "Build for all deployment targets"
dependencies = ["build-musl", "build-arm"]
# ─── Safety verification ───
[tasks.miri]
description = "Run Miri on all tests"
toolchain = "nightly"
command = "cargo"
args = ["miri", "test", "--workspace"]
[tasks.audit]
description = "Check for known vulnerabilities"
install_crate = "cargo-audit"
command = "cargo"
args = ["audit"]
# ─── Release ───
[tasks.release-dry]
description = "Preview what cargo-release would do"
install_crate = "cargo-release"
command = "cargo"
args = ["release", "--workspace", "--dry-run"]
Usage:
常见用法:
# Equivalent of CI pipeline, locally
cargo make dev
# Generate and view coverage
cargo make coverage
# Build for all targets
cargo make build-all
# Run safety checks
cargo make miri
# Check for vulnerabilities
cargo make audit
Pre-Commit Hooks: Custom Scripts and cargo-husky
Pre-commit hook:自定义脚本与 cargo-husky
Catch issues before they reach CI. The simplest and most transparent approach is a custom git hook:
很多问题完全可以在推到 CI 之前就拦下来。最简单、也最透明的方式,就是自己写一个 git hook。
#!/bin/sh
# .githooks/pre-commit
set -e
echo "=== Pre-commit checks ==="
# Fast checks first
echo "→ cargo fmt --check"
cargo fmt --all -- --check
echo "→ cargo check"
cargo check --workspace --all-targets
echo "→ cargo clippy"
cargo clippy --workspace --all-targets -- -D warnings
echo "→ cargo test (lib only, fast)"
cargo test --workspace --lib
echo "=== All checks passed ==="
# Install the hook
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
Alternative: cargo-husky (auto-installs hooks via build script):
替代方案:cargo-husky,它会通过构建脚本自动装 hook。
⚠️ Note:
cargo-huskyhas not been updated since 2022. It still works but is effectively unmaintained. Consider the custom hook approach above for new projects.
⚠️ 注意:cargo-husky从 2022 年之后就几乎没怎么更新了,虽然还能用,但已经接近无人维护。新项目更建议走上面的自定义 hook 路线。
cargo install cargo-husky
# Cargo.toml — add to dev-dependencies of root crate
[dev-dependencies]
cargo-husky = { version = "1", default-features = false, features = [
"precommit-hook",
"run-cargo-check",
"run-cargo-clippy",
"run-cargo-fmt",
"run-cargo-test",
] }
Release Workflow: cargo-release and cargo-dist
发布流程:cargo-release 与 cargo-dist
cargo-release — automates version bumping, tagging, and publishing:cargo-release 负责自动版本提升、打 tag 和发布。
# Install
cargo install cargo-release
# release.toml — at workspace root
[workspace]
consolidate-commits = true
pre-release-commit-message = "chore: release {{version}}"
tag-message = "v{{version}}"
tag-name = "v{{version}}"
# Don't publish internal crates
[[package]]
name = "core_lib"
release = false
[[package]]
name = "diag_framework"
release = false
# Only publish the main binary
[[package]]
name = "diag_tool"
release = true
# Preview release
cargo release patch --dry-run
# Execute release (bumps version, commits, tags, optionally publishes)
cargo release patch --execute
# 0.1.0 → 0.1.1
cargo release minor --execute
# 0.1.1 → 0.2.0
cargo-dist — generates downloadable release binaries for GitHub Releases:cargo-dist 负责给 GitHub Releases 生成可下载的发布产物。
# Install
cargo install cargo-dist
# Initialize (creates CI workflow + metadata)
cargo dist init
# Preview what would be built
cargo dist plan
# Generate the release (usually done by CI on tag push)
cargo dist build
# Cargo.toml additions from `cargo dist init`
[workspace.metadata.dist]
cargo-dist-version = "0.28.0"
ci = "github"
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]
install-path = "CARGO_HOME"
This generates a GitHub Actions workflow that, on tag push:
它会生成一条在 tag push 时自动触发的工作流,通常会做这些事:
- Builds the binary for all target platforms
1. 为所有目标平台构建二进制。 - Creates a GitHub Release with downloadable
.tar.gz/.ziparchives
2. 创建 GitHub Release,并附上可下载的.tar.gz或.zip包。 - Generates shell/PowerShell installer scripts
3. 生成 shell 与 PowerShell 安装脚本。 - Publishes to crates.io (if configured)
4. 如果配置了,还能顺手发布到 crates.io。
Try It Yourself — Capstone Exercise
动手试一试:综合练习
This exercise ties together every chapter. You will build a complete engineering pipeline for a fresh Rust workspace:
这个练习会把整本书前面的内容全串起来。目标是给一个全新的 Rust workspace 搭一条完整工程流水线。
-
Create a new workspace with two crates: a library (
core_lib) and a binary (cli). Add abuild.rsthat embeds the git hash and build timestamp usingSOURCE_DATE_EPOCH.
1. 新建 workspace,包含一个库core_lib和一个二进制cli。补一个build.rs,用SOURCE_DATE_EPOCH把 git hash 和构建时间嵌进产物。 -
Set up cross-compilation for
x86_64-unknown-linux-muslandaarch64-unknown-linux-gnu. Verify both targets build withcargo zigbuildorcross.
2. 配置交叉编译,支持x86_64-unknown-linux-musl与aarch64-unknown-linux-gnu,并用cargo zigbuild或cross验证两边都能编过。 -
Add a benchmark using Criterion or Divan for a function in
core_lib. Run it locally and record a baseline.
3. 补一个 benchmark,给core_lib里的函数用 Criterion 或 Divan 做基准测试,并记录基线结果。 -
Measure code coverage with
cargo llvm-cov. Set a minimum threshold of 80% and verify it passes.
4. 测代码覆盖率,用cargo llvm-cov,把阈值设成 80%,确认它能通过。 -
Run
cargo +nightly careful testandcargo miri test. Add a test that exercisesunsafecode if present.
5. 运行cargo +nightly careful test和cargo miri test。如果代码里有unsafe,补一个覆盖它的测试。 -
Configure
cargo-denywith adeny.tomlthat bansopenssland enforces MIT/Apache-2.0 licensing.
6. 配置cargo-deny,准备一个deny.toml,禁止openssl,并强制只接受 MIT/Apache-2.0 许可。 -
Optimize the release profile with
lto = "thin"、strip = true、codegen-units = 1. Measure binary size before and after withcargo bloat.
7. 优化 release profile,加入lto = "thin"、strip = true、codegen-units = 1,然后用cargo bloat对比前后体积。 -
Add
cargo hack --each-featureverification. Create a feature flag for an optional dependency and ensure it compiles alone.
8. 加入cargo hack --each-feature验证。给一个可选依赖做 feature flag,确认它单独打开时也能编过。 -
Write the GitHub Actions workflow with all 6 stages. Add
Swatinem/rust-cache@v2withsave-iftuning.
9. 写完整的 GitHub Actions 工作流,把前面提到的 6 个阶段都接进去,再配上Swatinem/rust-cache@v2和save-if调优。
Success criteria: Push to GitHub → all CI stages green → cargo dist plan shows your release targets. At that point, the workspace already has a real production-grade pipeline.
完成标准:推到 GitHub 之后,所有 CI 阶段都变绿,cargo dist plan 也能列出发布目标。做到这里,就已经是一条像模像样的生产级 Rust 工程流水线了。
CI Pipeline Architecture
CI 流水线架构图
flowchart LR
subgraph "Stage 1 — Fast Feedback < 2 min"
CHECK["cargo check\ncargo clippy\ncargo fmt"]
end
subgraph "Stage 2 — Tests < 5 min"
TEST["cargo nextest\ncargo test --doc"]
end
subgraph "Stage 3 — Coverage"
COV["cargo llvm-cov\nfail-under 80%"]
end
subgraph "Stage 4 — Security"
SEC["cargo audit\ncargo deny check"]
end
subgraph "Stage 5 — Cross-Build"
CROSS["musl static\naarch64 + x86_64"]
end
subgraph "Stage 6 — Release (tag only)"
REL["cargo dist\nGitHub Release"]
end
CHECK --> TEST --> COV --> SEC --> CROSS --> REL
style CHECK fill:#91e5a3,color:#000
style TEST fill:#91e5a3,color:#000
style COV fill:#e3f2fd,color:#000
style SEC fill:#ffd43b,color:#000
style CROSS fill:#e3f2fd,color:#000
style REL fill:#b39ddb,color:#000
Key Takeaways
本章要点
- Structure CI as parallel stages: fast checks first, expensive jobs behind gates
CI 最好拆成并行阶段:先放快速检查,再把更重的任务挂在后面。 Swatinem/rust-cache@v2withsave-if: ${{ github.ref == 'refs/heads/main' }}prevents PR cache thrashingSwatinem/rust-cache@v2配上save-if限制主分支写缓存,能减少 PR 把缓存搅乱。- Run Miri and heavier sanitizers on a nightly
schedule:trigger, not on every push
Miri 和更重的 sanitizer 更适合放到 nightly 定时任务里,不适合每次推送都跑。 Makefile.toml(cargo make) bundles multi-tool workflows into a single command for local devMakefile.toml配合cargo make,可以把本地一长串工具命令收成一个入口。cargo-distautomates cross-platform release builds — stop writing platform matrix YAML by handcargo-dist可以自动化跨平台发布构建,很多手写矩阵 YAML 的苦活都能省掉。