dbt-core 发布流水线实战:用 `cargo ci` 完成版本号提升、多平台 Wheel 打包与 PyPI / Homebrew 发布
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-version2.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].versionwrite_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 规范SemVerPEP 440X.Y.ZX.Y.ZX.Y.Z-alpha.NX.Y.ZaNX.Y.Z-beta.NX.Y.ZbNX.Y.Z-rc.NX.Y.ZrcNX.Y.Z-preview.NX.Y.ZrcNX.Y.Z-dev.NX.Y.Z.devN几点从源码中可以确认的细节preview是rc的别名两者都翻译为rcN见 src/release_version.rsSemVer build metadatabuild不被支持直接报错见 src/release_version.rs预发布必须是-lane.N的单一形态形如2.0.0-preview-nightly.176这类复合预发布会被拒绝见 src/release_version.rs 的测试。这个映射是后续 wheel 文件名、sdist 版本号的统一来源保证 Cargo 的版本写法与 PyPI 的 PEP 440 版本写法始终一致。三、把预编译二进制打包成 Wheelcargo ci pypi pack3.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_binariessrc/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 tagx86_64-unknown-linux-gnumanylinux_2_28_x86_64aarch64-unknown-linux-gnumanylinux_2_28_aarch64i686-unknown-linux-gnumanylinux_2_28_i686x86_64-unknown-linux-muslmusllinux_1_2_x86_64aarch64-unknown-linux-muslmusllinux_1_2_aarch64x86_64-apple-darwinmacosx_10_12_x86_64aarch64-apple-darwinmacosx_11_0_arm64x86_64-pc-windows-msvc/x86_64-pc-windows-gnuwin_amd64i686-pc-windows-msvcwin32未知 triple 返回None并被跳过避免生成无法被 pip 识别的 wheel。3.4 生成的 Wheel 内部结构pack_wheelsrc/pack.rs生成的标准 wheel 包含四个条目以dbt-sa-cli为例dbt_sa_cli-2.0.0a1.data/scripts/dbt-sa-cli # 预编译二进制Windows 下为 .exe0755 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 376path,sha256b64,size文件名遵循 PEP 491{dist}-{version}-py3-none-{platform}.whl见wheel_filenamesrc/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.rsdescription 正文按 RFC 822 约定放在空行之后RECORD哈希对每个文件计算sha256URL-safe base64与字节数自身行留空符合 PEP 376。--out未指定时默认输出到workspace/target/wheelssrc/pack.rs--bin-name用于覆盖 wheel 内脚本名默认取[project].name。四、发布到 PyPI 生态cargo ci pypi publish4.1 两种发布模式publish子命令src/publish.rs根据是否传入--download-base-url走两条完全不同的路径模式 Awheel-upload 模式默认 从--dist默认workspace/target/wheels目录里扫描与 workspace pyproject 的[project].name匹配的 wheel逐个上传。--version作为 PEP 440 过滤条件只发布该版本的产物。这正是pypi pack输出的去向。模式 Bsdist 模式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然后只发布 sdistfiletypesdist。--download-base-url强制要求--version与至少一个--targetclap 层保证。另外还有一个纯构建模式--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_distsrc/publish.rs完整镜像了 twine 的多部分表单请求体字段名沿用 Warehouse 约定——重复字段用单数形式platform、supported_platform、license_file、provides_extra即使python-pkginfo解析出来是复数这是有测试背书的历史兼容点见 src/publish.rs 的测试注释同时提交md5_digest与sha256_digest重试与幂等服务端 5xx 或瞬时网络错误最多重试 4 次、指数退避而遇到文件已存在Warehouse 返回 400File already existsCodeArtifact 返回 409则直接视为成功——这保证部分发布后的重跑不会把流水线卡死is_already_existssrc/publish.rswheel 的 name/version/pyversion 从 PEP 491 文件名解析sdist 的则从 PKG-INFO 读取。4.4 sdist 模式download-at-install 架构sdist 的实现位于 src/sdist.rs产物是一个很小的.tar.gz内部只携带四样东西build_sdistsrc/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_sdistsrc/sdist.rs会先拒绝非https://的 base URLrequire_https逐个--target计算 wheel 文件名{dist}-{version}-{py}-{abi}-{platform}.whl并从 base URL 下载对在线字节求 sha256——保证 manifest 与实际安装拉取的内容一致元数据一致性校验check_wheel_metadata_agreessrc/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.rstar 头固定mtime0保证 sdist 字节级可复现src/sdist.rs。4.5 sdist 运行时元数据--runtime-metadata-fromsdist 必须声明与它所引用的 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_metadatasrc/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 wheelmaturin abi3 扩展 wheel 则用cp310/abi3之类见 src/args.rs 与 src/pack.rs 的wheel_filename注释。五、Homebrew 分发cargo ci homebrew render与publish5.1 render从发布 tarball 渲染 Formulacargo 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_withrequired_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-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 的冲突——典型场景是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.extraHeaderAuthorization: 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_TOKENPAT 需要对 tap 仓库有repo写权限。--tap-branch默认main。六、环境变量一览各环境需要的凭据集中在 crates/dbt-ci/README.md 的 Env vars 一节源码中的读取位置对应 src/publish.rs 与 src/homebrew/publish.rs用途环境变量stagingAWS CodeArtifact5 个缺一不可DBT_PYPI_STAGING_DOMAIN、DBT_PYPI_STAGING_DOMAIN_OWNER、DBT_PYPI_STAGING_REGION、DBT_PYPI_STAGING_REPOSITORY、DBT_PYPI_STAGING_PROFILEprodDBT_PYPI_PROD_TOKENtest-pypiDBT_PYPI_TEST_TOKENhomebrew 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. 先发布到 stagingCodeArtifact验证再发布到 prodPyPI 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-corepack会按 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 noticecrates/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-core2.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_prereleasessrc/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 信任静态元数据导致的错装强制 HTTPSsdist 模式在下载任何 wheel 之前就拒绝非 https base URLsrc/sdist.rs并有单元测试覆盖上传幂等文件已存在视为成功配合指数退避重试让部分失败的发布可以安全重跑字节级可复现的 sdisttar 头固定 mtime便于审计与缓存token 不进 URLHomebrew 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),仅供参考