dbt-core 发布流水线实战:用cargo ci完成版本号提升、多平台 Wheel 打包与 PyPI / Homebrew 发布
【免费下载链接】dbtdbt enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.项目地址: https://gitcode.com/GitHub_Trending/db/dbt
导读
本文围绕 dbt-core 仓库(dbt-fusion)中的dbt-cicrate 展开,它是整个项目"发布流水线命令"的载体,通过 workspace 级别名cargo ci暴露为 CLI。读完本文,你将掌握:如何用一条命令完成 workspace 版本号写入与Cargo.lock刷新;如何把按 cargo target triple 命名的预编译二进制打包成py3-none-{platform}格式的 wheel;如何以"上传 wheel"和"下载时再安装(download-at-install)的 sdist"两种模式发布到 PyPI / TestPyPI / AWS CodeArtifact;以及如何把发布产物渲染成 Homebrew formula 并推送到 tap 仓库。文中每个结论都附带对应的源码实现路径与测试证据。
前提说明:
dbt-ci是仓库内部的发布工具(crates/dbt-ci/Cargo.toml中声明publish = false),不面向最终用户分发,本文面向需要理解或维护 dbt-core 发布流程的开发者。
一、模块定位与命令总览
dbt-ci位于 crates/dbt-ci,其 Cargo.toml 描述为 "Release-pipeline commands for dbt-fusion: bump-cargo-version, pypi pack, pypi publish"。它通过 workspace 的 .cargo/config.toml 中ci = "run --quiet --package dbt-ci --"别名,让cargo ci <subcommand>直接可用。
从 src/main.rs 的 clap 定义可以看出,命令被组织为三层:
cargo ci ├── bump-cargo-version <version> [--no-lockfile] ├── pypi │ ├── pack --binaries-dir DIR --version X.Y.Z [--out DIR] [--bin-name NAME] │ └── publish --environment {staging|prod|test-pypi} [--version V] [--dist DIR] │ └── (sdist 模式)--version V --download-base-url URL --target TRIPLE … └── homebrew ├── render --tarballs-dir DIR --version X.Y.Z --url-template URL [--out PATH] … └── publish --formula PATH --tap-repo URL --version X.Y.Z [--tap-branch B] …所有参数都在 src/args.rs 中集中定义,并大量使用 clap 的requires/conflicts_with/required_unless_present做参数合法性校验。例如--download-base-url强制要求--version,--target又强制要求--download-base-url(见 src/args.rs),从而把"两种发布模式"的约束直接固化在命令行层面。
二、版本号提升:cargo ci bump-cargo-version
2.1 命令形态与执行流程
cargo ci bump-cargo-version <version> [--dry-run] [--no-lockfile]实现位于 src/bump_cargo_version.rs:
- 先调用
validate_release_version校验版本形状(必须是X.Y.Z或X.Y.Z-{lane}.N,见后文"版本形状"一节),非法则退出码 2; - 定位 cargo workspace 根目录,读取根 Cargo.toml;
- 用
toml_edit以保留注释与格式的方式,把新版本写入[workspace.package].version(write_workspace_version,见 src/bump_cargo_version.rs); - 除非指定
--no-lockfile,否则执行cargo update --workspace --offline刷新Cargo.lock;这一步是 best-effort——即使失败,版本号写入仍然生效,只打印警告(见 src/bump_cargo_version.rs)。
write_workspace_version的单元测试(src/bump_cargo_version.rs)验证了"覆盖旧版本、保留其余字段",并验证当Cargo.toml缺少[workspace]表时报错。
2.2 版本形状:SemVer 与 PEP 440 的映射
版本解析集中在 src/release_version.rs,它定义了发布流程统一接受的版本写法,并在解析时自动转换为 PEP 440 规范:
| SemVer | PEP 440 |
|---|---|
X.Y.Z | X.Y.Z |
X.Y.Z-alpha.N | X.Y.ZaN |
X.Y.Z-beta.N | X.Y.ZbN |
X.Y.Z-rc.N | X.Y.ZrcN |
X.Y.Z-preview.N | X.Y.ZrcN |
X.Y.Z-dev.N | X.Y.Z.devN |
几点从源码中可以确认的细节:
preview是rc的别名,两者都翻译为rcN(见 src/release_version.rs);- SemVer build metadata(
+build)不被支持,直接报错(见 src/release_version.rs); - 预发布必须是
-lane.N的单一形态,形如2.0.0-preview-nightly.176这类复合预发布会被拒绝(见 src/release_version.rs 的测试)。
这个映射是后续 wheel 文件名、sdist 版本号的统一来源,保证 Cargo 的版本写法与 PyPI 的 PEP 440 版本写法始终一致。
三、把预编译二进制打包成 Wheel:cargo ci pypi pack
3.1 命令形态
cargo ci pypi pack --binaries-dir DIR --version X.Y.Z [--out DIR] [--bin-name NAME]实现位于 src/pack.rs,核心思路是:不重新编译,而是把 CI 上按目标平台预先构建好的二进制原样塞进 wheel 的{dist}-{version}.data/scripts/目录,再补齐标准的dist-info元数据。
3.2 输入目录的命名约定
--binaries-dir下的文件必须按 cargo target triple 命名(.exe后缀表示 Windows),例如:
binaries/ ├── x86_64-unknown-linux-gnu ├── x86_64-pc-windows-msvc.exe └── aarch64-apple-darwincollect_binaries(src/pack.rs)只挑出能映射到已知 platform tag 的文件,其余文件(README、LICENSE、.DS_Store等)一律忽略——测试collect_binaries_picks_only_triple_named_files明确验证了这一行为。
3.3 target triple → PEP 491 platform tag 映射
src/pack.rs 中维护了一张映射表,这是平台分发能力的关键:
| Cargo target triple | PEP 491 platform tag |
|---|---|
x86_64-unknown-linux-gnu | manylinux_2_28_x86_64 |
aarch64-unknown-linux-gnu | manylinux_2_28_aarch64 |
i686-unknown-linux-gnu | manylinux_2_28_i686 |
x86_64-unknown-linux-musl | musllinux_1_2_x86_64 |
aarch64-unknown-linux-musl | musllinux_1_2_aarch64 |
x86_64-apple-darwin | macosx_10_12_x86_64 |
aarch64-apple-darwin | macosx_11_0_arm64 |
x86_64-pc-windows-msvc/x86_64-pc-windows-gnu | win_amd64 |
i686-pc-windows-msvc | win32 |
未知 triple 返回None并被跳过,避免生成无法被 pip 识别的 wheel。
3.4 生成的 Wheel 内部结构
pack_wheel(src/pack.rs)生成的标准 wheel 包含四个条目,以dbt-sa-cli为例:
dbt_sa_cli-2.0.0a1.data/scripts/dbt-sa-cli # 预编译二进制(Windows 下为 .exe),0755 dbt_sa_cli-2.0.0a1.dist-info/METADATA # PEP 621 元数据渲染 dbt_sa_cli-2.0.0a1.dist-info/WHEEL # Tag: py3-none-{platform};Root-Is-Purelib: false dbt_sa_cli-2.0.0a1.dist-info/RECORD # PEP 376:path,sha256=<b64>,<size>文件名遵循 PEP 491:{dist}-{version}-py3-none-{platform}.whl(见wheel_filename,src/pack.rs)。其中:
py3-none标签:CLI wheel 只是把预编译二进制包进去,与 Python 解释器版本无关,所以是解释器无关的py3-none(见 src/pack.rs 注释);METADATA渲染:来自 workspace 根 pyproject.toml 的[project]表(name、description、requires-python、dependencies、classifiers、urls、authors、license、readme等),完整支持 PEP 621 的各种写法(见 src/pyproject.rs),description 正文按 RFC 822 约定放在空行之后;RECORD哈希:对每个文件计算sha256(URL-safe base64)与字节数,自身行留空,符合 PEP 376。
--out未指定时默认输出到<workspace>/target/wheels(src/pack.rs);--bin-name用于覆盖 wheel 内脚本名,默认取[project].name。
四、发布到 PyPI 生态:cargo ci pypi publish
4.1 两种发布模式
publish子命令(src/publish.rs)根据是否传入--download-base-url走两条完全不同的路径:
模式 A:wheel-upload 模式(默认) 从--dist(默认<workspace>/target/wheels)目录里扫描与 workspace pyproject 的[project].name匹配的 wheel,逐个上传。--version作为 PEP 440 过滤条件,只发布该版本的产物。这正是pypi pack输出的去向。
模式 B:sdist 模式
cargo ci pypi publish --environment {staging|prod|test-pypi} --version V \ --download-base-url https://… --target TRIPLE [--target TRIPLE]…从https://base URL 下载本次发布的所有 wheel(每个--target一个),对在线字节计算 sha256,组装出"下载时再安装"的 sdist,然后只发布 sdist(filetype=sdist)。--download-base-url强制要求--version与至少一个--target(clap 层保证)。
另外还有一个纯构建模式--sdist-out DIR:只把 sdist 构建到本地目录而不上传,供发布流水线自行走 S3/CDN 分发(见 src/publish.rs 与 src/args.rs)。
4.2 目标环境与认证
目标环境由--environment决定(Environment枚举与解析在 src/publish.rs):
| 环境 | 认证方式 | 上传地址 |
|---|---|---|
staging | AWS CodeArtifact,读取 5 个环境变量(见下) | 由域名等信息拼接的https://{domain}-{owner}.d.codeartifact.{region}.amazonaws.com/pypi/{repository}/ |
prod | DBT_PYPI_PROD_TOKEN | https://upload.pypi.org/legacy/ |
test-pypi | DBT_PYPI_TEST_TOKEN | https://test.pypi.org/legacy/ |
staging 走 CodeArtifact:先调用aws codeartifact get-authorization-token(有效期 900 秒)换取令牌,再上传。CodeArtifact 上传 URL 的拼装逻辑有单元测试覆盖(src/publish.rs)。
4.3 上传协议的实现细节
上传逻辑(upload_dist,src/publish.rs)完整镜像了 twine 的多部分表单:
- 请求体字段名沿用 Warehouse 约定——重复字段用单数形式(
platform、supported_platform、license_file、provides_extra),即使python-pkginfo解析出来是复数,这是有测试背书的历史兼容点(见 src/publish.rs 的测试注释); - 同时提交
md5_digest与sha256_digest; - 重试与幂等:服务端 5xx 或瞬时网络错误最多重试 4 次、指数退避;而遇到"文件已存在"(Warehouse 返回 400
File already exists,CodeArtifact 返回 409)则直接视为成功——这保证部分发布后的重跑不会把流水线卡死(is_already_exists,src/publish.rs); - wheel 的 name/version/pyversion 从 PEP 491 文件名解析,sdist 的则从 PKG-INFO 读取。
4.4 sdist 模式:download-at-install 架构
sdist 的实现位于 src/sdist.rs,产物是一个很小的.tar.gz,内部只携带四样东西(build_sdist,src/sdist.rs):
{dist}-{version}/ ├── pyproject.toml # 声明内嵌 PEP 517 后端 ├── PKG-INFO # 富元数据 └── _dbt_sa_build/ ├── __init__.py # 内嵌的 PEP 517 后端(templates/sdist_build_backend.py) └── assets.json # 每个平台 wheel 的 filename + sha256 + base_url + 可选 notice模板源码在 crates/dbt-ci/templates/sdist_build_backend.py:用户pip install这个 sdist 时,后端读取assets.json,按当前平台选择对应 wheel 并校验 sha256 后下载安装——这就是"下载时再安装"的含义,sdist 本体只做路由。
构建流程(build_release_sdist,src/sdist.rs)会:
- 先拒绝非
https://的 base URL(require_https); - 逐个
--target计算 wheel 文件名({dist}-{version}-{py}-{abi}-{platform}.whl)并从 base URL 下载,对在线字节求 sha256——保证 manifest 与实际安装拉取的内容一致; - 元数据一致性校验(
check_wheel_metadata_agrees,src/sdist.rs):把下载 wheel 内METADATA的Requires-Python与Requires-Dist与 sdist 静态声明的做双向比对,不一致即失败。原因是 pip 会重新读取构建出的 wheel 元数据,而 uv 信任 sdist 的静态元数据不再复查——一旦Requires-Python或依赖声明错位,就可能把 wheel 装到不支持的 Python 上、或漏装依赖后首次运行即崩溃(详见 src/sdist.rs 的注释); - 每个 platform tag 只允许一个 wheel(两个 target 映射到同一 tag 时直接报错,src/sdist.rs);
- tar 头固定
mtime=0,保证 sdist 字节级可复现(src/sdist.rs)。
4.5 sdist 运行时元数据:--runtime-metadata-from
sdist 必须声明与它所引用的 wheel相同的requires-python与依赖(见 crates/dbt-ci/README.md 的 "sdist runtime metadata" 一节)。由于 dbt-core / dbt-oss 引用的是 maturin 扩展 wheel,其运行时元数据并不在 workspace 根 pyproject 中,因此发布时通过--runtime-metadata-from crates/dbt-python单独指定:实现上对应Spec::overlay_runtime_metadata(src/pyproject.rs),只覆盖requires-python与dependencies两个运行时字段,保留根 pyproject 的描述性元数据(name、description 等)。而dbt-core-experimental-parser发布的是无 Python 依赖的py3-none二进制 CLI wheel,无需该标志。
相关的--python-tag/--abi-tag参数在 sdist 模式下用于构造被引用 wheel 的文件名:默认py3/none(二进制 CLI wheel),maturin abi3 扩展 wheel 则用cp310/abi3之类(见 src/args.rs 与 src/pack.rs 的wheel_filename注释)。
五、Homebrew 分发:cargo ci homebrew render与publish
5.1 render:从发布 tarball 渲染 Formula
cargo ci homebrew render --tarballs-dir DIR --version X.Y.Z --url-template URL \ [--out PATH] [--formula-name NAME] [--binary-name NAME] [--conflicts-with NAME]…实现位于 src/homebrew/render.rs,输入输出如下:
- 输入:一个目录,内含按
{tarball-prefix}{version}-{target}.tar.gz命名的发布 tarball(默认前缀fs-v,即fs-v2.0.0-x86_64-apple-darwin.tar.gz);或者改用--sha256sums FILE直接提供一个sha256sum格式的清单文件(<hex> <filename>每行一个),避免重复下载与哈希。两者互斥且必须恰好提供一个(clap 的conflicts_with+required_unless_present保证); - 公式元数据:name / license / homepage 等从 workspace 根 pyproject 读取;
--url-template是下载 URL 模板,支持{filename}、{version}、{target}占位符; - 输出:一个
.rbformula,默认写到<workspace>/target/homebrew/Formula/{formula-name}.rb。
关键行为(均有源码与注释佐证):
- 只覆盖 Homebrew 支持的四个平台组合:
on_macos/on_linux×on_arm/on_intel,对应aarch64-apple-darwin、x86_64-apple-darwin、aarch64-unknown-linux-gnu、x86_64-unknown-linux-gnu;Windows tarball 被跳过(模块顶部注释 src/homebrew/render.rs); - 每个 tarball 的 SHA256 在进程内计算并写入 formula;
--binary-name指定 tarball 内二进制文件名(默认dbt);--install-as控制 Homebrew 安装后的命令名(bin.install "X" => "Y"),默认取 formula 名,例如dbt-core.rb可以把二进制装成dbt(见 src/args.rs 的注释);--conflicts-with用于声明与其他 formula 的冲突——典型场景是dbt与dbt-core都安装到/<prefix>/bin/dbt,所以互相声明冲突(src/args.rs)。
5.2 publish:推送到 tap 仓库
cargo ci homebrew publish --formula PATH --tap-repo URL --version X.Y.Z \ [--tap-branch B] [--token-env VAR] [--dry-run]实现位于 src/homebrew/publish.rs,策略如下:
- 浅克隆tap 仓库到临时目录(HTTPS URL 时用
git -c http.extraHeader=Authorization: Basic <b64(x-access-token:TOKEN)>注入认证,令牌不进 URL,日志与git remote -v保持干净,与 actions/checkout 同款做法); - 把渲染好的
.rb复制进Formula/; - 幂等:若
git status --porcelain为空(公式字节级一致),直接成功退出,不产生提交(见 src/homebrew/publish.rs); - 提交信息形如
{formula-stem} {version};可通过--commit-author/--commit-email(必须成对)覆盖提交身份,否则继承本地 git config; - 推送;
--dry-run则只展示 diff 不推送。
--token-env指定读取 PAT 的环境变量名,默认HOMEBREW_TAP_REPO_TOKEN(PAT 需要对 tap 仓库有repo写权限)。--tap-branch默认main。
六、环境变量一览
各环境需要的凭据集中在 crates/dbt-ci/README.md 的 "Env vars" 一节,源码中的读取位置对应 src/publish.rs 与 src/homebrew/publish.rs:
| 用途 | 环境变量 |
|---|---|
staging(AWS CodeArtifact,5 个缺一不可) | DBT_PYPI_STAGING_DOMAIN、DBT_PYPI_STAGING_DOMAIN_OWNER、DBT_PYPI_STAGING_REGION、DBT_PYPI_STAGING_REPOSITORY、DBT_PYPI_STAGING_PROFILE |
prod | DBT_PYPI_PROD_TOKEN |
test-pypi | DBT_PYPI_TEST_TOKEN |
homebrew publish | HOMEBREW_TAP_REPO_TOKEN(或--token-env指定的其他变量),PAT 需对 tap 仓库有写权限 |
缺失环境变量时,CodeArtifactTarget::resolve会把所有缺失项一次性列出报错,便于 CI 排障(有测试覆盖,src/publish.rs)。
七、典型 CI 发布序列
把 crates/dbt-ci/README.md 的 "Typical CI sequence" 与源码对应关系整理如下,这是一条端到端可执行的流水线:
# 1. 提升 workspace 版本号并刷新 Cargo.lock cargo ci bump-cargo-version X.Y.Z # 2. 每个平台分别编译(CI 的矩阵任务) cargo build --release --bin <bin> --target <triple> # 3. 把各平台二进制按 target triple 命名归入 binaries/(Windows 加 .exe) # binaries/x86_64-unknown-linux-gnu # binaries/x86_64-pc-windows-msvc.exe # … # 4. 打包成 py3-none-{platform} wheel cargo ci pypi pack --binaries-dir binaries --version X.Y.Z # 5. 先发布到 staging(CodeArtifact)验证,再发布到 prod(PyPI) cargo ci pypi publish --environment staging --version X.Y.Z cargo ci pypi publish --environment prod --version X.Y.Z # 6. 发布 download-at-install 的 sdist,指向本次发布的 wheel 托管地址 # (每个已发布 wheel 一个 --target) # 对于 dbt-core / dbt-oss 还需追加 # --python-tag cp311 --abi-tag abi3 --runtime-metadata-from crates/dbt-python, # 因为这两个包引用 maturin 扩展 wheel: cargo ci pypi publish --environment prod --version X.Y.Z \ --download-base-url https://github.com/dbt-labs/dbt-core/releases/download/vX.Y.Z \ --target x86_64-unknown-linux-gnu \ --target aarch64-unknown-linux-gnu \ --target x86_64-apple-darwin \ --target aarch64-apple-darwin \ --target x86_64-pc-windows-msvc # 7. Homebrew(需要发布 tarball,而非裸二进制) cargo ci homebrew render \ --tarballs-dir release-artifacts \ --version X.Y.Z \ --url-template "https://public.cdn.getdbt.com/fs/cli/{filename}" \ --conflicts-with dbt-core cargo ci homebrew publish \ --formula target/homebrew/Formula/dbt.rb \ --tap-repo https://github.com/dbt-labs/homebrew-dbt.git \ --version X.Y.Z对上面这条序列的几点说明:
- 第 4 步的 wheel 名来自 workspace 根 pyproject.toml 的
[project].name(当前为dbt-core),pack会按 PEP 503 规则规范化(-/_/.折叠为_,见 src/pack.rs); - 第 6 步的 sdist 上传后,PyPI 上的
dbt-core-2.xsdist 本身不含二进制,安装时才按平台拉取对应 wheel——这解释了为何 sdist 需要与 wheel 的Requires-Python/依赖完全一致(uv 信任静态元数据); - 第 7 步要求 tarball 存在;如果发布流程只产出裸二进制,应回到
pack路径而不是homebrew render。
八、dbt-core 2.x 的安装时命名提示(namespace notice)
crates/dbt-ci/README.md 的 "Install-time namespace notice" 一节描述了一个细致的兼容设计,其实现位于 src/sdist.rs:
- 命名
dbt_core(即dbt-core)且主版本 ≥ 2 的预发布sdist,会在assets.json的notice字段嵌入一段提示文本,PEP 517 后端在安装时打印,引导用户改用dbt/dbt-oss发行名; - 为什么只覆盖预发布?因为解析器不会主动选择预发布——用户只有显式传
--pre或固定dbt-core==2.0.0rc…才会装到预发布,因此这些安装正是最需要被引导的集合; dbt-core2.x 仍在与dbt/dbt-oss并行发布,所以这是"指向更优命名的提示(pointer),而非弃用声明(deprecation)";- 正式版、旧主版本以及其他发行名(
dbt-oss、dbt-core-experimental-parser等)不嵌入 notice;若想放宽到所有 2.x 的dbt-coresdist,只需在 src/sdist.rs 的install_notice中移除预发布判断(README 原文亦如此说明)。
对应的测试install_notice_targets_dbt_core_prereleases(src/sdist.rs)系统验证了:2.0.0rc1/2.0.0a4/2.0.0b2/2.0.0.dev5/3.1.0rc2均有提示,而2.0.0正式版、1.11.0rc1、dbt-oss、dbt-core-experimental-parser均无提示。
九、可复现性与发布安全设计小结
从源码可以归纳出 dbt-ci 在发布安全上的几个工程要点:
- 单一版本源:所有命令共享
release_version的 SemVer → PEP 440 转换,wheel 文件名、sdist 版本、formula 版本来自同一输入; - sdist 与 wheel 严格一致:
check_wheel_metadata_agrees在构建期拦截Requires-Python/Requires-Dist的任何偏差,防止 uv 信任静态元数据导致的错装; - 强制 HTTPS:sdist 模式在下载任何 wheel 之前就拒绝非 https base URL(src/sdist.rs),并有单元测试覆盖;
- 上传幂等:文件已存在视为成功,配合指数退避重试,让部分失败的发布可以安全重跑;
- 字节级可复现的 sdist:tar 头固定 mtime,便于审计与缓存;
- token 不进 URL:Homebrew tap 的认证通过 git
http.extraHeader注入,日志与 remote 配置均不泄露凭据。
这些行为均可在 crates/dbt-ci/src 各模块及其单元测试中直接验证,测试覆盖了版本解析、wheel 结构、元数据渲染、上传表单字段、sdist 清单、notice 注入等关键路径,是理解 dbt-core 发布流程的第一手资料。
【免费下载链接】dbtdbt enables data analysts and engineers to transform their data using the same practices that software engineers use to build applications.项目地址: https://gitcode.com/GitHub_Trending/db/dbt
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考