Black 发布流程全解:CalVer 版本策略、release.py 自动化与 GitHub Actions 发布流水线
本文基于 Black 仓库的官方文档 发布流程 展开,系统讲解 Black 的发布节奏与 CalVer 版本策略、完整的发版操作步骤,以及背后由 release.py 与 GitHub Actions 工作流(build and publish、publish binaries、docker、post release)组成的发布自动化体系。读完本文,你可以完整掌握:如何计算下一个版本号、如何生成发版 PR、发布产物(sdist、wheel、mypyc 编译 wheel、独立可执行文件、Docker 镜像)分别由哪个工作流构建并分发到哪里,以及发版后需要关注的收尾动作。
发布节奏与 CalVer 版本策略
Black 的发布节奏目标是:每 1~2 个月将 main 分支上的内容发布一次。这一节奏在"尽快把已合并的改进与修复送达用户"和"不让用户群体被过密的版本切分"之间取得平衡,同时保持维护者工作量的稳定与可预测。如果 main 上没有足够多的新内容,可以跳过某个月的发布。
几条硬性约束值得注意:
- 1 月份的理想发布不应跳过。按照 Black 的稳定性策略(stability policy),每个日历年度的第一个版本允许修改 stable 风格。将 stable 风格的变更限定在 1 月发布,可以让用户对风格变化形成可预期的预期。
- 没有严重的回归或需要立即修补的 bug 时,每月最多发布一次。版本号本身没有成本,但每次发布都需要维护者投入精力执行"切版",并处理发布后可能出现的连锁问题;更频繁的发布收益递减很快。
- 版本号遵循 CalVer 标准的
YY.M.N格式:除非本月已经发过版,N应为0。例如 2026 年 1 月的第一个版本号为26.1.0。
release.py 如何计算当前版本与下一个版本
版本号并非人工拍定,而是由 release.py 从 git tag 推导。从源码实现看:
- get_git_tags() 执行
git tag并只保留以数字开头的 tag(即 CalVer 版本 tag); - tuple_calver() 把
YY.M.N字符串转成整数元组用于排序,无法解析的 tag(如 alpha/beta 预发布 tag)回退为(0, 0, 0),源码中留有 TODO 说明对预发布版本的排序支持尚未完善; - get_current_version() 取排序后的最新 tag 作为当前版本;
- get_next_version() 先用
datetime.today().strftime("%y.%m")取当前年月并去掉月份的前导零(如01→1),再扫描同月的历史 tag:若同月没有正式版则返回YY.M.0,否则在最后一个同月版本的N上 +1。
这套逻辑有专门的单元测试 release_tests.py 保障,例如:mock 当前日期为 69.01(故意带前导零以验证去零逻辑),历史 tag 为 ["1.1.0", "69.1.0", "69.1.1", "2.2.0"] 时,get_current_version() 应返回 69.1.1,get_next_version() 应返回 69.1.2。这些测试由工作流 release_tests.yml 在 Python 3.12~3.15 × macOS / Ubuntu / Windows 的矩阵上运行——这也印证了文档中"release.py 仅在 Python 3.12+ 上测试过"的说法。该工作流仅在 scripts/release.py、scripts/release_tests.py 或工作流文件本身变更时触发,并以 coverage run scripts/release_tests.py 方式执行。
切版操作:权限要求与前置条件
执行发布的人必须拥有 Black 仓库的 write 权限。 发布流程的万米高空视角是:先准备一个"发版 PR",然后发布一个 GitHub Release;后者的 published 事件会触发所有发布自动化工作流,由其构建并分发全部发布产物。
发布 PR 的生成依赖 scripts/release.py 脚本,日常使用方式是:
python3 scripts/release.py --help # 查看可用参数
python3 scripts/release.py # 生成发版所需的大部分变更
python3 scripts/release.py --debug # 开启 DEBUG 日志
从 parse_args() 可以看到脚本仅有两个开关:-d/--debug(输出调试日志)与 -a/--add-changes-template(向 CHANGES.md 添加新的 Unreleased 模板,发版后使用,详见后文)。执行默认路径时,main() 会先打印检测到的当前版本与计算出的下一个版本(日志输出到 stdout,方便复制粘贴),然后调用 update_repo_for_release() 修改仓库文件。
切版步骤详解
以下是完整的切版流程,与官方文档保持一致:
1. 确定版本号
运行 python3 scripts/release.py 后,脚本会把计算好的 YY.M.N 版本号打印到 stdout(Next version will be ...),直接复制即可。
2. 检查 changelog 分区是否正确
确认自上一个版本以来,没有任何 changelog 条目被放进了错误的分区。官方推荐的做法是运行:
git diff origin/stable CHANGES.md
通过对比 stable 分支(指向最近一次发布 tag,见下文 post release 工作流)与 main 上的 CHANGES.md 差异,可以快速审读本次将发布的全部变更条目。
3. 提交发版 PR
提交一个编辑 CHANGES.md 与文档的 PR,将最新的变更"版本化"。核心命令:
python3 scripts/release.py [--debug]
从源码看,这一步实际做了两类修改:
(a)清理 CHANGES.md 模板 —— cleanup_changes_template_for_release() 依次执行三个变换:
- 将
## Unreleased标题替换为## Version <下一版本号>; - 用正则删除所有 HTML 注释(即模板中指导贡献者如何填写的
<!-- ... -->说明块); - 删除空的子分区标题(即本次发布没有条目的
### xxx小节)。
CHANGES.md 的 Unreleased 模板本身由 NEW_VERSION_CHANGELOG_TEMPLATE 定义,包含 Highlights、Stable style、Preview style、Configuration、Packaging、Parser、Performance、Output、Blackd、Integrations、Documentation 等分区,每个分区都附有"PR 作者请注明:changelog 条目应写 PR 编号而非 issue 编号"等填写指引。
(b)更新文档中的版本号引用 —— update_version_in_docs() 会把文档中出现的"当前版本"字符串全局替换为"下一版本"。注意 SourceFiles 类 中维护的 version_doc_paths 实际包含 三个文件,比文档正文提到的多一个:
- docs/integrations/source_version_control.md(文档正文提及)
- docs/usage_and_configuration/the_basics.md(文档正文提及)
- docs/guides/using_black_with_jupyter_notebooks.md(仅由脚本维护,文档正文未列出)
4. 手动修正(如果脚本失败)
如果 release.py 执行失败,需要手动完成等价编辑:
- 把
## Unreleased标题替换为版本号; - 删除当前版本下的空分区;
- (可选)通读并编辑 changelog,如移动条目、修正错别字、改写措辞。
5. 等待 CI 通过
发版 PR 合并后,等待所有 CI 通过。如果 CI 失败,必须停下来调查失败原因——Black 的惯例是先修好失败的 CI 再切版。
6. 创建 GitHub Release(Draft)
- 点击
Choose a tag,输入版本号,然后选择自动出现的Create new tag: YY.M.N on publish选项; - 确认新 tag 指向
main分支; - 确保 Release 标题设置为版本号(
YY.M.N),否则默认标题会是最后一条 commit 的标题; - 将当前版本的 changelog 原始 Markdown 复制粘贴到描述框中。
7. 发布,触发自动化
点击发布后,release: published 事件会触发全部发布工作流(详见下一节),构建与上传工作由自动化接管。
8. CI 完成后补上下一轮的 Unreleased 模板
CI 跑完后,向 CHANGES.md 追加一个供下次发布使用的空模板:
python3 scripts/release.py --add-changes-template # 或简写 -a
对应实现是 add_template_to_changes():若 CHANGES.md 中已经存在 ## Unreleased 则报错返回 1(避免重复插入),否则把 NEW_VERSION_CHANGELOG_TEMPLATE 插入到 # Change Log 标题之后。若脚本失败,回退方案是从 release.py 源码中手动复制模板粘贴。
值得一提的是,这一步实际上已经由 post release 工作流 中的 new-changelog job 自动完成(见下文),手动命令是它的兜底手段。
9. 观察并验证发布工作流
至此发布基本完成。良好的实践是去观察所有发布工作流是否通过——当然,任何工作流失败时你都会收到 GitHub 通知。若有失败,不要慌张:去阅读对应工作流的日志与配置文件,反推出修复方案。
关于 hotfix 的官方建议:发布产物到达 PyPI 后,可能会看到新 issue 报告回归。回归不等于必须发 hotfix——除非回归严重且影响大量用户,否则通常无需紧急发版。最终请运用你的最佳判断,并征询其他维护者的意见。
发布自动化:四个 GitHub Actions 工作流
Black 的全部发布自动化都基于 GitHub Actions,工作流配置是仓库 .github/workflows 目录下的 YAML 文件,统一由 GitHub Release 的发布事件(release: published)触发。下面按官方文档的结构逐一解析,并结合工作流源码补充实现细节。
build and publish:构建并上传 PyPI 产物
这是主发布工作流,配置文件为 pypi_upload.yml。它构建 sdist 与 wheel 并上传到 PyPI——绝大多数用户从 PyPI 安装 Black。工作流同时监听 pull_request 与 push: main 事件以做构建演练,但上传 job 仅在 release 事件下执行(if: github.event_name == 'release')。整个流程分为三组 job:
(1)sdist + pure wheel(hatch job)
hatch job 使用 Hatch 构建 sdist 与纯 Python wheel(只包含 Python 代码的 wheel)。这类产物是通用型的,基本可在 Python 支持的任意平台上使用。关键步骤:
- 通过
python -m pip install --group hatch安装 pyproject.toml 中定义的hatch依赖组(含固定版本的 hatch、hatch-fancy-pypi-readme、hatch-vcs 等); - 执行
python -m hatch build构建dist/,再作为 artifact 上传供发布 job 下载。
版本号的来源也值得说明:pyproject.toml 配置 [tool.hatch.version] source = "vcs",即由 hatch-vcs 从 git 元数据推导版本并生成 src/_black_version.py;仓库根目录的 .git_archival.txt 中的 describe-name: $Format:%(describe:tags=true,match=[0-9]*)$ 保证 sdist 中也带有正确的 git describe 信息。此外 tool.hatch.metadata.hooks.fancy-pypi-readme 会把 README.md 与 CHANGES.md 拼接成 PyPI 页面上的项目说明。
(2)mypyc wheels(configure + mypyc 矩阵 job)
Black 使用 mypyc 把核心模块编译为 CPython C 扩展以获得显著的性能提升。mypyc 构建的 wheel 与平台和 Python 版本绑定,因此需要矩阵化构建。从 pypi_upload.yml 看:
configurejob 通过cibuildwheel --print-build-identifiers分别为 linux / macos / windows(含 windows ARM64)打印构建标识符,汇总成构建矩阵输出。完整矩阵仅在 release 事件与 push 到 main 时生成;普通 PR 只构建缩小的两个标识符(cp310Linux +cp314Windows),除非 PR 带有ci: build all wheels标签——这是对 PR 检查耗时的优化;- 每个
mypyc wheels <标识符>job 在对应平台 runner 上执行cibuildwheel . --only <标识符>,产物上传为wheelhouse/artifact; - 构建目标与跳过规则定义在 pyproject.toml 的
[tool.cibuildwheel]段:build = "cp31*"(CPython 3.10+,仅 64 位),跳过 musllinux、32 位 win32、PyPy,以及 free-threaded 构建(cp31?t-*,mypyc 对其支持不佳);Linux 使用manylinux_2_28镜像;编译环境设置HATCH_BUILD_HOOKS_ENABLE=1与MYPYC_OPT_LEVEL=3,即真正启用 mypyc 编译钩子; - mypyc 编译的排除名单同样在 pyproject.toml 中:
blackd、blib2to3的部分模块、output.py、debug.py、__main__.py等不参与编译,理由从注释可见("blackd 没有编译的好理由"、"性能不敏感"、"编译后测试套件会挂"等); - 每个 wheel 构建完会跑测试:
pytest {project} -k "not incompatible_with_mypyc" -n auto,确保编译版 Black 行为一致。
(3)publish-hatch / publish-mypyc:上传 PyPI
这两个 job 分别下载 sdist-and-pure-wheel 与全部 *-mypyc-wheels artifact,然后使用 PyPI 的 Trusted publishing(可信发布,基于 OIDC 的 id-token: write 权限,而非 API token)将 sdist 与所有 wheel 上传到 PyPI。两个 job 都挂 environment: release 并声明 permissions: id-token: write,注释明确写道 "Required for PyPI trusted publishing"。
publish binaries:构建独立可执行文件
配置文件为 publish_binaries.yml。该工作流用 PyInstaller 为多个平台构建原生可执行文件,让用户无需安装 Python 运行时即可运行 Black。实现细节:
- 构建矩阵覆盖 5 个目标:
windows-latest(x86_64)、windows-11-arm、ubuntu-latest、ubuntu-24.04-arm、macos-latest,产物分别为black_windows.exe、black_windows-arm.exe、black_linux、black_linux-arm、black_macos; - 安装依赖使用
python -m pip install ".[colorama]" --group pyinstaller,随后执行python -m PyInstaller -F ... src/black/__main__.py做单文件打包,并通过--add-data 'src/blib2to3'把内置的 blib2to3 包数据打进可执行文件;Linux 产物额外加--strip去符号; - 上传前有一个冒烟测试:运行
./dist/<产物> --version与./dist/<产物> src --verbose确认可执行文件可用; - 最后通过
gh release upload把二进制作为资产挂到对应的 GitHub Release 上(contents: write权限)。
按照文档说明,这些二进制挂在 GitHub Release 上、目前只能通过 IPv4 下载(GitHub 尚无 IPv6 访问);工作流也支持 workflow_dispatch 手动指定 tag 重跑。
docker:构建官方 Docker 镜像
配置文件为 docker.yml。该工作流使用 Docker buildx(QEMU 支持)构建官方 Black 镜像的 arm64 与 amd64/x86_64 版本,推送到 Docker Hub 的 pyfound/black 仓库。两个要点:
- 触发条件比文档描述更宽:除 release 发布事件外,每次 push 到
main也会运行; - tag 策略(见 push job):构建 amd64 与 arm64 两个平台镜像后,
pushjob 用docker buildx imagetools create合并 manifest 并打 tag。release 事件下打latest与版本号 tag,预发布另打latest_prerelease,正式发版打latest_release;非 release 的 main 推送只打latest与latest_non_release。
文档还注明:当前该工作流使用关联到维护者账号的 API Token(DockerHub 凭证经 DOCKERHUB_USERNAME / DOCKERHUB_TOKEN secrets 注入)。
post release:发布后的仓库维护
配置文件为 post_release.yml,由 release: published 触发(且仅在非预发布时执行),包含两个 job:
update-stable:把 stable 分支强制推进到最新 release tag。实现非常直接——以 stable 为 ref 检出后执行 git reset --hard "${TAG_NAME}" && git push(见 post_release.yml)。这样维护者就不用记得在发版后某个时点手动更新 stable 分支。这也解释了切版第 2 步为何能用 git diff origin/stable CHANGES.md 来审读"本次发布包含的变更":stable 始终等价于上一次发布。
new-changelog:开一个新 PR,把 "Unreleased" 分区加回 changelog。实现是检出 main(带 tags)后执行 python scripts/release.py -a——与上文第 8 步的手动命令完全一致——再用 git-auto-commit-action 提交到 ci/new-changelog 分支,并以 gh pr create 创建标题为 "Add new changelog" 的 PR(标签 ci: skip news、C: maintenance)。这个 PR 刻意不自动合并:万一发布出问题需要重切版本,changelog 状态还可以回退处理。
关键文件索引
| 类别 | 文件 | 作用 |
|---|---|---|
| 流程文档 | docs/contributing/release_process.md | 本文主体依据的发布流程说明 |
| 发布脚本 | scripts/release.py | 版本计算、CHANGES.md 版本化、Unreleased 模板管理 |
| 脚本测试 | scripts/release_tests.py | CalVer 计算逻辑的单元测试 |
| 测试工作流 | .github/workflows/release_tests.yml | 在 3.12+ 与三大操作系统上运行发布脚本测试 |
| PyPI 发布 | .github/workflows/pypi_upload.yml | sdist / 纯 wheel / mypyc wheel 构建与可信发布 |
| 二进制发布 | .github/workflows/publish_binaries.yml | PyInstaller 多平台可执行文件构建 |
| Docker 镜像 | .github/workflows/docker.yml | amd64 + arm64 多架构镜像构建与 tag 策略 |
| 发布后维护 | .github/workflows/post_release.yml | 更新 stable 分支、自动提交新 changelog 模板 PR |
| 构建配置 | pyproject.toml | hatch 构建、mypyc 编译钩子、cibuildwheel 目标 |
| 变更日志 | CHANGES.md | 所有版本变更的 Unreleased 模板与历史条目 |
总结
Black 的发布体系可以概括为"一个脚本 + 一条手动 PR + 四条自动化工作流":release.py 负责版本号推导与 changelog/文档的版本化编辑;维护者手动完成"发版 PR 合并 → CI 全绿 → 创建并发布 GitHub Release"这条主线;随后 release: published 事件驱动 PyPI 发布、独立可执行文件发布、Docker 镜像发布 与 仓库收尾 四条工作流,把 sdist、纯 Python wheel、各平台 mypyc wheel、五个平台的原生二进制、双架构 Docker 镜像分发到 PyPI、GitHub Release 与 Docker Hub,并自动维护 stable 分支与下一轮 changelog 模板。整个流程中几乎不依赖 API token(PyPI 用可信发布、GitHub 用 github.token),并通过 release_tests.py 的跨平台测试把"最容易出错的版本计算逻辑"锁在自动化之下。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00