- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
Apache OpenDAL 的 Rust 发布不是一个简单的cargo publish,而是涉及 99 个 crates 的依赖排序、包名预留、可信发布(Trusted Publishing)与多阶段 CI 校验的完整流程。本篇指南以官方发布技能文档.agents/skills/opendal-release/references/rust.md为核心骨架,结合仓库中的发布辅助脚本与工作流源码,系统讲解 Rust 发布就绪检查的每一步:如何检查发布计划、满足五条发布安全要求、以及如何诊断与修复发布辅助 CI 失败。读完本文,你将掌握 Apache OpenDAL 从"发布计划"到"crates.io 全量发布"的完整核查方法,并能在正式发布与投票前独立完成 Rust 发布就绪检查。
Rust 发布就绪检查概览
发布技能文档定义了 Rust 发布就绪检查的定位:在官方发布之前(最好在投票之前),检查 Rust 发布计划(publish plan)。其核心命令为:
python3 .github/scripts/release_rust/plan.py该命令输出按依赖拓扑排序的待发布 crate 路径列表(JSON 格式)。在此基础上,0.56.0 发布周期沉淀了五条发布安全要求,是就绪检查必须逐条核对的硬性门槛:
- 顶层
opendal通过testsfeature 引用core/testkit(即opendal-testkit)时,该 crate 必须出现在 Rust 发布计划中; - 发布辅助脚本必须使用
cargo publish --package <name>,而不能依赖 workspace 默认行为; - 仓库内部的
dev-dependencies即使在cargo publish --no-verify下也会破坏打包,必须使用.github/scripts/release_rust/publish.py并保持其测试为绿色; - bootstrap 工作流必须在 RC 打标签之前成功,且发布计划之后若新增 crate,必须重跑 bootstrap;
- 正式发布工作流只能使用 Trusted Publishing:每次发布尝试(包括重试)都要获取并吊销一个新的 OIDC 派生的 crates.io token。
下文逐一展开。需要说明的是,文档中的经验来自 0.56.0 周期,而当前仓库 core/Cargo.toml 的 crate 版本已演进到0.59.4,但发布安全要求与工具链结构保持不变。
第一步:检查 Rust 发布计划
为什么要有一个"发布计划"脚本
在仓库拆分之后,OpenDAL 的 Rust crates 不再由一份手工维护的简短列表表示。release_rust 目录说明 明确指出,发布工作流需要做到四件事:
- 发现
core/与integrations/下所有可发布的 Rust crates; - 排除
publish = false的 crate; - 按依赖顺序发布,保证本地 path 依赖在发布时已存在于 crates.io;
- 打包时剔除仓库内部 dev-dependencies,避免同版本 dev-dependency 循环阻塞发布。
把这些逻辑独立成脚本,既便于单元测试,也让 workflow YAML 保持简洁可读。
plan.py 的扫描范围与排序原理
plan.py 中定义了七类扫描根:
core/Cargo.toml core/core/Cargo.toml core/testkit/Cargo.toml core/http-transports/*/Cargo.toml core/layers/*/Cargo.toml core/services/*/Cargo.toml integrations/*/Cargo.toml对每个 manifest,脚本先读取package段,跳过没有 package 或publish = false的 crate;然后读取dependencies、build-dependencies以及各[target.*]下的对应表,构建本地依赖图;最后通过经典的 Kahn 拓扑排序(按路径字典序稳定出队)生成确定性的发布顺序。若排序结果与包总数不一致,脚本会抛出failed to resolve publish order for Rust crates错误。
实际运行 plan.py
在当前仓库根目录运行:
python3 .github/scripts/release_rust/plan.py输出为 JSON 数组,例如当前仓库的实际输出以["core/core", "core/http-transports/reqwest", "core/layers/async-backtrace", ...]开头,以["core", "integrations/object_store", "integrations/parquet", "integrations/unftp-sbe"]收尾,共99 个可发布包。注意排序特征:core/core(opendal-core)永远在最前,顶层core(opendal)位于依赖它的服务/layer 之后,三个集成 crate(object_store、parquet、unftp-sbe)依赖顶层opendal而排在最后。
若需要把计划写入 GitHub Actions 输出,供下游 job 消费:
python3 .github/scripts/release_rust/plan.py --github-output该模式会将packages=<json>追加写入GITHUB_OUTPUT环境变量指定的文件(见 plan.py#L142-L148)。
测试如何保障计划正确性
test_plan.py 覆盖了三个关键场景:临时 fixture 中按 path 依赖排序(含publish = false的排除)、真实仓库计划必须包含core/core、core/testkit、core与集成 crate 且排除bindings/python等非发布路径、以及仓库真实计划的相对顺序约束(core/core先于core,core先于所有集成 crate)。这也是文档要求"保持 helper 测试绿色"的验证基础。
发布安全要求一:opendal-testkit 必须进入发布计划
顶层opendalcrate 通过testsfeature 引用测试基础设施opendal-testkit。在 core/Cargo.toml#L223 中可以看到:
tests = ["opendal-testkit"]而在 core/Cargo.toml#L339:
opendal-testkit = { path = "testkit", version = "0.59.4", optional = true }也就是说,core/testkit(crates.io 上名为opendal-testkit)是顶层opendal的本地 path 依赖。当opendal发布时,如果opendal-testkit尚未在 crates.io 上就位,cargo publish会因为找不到同版本的本地依赖而失败。因此发布就绪检查的第一条是:确认core/testkit出现在 plan.py 的输出中,且其发布顺序早于顶层core。当前仓库的运行结果满足这一点(core/testkit排在core之前)。
排障参考也印证了这一要求:troubleshooting.md 中的 "opendal-testkit is missing from crates.io plan" 条目明确写道:当opendal通过testsfeature 引用它时,必须把core/testkit设为可发布并在顶层opendal之前发布。
发布安全要求二:按包发布,使用 cargo publish --package
不要依赖 workspace 默认值来发布。原因在于:OpenDAL 仓库根目录是 workspace,若直接cargo publish可能发布到错误的目标或带上错误的包集合;而明确指定包名可以确保"发布的是计划中的那一个 crate"。
release_rust.yml 的publishjob 中,发布命令由 publish.py 执行,其核心命令构造为:
cmd = ["cargo", "publish", "--package", name, "--no-verify"]其中name来自目标包自身 manifest 的package.name(而非目录名推断),从而保证 workspace 中每个包都以自己的身份发布。--no-verify跳过本地构建验证以节省时间,代价是本地打包问题只能靠 manifest 处理来规避——这正是下一条要求存在的原因。
发布安全要求三:剥离仓库内部 dev-dependencies
为什么 dev-dependencies 会破坏打包
即使加了--no-verify,cargo publish仍会执行打包(packaging)步骤:它会读取 manifest 中的依赖并校验它们是否可解析。仓库内部的 dev-dependencies 指向本仓库其他目录(例如../local-dev),打包后这些 path 依赖并不存在,从而触发解析失败;同版本的 dev-dependency 循环也会在发布时造成阻塞。文档因此强调:仓库内部 dev-dependencies 可以破坏打包,即使使用cargo publish --no-verify。
publish.py 的处理方式
publish.py 实现了完整的"剥离—发布—恢复"流程:
local_dev_dependency_names:扫描 manifest 的[dev-dependencies]与各[target.*.dev-dependencies],只收集 path 指向本仓库目录((manifest_dir / path).resolve().is_dir())的依赖名;strip_local_dev_dependencies:按行解析 manifest,识别 dev-dependency section,删除命中的依赖条目(支持跨行{ ... }与[ ... ]结构);- 发布前通过
prepare_manifest记录原始 manifest 与全仓库所有Cargo.lock(排除 target 目录),发布后通过restore完整还原,不污染工作区。
剥离发生在cargo publish之前,且由于 manifest 被临时修改,命令会附带--allow-dirty。发布结束后无论成功失败,finally都会执行restore(patch)。
测试佐证
test_publish.py 的test_strip_local_dev_dependencies构造了一个包含普通本地依赖、外部 dev-dependency、本地 dev-dependency(含多行 features 写法)与 target 级本地 dev-dependency 的 fixture,验证剥离后:普通本地依赖与外部 dev-dependency 保留,而所有本地 dev-dependency 条目被删除。
发布安全要求四:bootstrap 工作流在 RC 打标签前成功
为什么需要 bootstrap
OpenDAL 的 Rust crate 名散布在core/与integrations/下,需要确保每个计划中的 crate 名都已在 crates.io 上被正确"预留"并只允许 OpenDAL 通过可信发布方式发布。bootstrap 工作流(bootstrap_rust_crates.yml)完成这一使命:对缺失的包名发布一个无依赖的0.0.0命名预留包,并配置唯一的 Trusted Publisher。文档明确强调:0.0.0包是对外的、不可逆的命名空间预留,不是 ASF 软件发布或发布制品。
bootstrap 的触发与状态流转
发布经理在计划发布前约三天选定预留时间后,运行仓库内的辅助脚本:
.agents/skills/opendal-release/scripts/bootstrap-rust-crates.sh脚本 bootstrap-rust-crates.sh 的执行步骤与文档一致:
- 校验 checkout 干净且位于
apache/opendalmain的最新提交; - 通过
gh api校验rust-bootstrapGitHub 环境存在required_reviewers保护规则(不存在则直接失败); gh workflow run bootstrap_rust_crates.yml --ref main触发工作流;- 解析出对应 run(按标题、headSha、创建时间匹配),校验
headSha后gh run watch --exit-status等待完成; - 完成后执行
python3 .github/scripts/release_rust/bootstrap.py verify做公开校验。
该工作流本身由两个 job 组成:discoverjob 运行bootstrap.py discover报告缺失与占位 crate;bootstrapjob 运行在受保护的rust-bootstrap环境中,使用CARGO_REGISTRY_BOOTSTRAP_TOKEN执行bootstrap.py apply。
rust-bootstrap 环境的保护配置
.asf.yaml#L44-L64 中 ASF Infrastructure 为该环境配置了:
environments: rust-bootstrap: required_reviewers: [Xuanwo, dentiny, erickguan, tisonkun, koushiro, PsiACE] wait_timer: 0 prevent_self_review: true deployment_branch_policy: protected_branches: false policies: - name: main type: branch即每次运行都需要 PMC 成员审核批准(prevent_self_review 禁止自审),且只允许从main部署。此外,PMC 发布经理必须在环境中配置CARGO_REGISTRY_BOOTSTRAP_TOKEN密钥;该 token 只应拥有publish-new与trusted-publishing端点权限,crate 范围仅限 OpenDAL 包名,并且绝不能暴露给正常发布工作流。
bootstrap.py 的审计与对账逻辑
bootstrap.py 提供三个子命令:
discover:无凭据读取 crates.io 公共元数据,报告缺失包名与现有0.0.0占位包;apply:使用受保护的 bootstrap token 执行完整流程——先对所有计划 crate 做认证预检(校验 crate 名、repository 归属、占位包描述、Trusted Publisher 配置与trustpub_only状态),再对缺失名发布无依赖的0.0.0占位包、创建唯一的 Trusted Publisher(apache/opendal+release_rust.yml+release环境)、启用trustpub_only并修复部分完成的占位包,最后做一次认证终检;verify:读取公共元数据,确认所有计划 crate 都存在且要求 Trusted Publishing。
关键设计是:bootstrap 永不修改已建立的 crate。若某个名字在发现后已变成正式 crate,reconcile_crate会直接拒绝修改。已存在 crate 的一次性 Trusted Publishing 迁移是独立的管理性前置条件,必须预先完成——bootstrap 会审计但不会改写这些 crate,迁移未完成时工作流直接失败。
计划新增 crate 时必须重跑
文档明确指出:bootstrap 工作流总是从它的main提交扫描发布计划。如果成功预留之后发布计划又新增了一个 crate,发布经理需在选定的时间再次运行辅助脚本(重跑会对每个占位包做对账),确保新增名字同样被预留并配置好 Trusted Publisher。
发布安全要求五:正式发布仅使用 Trusted Publishing
工作流侧的配置
release_rust.yml 中publishjob 的关键配置:
- 触发条件:
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-'),即仅在正式 tag(不含-,从而排除 RC tag)上执行; environment: release;- 权限:
id-token: write——这是 GitHub OIDC 的前提; - 发布命令:
python3 .github/scripts/release_rust/publish.py,PACKAGES来自上游planjob 的输出。
同时,planjob 在每次运行前都会执行python3 -m unittest discover -s .github/scripts/release_rust -p "test_*.py",把 helper 测试作为发布流水线的第一道门禁——这正是"保持 publish.py 测试绿色"要求在工作流中的落地。
每次发布尝试都换发并吊销 OIDC token
trusted_publishing.py 实现了完整的"换取—使用—吊销"协议:
- 从
ACTIONS_ID_TOKEN_REQUEST_URL/ACTIONS_ID_TOKEN_REQUEST_TOKEN读取 GitHub OIDC 端点,请求 audience 为crates.io的 OIDC token(JWT); - POST 到 crates.io 的
/api/v1/trusted_publishing/tokens,把 JWT 换成一个短期 crates.io Trusted Publishing token; - 通过
temporary_trusted_publishing_token上下文管理器使用该 token 执行cargo publish,并在finally中调用revoke_trusted_publishing_token立即吊销。
两个细节值得注意:token 在输出时会通过::add-mask::打码防止泄露;每次发布尝试都会走一遍换取—吊销协议——包括限流后的重试,重试时绝不复用旧 token。这正是文档"它获取并吊销一个新的 OIDC 派生 crates.io token,用于每次发布尝试(包括重试)"的源码实现。
对应测试 test_trusted_publishing.py 验证了:OIDC 请求保留既有 query 并追加audience=crates.io、token 交换使用 GET+POST 且携带 Bearer 头、吊销使用 DELETE、以及temporary_trusted_publishing_token在异常路径下也会保证吊销。此外,test_publish.py 的test_live_publish_fetches_a_new_token_for_every_attempt用一个返回两次不同 token 的 provider 模拟限流重试,断言两次cargo publish各拿到一个新 token、且两个 token 都被释放;test_dry_run_does_not_request_a_trusted_publishing_token则确认--dry-run模式不会请求 OIDC token。
限流与"已发布"识别
publish_package对 crates.io 的三种响应分别处理:
- 成功(returncode 0):返回;
- 输出含
already uploaded/already exists/is already uploaded:判定已发布,跳过; - 输出含
Too many requests/rate limit/you have published too many crates:解析服务端返回的Please try again after ...时间(支持 RFC 日期与 ISO 日期),至少等待 60 秒(默认 610 秒)后重试。
因此"Rust 发布耗时很长"本身不一定是异常——99 个 crate 按计划顺序逐个发布,辅以限流退避,运行数十分钟甚至更久都属正常(troubleshooting.md 对此有专门说明)。
诊断与修复发布辅助 CI 失败
总原则
当发布辅助 helper 的 CI 失败时,文档给出明确的处置纪律:
- 从失败的 job 直接诊断根因,而不是盲目重跑或改配置;
- 仅在失败确实需要更新的 base 时才 rebase;
- 仅在证据指向代码或依赖问题时才更新代码或依赖;
- 针对受影响的特定行为运行针对性测试;
- 当相关代码、依赖与环境未变化时,复用已通过的测试结果,不要无意义重跑。
常见失败模式与处置
结合 troubleshooting.md 的 Rust 相关条目,就绪检查中常见的问题与处置如下:
| 失败现象 | 根因 | 处置 |
|---|---|---|
cargo publish因 dev-dependencies 失败 | 仓库内部 path dev-dependency 打包后不可解析 | 不要盲目改 workflow YAML;使用并测试 publish.py,它负责剥离与恢复 |
opendal-testkit不在计划中 | 顶层opendal的testsfeature 引用了它 | 让 core/testkit 可发布,并在顶层opendal之前发布 |
| publish job 长时间运行 | 99 个 crate 按拓扑顺序逐个发布 + 限流退避 | 查看 plan.py 输出,观察工作流日志,逐个确认 crate 渐进发布,而非误判为卡死 |
crates.io 稀疏索引抖动(如no matching package named ... found) | 注册表瞬时故障 | 先重跑失败的 job,再考虑开代码修复 PR |
本地运行 helper 测试
发布就绪检查的收尾是让 helper 测试保持绿色:
python3 -m unittest discover -s .github/scripts/release_rust -p "test_*.py"在当前仓库实际运行该命令,30 个测试中 29 个通过;唯一失败项test_placeholder_can_be_packaged_offline是因为本机未安装cargo(FileNotFoundError: 'cargo'),与脚本逻辑无关——这恰好印证了文档"保持测试绿色"的前提是相关环境就绪。在 CI 的planjob 中,该测试集作为发布流水线的门禁在每次运行时都会执行。
总结:完整的 Rust 发布就绪检查清单
将上文内容归纳为一张可执行的检查清单:
- 检查发布计划:运行
python3 .github/scripts/release_rust/plan.py,确认输出包含core/core、core/testkit、core与全部integrations/*,且顺序满足依赖拓扑; - 核对 testkit:确认
core/testkit可发布且排在顶层opendal之前(对应 core/Cargo.toml#L223 的testsfeature); - 核对发布方式:发布统一经 publish.py 的
cargo publish --package <name>执行,仓库内 dev-dependencies 被自动剥离并恢复; - 核对 bootstrap:
bootstrap_rust_crates.yml已在 RC 打标签前于最新main上成功,所有计划 crate 名存在、Trusted Publisher 唯一且trustpub_only已启用;发布计划新增 crate 后已重跑; - 核对可信发布:正式发布工作流 release_rust.yml 仅依赖 OIDC 派生的临时 token(
id-token: write+environment: release),每次尝试换取并吊销; - 核对测试与诊断:
.github/scripts/release_rust下的单元测试为绿色;任何 helper CI 失败都从失败 job 定位根因,仅在证据充分时才 rebase 或改代码,并在代码、依赖、环境不变时复用既有通过结果。
通过以上六步,发布经理即可在投票与正式发布前确认 Apache OpenDAL 的 Rust 发布处于就绪状态,避免在 RC 阶段才发现opendal-testkit缺失、dev-dependency 打包失败或 Trusted Publisher 配置错误等"最后一公里"问题。
延伸阅读:若需深入了解本文引用的完整流程,可继续阅读仓库内的相关文档:Rust crate bootstrap 参考、发布技能总览、发布辅助脚本说明,以及 发布排障参考。
- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
相关推荐
Apache OpenDAL Release Candidate(RC)发布全流程实战指南:从每周 ATR 候选到 Nexus 投票就绪
Apache OpenDAL Release Candidate(RC)发布全流程实战指南:从每周 ATR 候选到 Nexus 投票就绪 Apache Open
数据存储后端Apache DataFusion 发布流程全指南:从 RC 构件、社区投票到 crates.io 发布
Apache DataFusion 发布流程全指南:从 RC 构件、社区投票到 crates.io 发布 Apache DataFusion 作为 Apache
大数据数据分析后端Apache OpenDAL Go Binding 发布就绪检查:独立 Go Module 的 RC 标签与服务制品验证指南
Apache OpenDAL Go Binding 发布就绪检查:独立 Go Module 的 RC 标签与服务制品验证指南 导读 :本文聚焦 Apache O
数据存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考