Rust 2026 经验谈 - 从零到发布 CI/CD 全链路
一个 Rust 库从本地开发到用户 cargo add 可用,中间要经过测试、lint、版本管理、发布、文档部署等多个环节。把这些环节串成可靠的自动化流水线,是库维护者的基本功。本文将分享我在 20+ 个 Rust crate 的发布实践中积累的 CI/CD 经验。
GitHub Actions + cargo 典型 Workflow
基础 Workflow
name: CI
on: push: branches: [main] pull_request: branches: [main]
env: CARGO_TERM_COLOR: always
jobs: check: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo check --workspace
test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo install cargo-nextest --locked - run: cargo nextest run --workspace
clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --workspace -- -D warnings
fmt: name: Formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --all -- --check
docs: name: Docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo doc --workspace --no-deps env: RUSTDOCFLAGS: -D warnings缓存配置:为什么用 Swatinem/rust-cache
Swatinem/rust-cache@v2 是 Rust CI 缓存的事实标准。它缓存:
~/.cargo/registry:下载的 crate 源码~/.cargo/git:git 依赖target/:编译产物(按 key 哈希分区)
关键配置:
- uses: Swatinem/rust-cache@v2 with: key: ubuntu-stable # 区分不同 job 的缓存 cache-on-failure: true # 即使测试失败也缓存编译产物踩坑:如果你的 workflow 有多个 job 都编译同一个项目,它们会各自维护 target/ 缓存,可能导致重复编译。解决方案是在根 job 中编译,后续 job 依赖其缓存。或者接受这个开销——在 GitHub Actions 的并行执行模型下,重复编译往往比串行更快。
Matrix 测试:多平台 × 多版本
test-matrix: name: Test (${{ matrix.os }}, ${{ matrix.rust }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false # 一个失败不影响其他 matrix: os: [ubuntu-latest, macos-latest, windows-latest] rust: [stable, beta] exclude: - os: windows-latest rust: beta # 排除 Windows beta,减少 CI 时间
steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.os }}-${{ matrix.rust }} - run: cargo test --workspaceMSRV 测试(Minimum Supported Rust Version):
msrv: name: MSRV runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: "1.85.0" # Rust 2024 Edition 的最低稳定版本;声明在 Cargo.toml 的 rust-version - run: cargo check在 Cargo.toml 中声明 MSRV:
[package]rust-version = "1.85.0"cargo 会在用户工具链低于 rust-version 时给出清晰的错误提示。
语义化版本自动化
cargo-release:一站式发布工具
cargo install cargo-releasecargo-release 自动化整个发布流程:bump 版本号、更新 changelog、创建 git tag、发布到 crates.io、推送 git tag。
# Cargo.toml 中的 release 配置[package.metadata.release]pre-release-hook = ["cargo", "test"] # 发布前跑测试tag-prefix = "" # tag 格式:v0.5.0consolidate-commits = true # 多个变更合并为一个 commit典型流程:
# 发布 patch 版本(0.5.0 → 0.5.1)cargo release patch
# 发布 minor 版本(0.5.0 → 0.6.0)cargo release minor
# dry-run 模式(只预览,不实际执行)cargo release patch --dry-runConventional Commits + 自动版本推断
Conventional Commits 约定提交消息格式:
feat: add streaming support → minor bumpfix: correct timeout handling → patch bumpfeat!: change API signature → major bump (breaking)配合 git-cliff 生成 changelog:
cargo install git-cliffgit cliff --tag v0.6.0 > CHANGELOG.mdgit-cliff 配置文件 cliff.toml:
[changelog]header = """# Changelog\n"""body = """{% if version %}## {{ version }} ({{ date | date(format="%Y-%m-%d") }}){% else %}## Unreleased{% endif %}{% for group in commits | group_by(attribute="group") %}### {{ group.key | capitalize }}{% for commit in group.commits %}- {{ commit.message | upper_first }}{% endfor %}{% endfor %}\n"""
[git]conventional_commits = truefilter_unconventional = truecommit_parsers = [ { message = "^feat", group = "Features" }, { message = "^fix", group = "Bug Fixes" }, { message = "^doc", group = "Documentation" }, { message = "^perf", group = "Performance" }, { message = "^refactor", group = "Refactor" }, { message = "^chore", skip = true },]经验谈:Conventional Commits 的价值不在于格式本身,而在于让版本号变更有据可查。当用户问”为什么 0.6.0 有 breaking change?“,你可以直接指向 feat!: change API signature 这个 commit。但不要过度追求——如果团队不习惯这个格式,强制推行只会增加摩擦。我建议先在 CI 中用 commitlint 做可选检查,而非强制拒绝。
crates.io 发布流程与最佳实践
发布前 Checklist
cargo semver-checks:确保 API 兼容性cargo test --workspace:全部测试通过cargo clippy -- -D warnings:无 clippy 警告cargo doc --no-deps:文档无警告cargo audit:无已知安全漏洞- 检查
Cargo.toml元数据:description、license、repository、keywords、categories 完整
Cargo.toml 元数据
[package]name = "my-crate"version = "0.6.0"edition = "2024"description = "A concise description of what this crate does"license = "MIT OR Apache-2.0" # Rust 社区标准双许可repository = "https://github.com/user/my-crate"homepage = "https://user.github.io/my-crate"documentation = "https://docs.rs/my-crate" # 通常自动生成,无需显式设置keywords = ["async", "stream", "backpressure"]categories = ["asynchronous", "network-programming"]rust-version = "1.85.0"include = [ # 仅包含必要文件,减小 crate 体积 "src/**/*.rs", "Cargo.toml", "README.md", "LICENSE-*",]踩坑:include 字段很关键。默认情况下 cargo publish 包含仓库根目录的所有文件(除了 gitignore 排除的)。如果你的仓库有大型测试数据、benchmark 数据、文档源文件等,它们会被不必要地包含在发布的 crate 中,增加用户下载时间。
发布命令
# 首次发布需要登录cargo login <API_TOKEN> # 从 crates.io/me 获取
# 发布(dry-run 先预览)cargo publish --dry-runcargo publish
# 撤回发布(仅阻止新下载,已下载的不受影响)cargo yank --vers 0.6.0重要规则:
- crates.io 不允许删除已发布的版本,只能 yank
- yank 后该版本仍然可下载(已有
Cargo.lock的用户不受影响),但不允许新的Cargo.lock选用它 - 版本号一旦发布就不能重复使用,即使 yank 了也不行
发布自动化 Workflow
name: Release
on: push: tags: ['v*']
jobs: publish: name: Publish to crates.io runs-on: ubuntu-latest environment: release # GitHub Environment,保护 secrets steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Publish run: cargo publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}安全配置:把 CARGO_REGISTRY_TOKEN 存在 GitHub Environment 的 secrets 中,配合 environment protection rules(要求手动审批)可以防止误发布。
文档部署
docs.rs 定制
docs.rs 是 Rust 社区的文档托管服务,cargo publish 后自动构建和部署。你可以通过 Cargo.toml 定制构建行为:
[package.metadata.docs.rs]features = ["full"] # 启用所有 feature 以展示完整文档rustdoc-args = ["--cfg", "docsrs"] # 传递 cfg 标志targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-musl"]在代码中使用 docsrs cfg:
#[cfg(docsrs)]/// 此文档仅在 docs.rs 上可见完整内容。/// 在本地 `cargo doc` 中,此段可能被省略。pub fn advanced_feature() {}rustdoc 配置最佳实践
- 启用
deny(rustdoc::broken_intra_doc_links):文档内链接失效即报错,防止文档中引用了不存在的项。
[lints.rustdoc]broken_intra_doc_links = "deny"-
为 public item 编写 doc comment:用
///或//!,而非/* */。doc comment 支持 Markdown。 -
使用
#[doc(alias = "...")]:为类型/函数添加搜索别名。
#[doc(alias = "Future")]#[doc(alias = "promise")]pub struct AsyncTask { /* ... */ }- 文档测试(doc test):doc comment 中的代码块会作为测试运行。对于不需要运行的代码块,标注
no_run或ignore:
/// ```/// // 需要网络连接,CI 中不运行/// let result = my_crate::fetch("https://example.com").await;/// ```////// ```no_run/// let result = my_crate::fetch("https://example.com").await;/// ```pub async fn fetch(url: &str) -> String { /* ... */ }- 隐藏实现细节:用
#[doc(hidden)]标记不想暴露在文档中的 public item。
CI 中的安全审计
cargo-audit:已知漏洞检测
audit: name: Security Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: rustsec/audit-check@v2 with: token: ${{ secrets.GITHUB_TOKEN }}rustsec/audit-check action 会检查 Cargo.lock 中的依赖是否包含已知安全漏洞(基于 RustSec 数据库),并在发现漏洞时创建 GitHub Issue。
cargo-vet:供应链验证
cargo-vet 是 Mozilla 开发的供应链审计工具,用于验证依赖的特定版本是否经过人工审查:
cargo install cargo-vet
# 初始化(创建 supply-chain/ 目录)cargo vet init
# 审查未验证的依赖cargo vet suggest
# 标记已审查的依赖cargo vet certify <crate> <version> <criteria>cargo-vet 的核心思想是每个依赖的每个版本都必须经过某种形式的人工审查,才能进入项目。审查标准(criteria)可自定义,如 “safe-to-deploy”、“safe-to-run”。
supply-chain/audits.toml 示例:
[[audits.serde]]who = "Alice <alice@example.com>"criteria = "safe-to-deploy"version = "1.0.195"notes = "Reviewed diff from 1.0.194, only performance improvement"
[[audits.serde_json]]who = "Alice <alice@example.com>"criteria = "safe-to-deploy"delta = "1.0.112 -> 1.0.113" # 仅审查 diff何时用 cargo-vet:如果你的项目对安全性有较高要求(金融、医疗、基础设施),cargo-vet 提供了比 cargo-audit 更细粒度的控制。对于一般项目,cargo-audit 足矣。
每日定时审计
name: Daily Audit
on: schedule: - cron: '0 6 * * 1' # 每周一 06:00 UTC
jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: rustsec/audit-check@v2经验谈:把审计放在定时任务而非每次 push 中,因为漏洞数据库是独立更新的——你的代码没变,但某个依赖可能新被披露了漏洞。定时检查能及时捕获这种情况。
完整 CI/CD Workflow 组合
把上述所有环节组合,推荐的 workflow 结构:
| Workflow | 触发条件 | 内容 |
|---|---|---|
ci.yml | push / PR | check + test + clippy + fmt + docs |
ci-matrix.yml | push to main | 多平台 × 多版本测试 |
audit.yml | 定时 + push to main | cargo-audit |
release.yml | tag push | publish to crates.io |
msrv.yml | push to main | MSRV 兼容性检查 |
这种分层设计的好处:PR 时跑快速 CI(1-3 分钟),合并到 main 时跑完整矩阵测试(10-15 分钟),发布时跑发布流程。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!
TinyZ's Blog