dbt-core 发布流水线实战:用 `cargo ci` 完成版本号提升、多平台 Wheel 打包与 PyPI / Homebrew 发布
2026/9/15 4:03:42 网站建设 项目流程

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:

  1. 先调用validate_release_version校验版本形状(必须是X.Y.ZX.Y.Z-{lane}.N,见后文"版本形状"一节),非法则退出码 2;
  2. 定位 cargo workspace 根目录,读取根 Cargo.toml;
  3. toml_edit以保留注释与格式的方式,把新版本写入[workspace.package].versionwrite_workspace_version,见 src/bump_cargo_version.rs);
  4. 除非指定--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 规范:

SemVerPEP 440
X.Y.ZX.Y.Z
X.Y.Z-alpha.NX.Y.ZaN
X.Y.Z-beta.NX.Y.ZbN
X.Y.Z-rc.NX.Y.ZrcN
X.Y.Z-preview.NX.Y.ZrcN
X.Y.Z-dev.NX.Y.Z.devN

几点从源码中可以确认的细节:

  • previewrc的别名,两者都翻译为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-darwin

collect_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 triplePEP 491 platform tag
x86_64-unknown-linux-gnumanylinux_2_28_x86_64
aarch64-unknown-linux-gnumanylinux_2_28_aarch64
i686-unknown-linux-gnumanylinux_2_28_i686
x86_64-unknown-linux-muslmusllinux_1_2_x86_64
aarch64-unknown-linux-muslmusllinux_1_2_aarch64
x86_64-apple-darwinmacosx_10_12_x86_64
aarch64-apple-darwinmacosx_11_0_arm64
x86_64-pc-windows-msvc/x86_64-pc-windows-gnuwin_amd64
i686-pc-windows-msvcwin32

未知 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]表(namedescriptionrequires-pythondependenciesclassifiersurlsauthorslicensereadme等),完整支持 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):

环境认证方式上传地址
stagingAWS CodeArtifact,读取 5 个环境变量(见下)由域名等信息拼接的https://{domain}-{owner}.d.codeartifact.{region}.amazonaws.com/pypi/{repository}/
prodDBT_PYPI_PROD_TOKENhttps://upload.pypi.org/legacy/
test-pypiDBT_PYPI_TEST_TOKENhttps://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 约定——重复字段用单数形式platformsupported_platformlicense_fileprovides_extra),即使python-pkginfo解析出来是复数,这是有测试背书的历史兼容点(见 src/publish.rs 的测试注释);
  • 同时提交md5_digestsha256_digest
  • 重试与幂等:服务端 5xx 或瞬时网络错误最多重试 4 次、指数退避;而遇到"文件已存在"(Warehouse 返回 400File 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)会:

  1. 先拒绝非https://的 base URL(require_https);
  2. 逐个--target计算 wheel 文件名({dist}-{version}-{py}-{abi}-{platform}.whl)并从 base URL 下载,对在线字节求 sha256——保证 manifest 与实际安装拉取的内容一致;
  3. 元数据一致性校验check_wheel_metadata_agrees,src/sdist.rs):把下载 wheel 内METADATARequires-PythonRequires-Dist与 sdist 静态声明的做双向比对,不一致即失败。原因是 pip 会重新读取构建出的 wheel 元数据,而 uv 信任 sdist 的静态元数据不再复查——一旦Requires-Python或依赖声明错位,就可能把 wheel 装到不支持的 Python 上、或漏装依赖后首次运行即崩溃(详见 src/sdist.rs 的注释);
  4. 每个 platform tag 只允许一个 wheel(两个 target 映射到同一 tag 时直接报错,src/sdist.rs);
  5. 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-pythondependencies两个运行时字段,保留根 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 renderpublish

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-darwinx86_64-apple-darwinaarch64-unknown-linux-gnux86_64-unknown-linux-gnuWindows 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 的冲突——典型场景是dbtdbt-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,策略如下:

  1. 浅克隆tap 仓库到临时目录(HTTPS URL 时用git -c http.extraHeader=Authorization: Basic <b64(x-access-token:TOKEN)>注入认证,令牌不进 URL,日志与git remote -v保持干净,与 actions/checkout 同款做法);
  2. 把渲染好的.rb复制进Formula/
  3. 幂等:若git status --porcelain为空(公式字节级一致),直接成功退出,不产生提交(见 src/homebrew/publish.rs);
  4. 提交信息形如{formula-stem} {version};可通过--commit-author/--commit-email(必须成对)覆盖提交身份,否则继承本地 git config;
  5. 推送;--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_DOMAINDBT_PYPI_STAGING_DOMAIN_OWNERDBT_PYPI_STAGING_REGIONDBT_PYPI_STAGING_REPOSITORYDBT_PYPI_STAGING_PROFILE
prodDBT_PYPI_PROD_TOKEN
test-pypiDBT_PYPI_TEST_TOKEN
homebrew publishHOMEBREW_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.jsonnotice字段嵌入一段提示文本,PEP 517 后端在安装时打印,引导用户改用dbt/dbt-oss发行名;
  • 为什么只覆盖预发布?因为解析器不会主动选择预发布——用户只有显式传--pre或固定dbt-core==2.0.0rc…才会装到预发布,因此这些安装正是最需要被引导的集合;
  • dbt-core2.x 仍在与dbt/dbt-oss并行发布,所以这是"指向更优命名的提示(pointer),而非弃用声明(deprecation)";
  • 正式版、旧主版本以及其他发行名(dbt-ossdbt-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.0rc1dbt-ossdbt-core-experimental-parser均无提示。

九、可复现性与发布安全设计小结

从源码可以归纳出 dbt-ci 在发布安全上的几个工程要点:

  1. 单一版本源:所有命令共享release_version的 SemVer → PEP 440 转换,wheel 文件名、sdist 版本、formula 版本来自同一输入;
  2. sdist 与 wheel 严格一致check_wheel_metadata_agrees在构建期拦截Requires-Python/Requires-Dist的任何偏差,防止 uv 信任静态元数据导致的错装;
  3. 强制 HTTPS:sdist 模式在下载任何 wheel 之前就拒绝非 https base URL(src/sdist.rs),并有单元测试覆盖;
  4. 上传幂等:文件已存在视为成功,配合指数退避重试,让部分失败的发布可以安全重跑;
  5. 字节级可复现的 sdist:tar 头固定 mtime,便于审计与缓存;
  6. token 不进 URL:Homebrew tap 的认证通过 githttp.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询