Cargo 发布到 crates.io:从命名、文档到 CI 自动发布的完整流水线
一、第一次 cargo publish 时踩的三个坑
第一次把自己的库发布到 crates.io 那天,我记得很清楚。先是名字被别人占了,然后cargo publish提示README.md不存在,加上 README 后又说我忘了设置description字段。好不容易元数据都填对了,push 上 GitHub 之后才发现 CI 里根本没配置自动发布,还得手动回到本机再跑一次。
发布 Rust 包看起来只是一条cargo publish命令,但真正让人踩坑的是发布前后那一整条链路。命名能不能通过、文档规不规范、打了 tag 之后 CI 能不能正确地触发发布、发了之后发现严重 bug 怎么办——这些才是实际工作中真正让人头疼的地方。
下面我把从你准备发布的那一刻开始,到 CI 帮你自动推到 crates.io 的完整流水线理了一遍。内容来自几次实际发布经验的总结,不是官方文档翻译。
二、一条完整的发布流水线长什么样
下面这张流程图描述了一个标准的工作流。从你在本地做好最终验证开始,到 CI 自动完成cargo publish为止。其中灰色标注的是需要人工检查的步骤。
flowchart TD A["本地 cargo package\n打包验证(不发布)"] --> B{"人工检查\nCargo.toml 元数据\nversion/description/license"} B -->|"通过"| C["本地 cargo publish --dry-run\n模拟发布(检查网络连通)"] C -->|"通过"| D["git commit + git tag v0.1.0\n打标签,确认要发布的版本号"] D --> E["git push && git push --tags\n推送代码和标签到 GitHub"] E --> F["GitHub Actions 触发\n读取 git tag 版本号"] F --> G["CI 运行: cargo test\n运行完整测试套件"] G --> H{"测试是否通过?"} H -->|"是"| I["CI 运行: cargo publish\n携带 CRATES_TOKEN 认证"] H -->|"否"| J["❌ 终止发布\n删除远端 tag"] I --> K["✅ 发布成功\n通知作者"]这张图的重点不是"自动化"三个字,而是在该人工确认的地方保留了你介入的空间。很多项目的 CI 配置是"打 tag 马上发布",但如果 tag 打错了(比如在 test 分支上误打了标签),连反应时间都没有。
我自己目前的做法是:打完 tag 之后触发 CI 跑测试,但不立即 publish。测试通过后写一条 commit 到CHANGELOG.md,再打一个不包含v前缀的内部 tag 触发真正的发布步骤。这样两次确认之间多了一个缓冲。
三、Cargo.toml 的元数据不是填了就行
[package] # 基础信息 — 这些字段 crates.io 要求必填 name = "my-ai-toolkit" version = "0.1.0" edition = "2021" description = "一个轻量的 AI 工具链: 文本分块、相似度计算和 Prompt 模板引擎" license = "MIT OR Apache-2.0" repository = "https://github.com/username/my-ai-toolkit" readme = "README.md" keywords = ["ai", "nlp", "prompt", "text-processing"] categories = ["development-tools", "text-processing"] # 最低 Rust 版本 — 告诉用户他们能不能用 rust-version = "1.75" [dependencies] # 指定最小可工作的版本, 用 ^ 自动兼容 serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } [dev-dependencies] # 测试依赖 — 只在 cargo test 时下载 criterion = "0.5" [features] # 特性门控制 — 让用户按需引入 default = ["json"] json = ["dep:serde_json"]有几个经常被忽略但很重要的字段。readme:如果不指定,crates.io 页面上不会显示你的 README 内容,用户看不到任何使用示例。keywords:最多 5 个,决定了你的包在搜索中能不能被找到。categories:必须是 crates.io 官方预定义的分类列表里的值,随便填的不会生效。rust-version:当你的包用到了特定版本才有的新特性时,这个字段可以帮用户提前判断。
另外就是 license 字段。很多人觉得"写个 MIT 就行了"。但如果你的包依赖了 GPL 协议的库,你的包也必须用 GPL 发布。所以license不是随便选的,要和整个依赖树兼容。
四、CI 自动发布的配置和维护
下面是 GitHub Actions 的 CI 配置。它只在检测到crates-tag-v*格式的推送时才触发发布:
# .github/workflows/publish.yml name: Publish to crates.io on: push: tags: # 只监听 crates-tag- 前缀的 tag, 和普通版本 tag 区分开 - "crates-tag-v*" jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: 安装 Rust 工具链 uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: 运行全部测试 run: cargo test --all-features - name: 发布到 crates.io env: # CRATES_TOKEN 需要在 GitHub Secrets 中配置 # 登录 crates.io → Account Settings → API Token → 生成一个 CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_TOKEN }} run: cargo publish - name: 通知 Slack (可选) uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} text: "包已发布到 crates.io: ${{ github.ref_name }}" env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}这套配置里有两个地方要特别注意。CARGO_REGISTRY_TOKEN是敏感信息,绝对不能硬编码在cargo publish命令后面,必须通过 GitHub Secrets 注入。另外就是 "运行全部测试" 这一步,在发布前跑一边全量测试是必须的安全感——有时候你在本地通过了测试,但 CI 的环境可能依赖版本不同导致失败。
还有一个关于 yank 的小知识。如果你发布后发现了一个严重 bug,不要慌,cargo yank --vers 0.1.0会把这版本标记为"不可用",已经依赖了这个版本的项目在cargo update时不会自动升级,但已经锁定了版本的项目不会受影响。yank 是你最后的安全网。
关于 semver 版本号,我刚开始总是搞不清楚 0.1.0 和 1.0.0 之间该怎么调整。简单记住一个规则就行:在0.x.y阶段,改y(补丁版)是向下兼容的 bug 修复,改x(次版本号)可能是破坏性改动。但严格来说,在 0.x 阶段,Cargo 不保证任何跨次版本号的兼容性。所以一旦你想让别人放心地依赖你的 API,就别再在 0.x 徘徊了。
五、总结
发布一个包到 crates.io 不复杂,复杂度在于那条流水线的完整性:有没有人工检查的环节、测试有没有全面、yank 机制是否准备好了、CI 的 token 有没有泄露风险。一条好的发布流程应该让"发布"这件事比"不发"更安全。
最后说一句我自己的教训:永远先跑cargo package和cargo publish --dry-run,这两个命令可以帮你拦截 80% 被拒的发布请求。特别是cargo package,它会检查本地文件目录结构和Cargo.toml里include/exclude字段是否冲突,这步验证在正式 publish 时是重复执行的,提前跑一次能省下很多查 log 的时间。