Anki 版本发布全流程指南:从 prepare 脚本到 GitHub Actions 的自动化发布管线
【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki
导读
Anki(间隔重复记忆卡片程序)的版本发布并不是手工打 tag、上传安装包的简单操作,而是一条**「本地脚本准备 + CI 自动验证 + GitHub Actions 跨平台构建发布」**的自动化流水线。本文以仓库中的官方发布文档 docs/releasing.md 为主体,结合 tools/prepare_release.py、.github/workflows/release.yml、release.just 等源码,完整讲解 Anki 的版本号规范、release/YY.MM分支工作流、发布参数矩阵、环境审批门禁、测试发布方法,以及独立的anki-audio音频包发布流程。读完后,你将能理解并复现 Anki 从开发分支到 PyPI/GitHub Release 的完整发布链路。
发布流水线总览
Anki 的版本发布由两部分组成:
tools/prepare_release.py(本地执行)—— 由维护者在自己的机器上运行,负责校验版本号、切换对应 release 分支、检查 CI 是否通过、确认没有重复的 tag 或 release、同步翻译文件,最后提交更新后的.version并推送分支;.github/workflows/release.yml(CI 执行)—— 等分支上的 CI 通过后手动触发(workflow_dispatch),负责构建全平台安装包和 wheel,并可选地对 macOS/Windows 产物签名、创建 GitHub 草稿 Release、发布 wheel 到 TestPyPI 与 PyPI。
两个阶段的分工可以这样理解:prepare 脚本负责"决定发什么版本"并产出带版本号的提交,release 工作流负责"把该提交变成真正的发布物"。release.yml使用了名为release的 concurrency group,且cancel-in-progress: false,因此同一时间只允许一个发布构建运行,不会出现两个发布互相覆盖的情况。
流程时序图
Prepare 脚本的环境要求
tools/prepare_release.py在维护者本机运行,因此对环境有明确要求:
| 要求 | 说明 |
|---|---|
| 干净的工作区 | 脚本第一步执行git status --porcelain检查,存在未提交变更会直接报错please commit any outstanding changes first |
已认证的ghCLI | 需要 push 权限访问ankitects/anki以及两个翻译仓库(ftl/core-repo/core与ftl/qt-repo/desktop) |
rsync在 PATH 中 | 用于把翻译模板拷贝到翻译仓库 |
带packaging模块的 Python | 项目通过uv环境提供该模块 |
从源码看,脚本的sync_translations()会先对两个翻译模块执行git checkout main+git pull origin main(拉取最新翻译),再用rsync -ai --delete把ftl/core、ftl/qt下的模板同步过去,有变更则提交并推送Update templates。脚本在提交.version之前就会推送翻译模板提交,所以如果这一步之后失败,翻译仓库的推送已经生效;但重新运行脚本是安全的——当没有新内容需要拷贝时,翻译步骤是空操作(git commit遇到 "nothing to commit" 会静默跳过)。
版本号格式:日历化 + PEP 440
Anki 采用日历版本(calendar versioning),并遵循 PEP 440 规范。仓库根目录的.version文件当前内容为26.08.1,格式规则由 tools/validate_version.py 中的正则^\d+\.(\d{2})(\.\d+)?(a\d+|b\d+|rc\d+)?$强制约束:
YY.MM:稳定版,如26.04;- 可选
.patch:如26.04.1; - 预发布后缀:
a1(alpha)、b1(beta)、rc1(release candidate); - 月份必须两位补零(
\d{2}且校验月份 ≤ 12),即26.05合法、26.5非法。
官方文档给出的示例:26.05b1(beta)、26.05rc1(候选版)、26.05(稳定版)、26.05.1(补丁版)。validate_version还会用packaging.version.Version做 PEP 440 解析,并强制要求新版本号必须大于当前.version,否则抛出version must be greater than current ...。
prepare_release.py中的release_branch()从版本号推导分支名:取base_version的release部分并把月份补零后拼成release/YY.MM,例如26.08b1→release/26.08。因此脚本没有 branch 参数,分支完全由版本号决定。
Release 分支工作流
所有版本都从release/YY.MM分支切出。分支名只含主版本号(YY.MM),不含预发布后缀——同一个周期的 beta、rc、稳定版都从同一条分支产出。
标准发布流程
# 1. 从 main 创建 release 分支并推送 git checkout -b release/26.05 main git push origin release/26.05 # 2. CI 在 push 到 release/** 分支时自动运行 # 3. 准备发布(在分支上更新 .version) just release::prepare --version 26.05b1 # 4. 在 TestPyPI 上验证 just release::testpypi --ref release/26.05 # 5. 发布完整版本 just release::public --ref release/26.05 # 6. 同一周期的后续预发布或稳定版:用新版本号重复步骤 3-5 # (例如 26.05b2、26.05rc1、26.05) # 7. 稳定版发布后,把 release 分支合并回 main,带回 .version 更新和 cherry-pick 的修复 git checkout main git merge release/26.05 git push origin main # 8. 稳定版发布后删除 release 分支安全修复与热修复(hotfix)发布
安全修复流程与标准流程的关键差异:先建 Security Advisory + 临时私有 fork,在私有 fork 里走正常 PR 流程修 bug,在修复就绪前不公开 PR 也不发布 advisory。修复就绪后的步骤:
# 1. 从最新 release tag 创建 release 分支 git checkout -b release/26.05 26.05 # 2. 把修复 cherry-pick 到 release 分支 # 3. 推送分支并等待 CI git push origin release/26.05 # 4. 准备并发布 just release::prepare --version 26.05.1 just release::public --ref release/26.05 # 5. 合并回 main # 6. 安全补丁:发布 advisory 并视情况致谢报告者热修复场景下 prepare 脚本可以通过--skip-ci-check跳过 CI 检查,release.yml的skip-ci-check输入项在注释中明确写明"for hotfix releases from non-main branches"。
Release 工作流的任务拓扑
.github/workflows/release.yml的prepare作业首先运行三个门禁检查:拒绝不兼容输入(draft-release=true且sign=false时报错Draft releases must be signed)、校验版本与.version是否一致、检查 CI 结论和重复 tag/release。随后各平台构建作业并行执行:
值得注意的实现细节(来自 release.yml 源码):
- macOS ARM/Intel 分成两个独立 job 而非矩阵:注释说明二者"很可能分叉"(签名怪癖、Xcode 版本、交叉编译标志、Rosetta 兼容处理),条件环境表达式已经足够复杂,再加矩阵维度会更难维护;
- Windows ARM 走四段 job 链:因为
azure/artifact-signing-action不支持 ARM runner,必须"ARM 构建 → x64 签 EXE → ARM 打包 MSI → x64 签 MSI"; - Linux x86 与 ARM 分开:wheel 构建用 ubuntu-22.04(目标 glibc 2.35),ARM 安装包用 ubuntu-24.04-arm(Qt wheels 只提供 24.04+ 的 ARM 包,且 Briefcase 不捆绑 glibc,导致 ARM 安装包运行时要求 glibc 2.39+,比 x86 安装包的 2.35 更高);
- Linux 构建作业还会编译打包 fcitx5-qt(输入法框架插件),保证 Linux 用户的中文等输入法体验。
输入参数详解
prepare_release.py 参数
脚本只接受一个必填的version位置参数和一个可选的--skip-ci-check标志。用法:
python3 tools/prepare_release.py 26.06 python3 tools/prepare_release.py 26.06b1 --skip-ci-checkrelease.yml 的 workflow_dispatch 输入
| 输入 | 效果 |
|---|---|
sign | 对 macOS 和 Windows 产物签名。需要release环境。为 false 时对应 job 上传未签名产物且不接触签名密钥 |
draft-release | 创建带自动生成发布说明和安装包的 GitHub 草稿 Release。要求sign=true、release环境、CI 通过(除非跳过)、无重复 tag/release、version与.version一致 |
publish-testpypi | 发布 wheel 到 TestPyPI。需要release环境 |
publish-pypi | 发布 wheel 到 PyPI。需要release环境、CI 通过(除非跳过)、version与.version一致;会先运行并等待 TestPyPI 发布 job;除非同时开启draft-release,否则不要求签名 |
skip-ci-check | 跳过 CI 状态检查,用于热修复发布 |
version | 对draft-release或publish-pypi必须与.version匹配;对纯构建、仅签名或仅 TestPyPI 的运行会被忽略(自动使用分支上的.version) |
所有布尔输入默认false。非发布运行直接使用仓库中已有的.version,因此不需要 prepare 步骤也能构建。一次正常公开发布需要开启前四个布尔量:
sign=true, draft-release=true, publish-testpypi=true, publish-pypi=true从源码看preparejob 的校验逻辑:只有draft-release或publish-pypi为 true 时才认为这是 public release,此时才强制version输入与.version文件一致,并执行 CI 检查与重复检查;其余情况会打印Non-release run — ignoring version input并回退使用.version。
环境审批门禁(Environment Gates)
release.yml使用 GitHubenvironments作为人工审批门禁。凡是接触签名凭据或发布产物的 job 都需要评审人先批准部署:
release环境:当sign、draft-release、publish-testpypi或publish-pypi任一开启时启用。保护代码签名密钥、release token、PyPI/TestPyPI 的 trusted publishing/OIDC 凭据;testpypi/pypi环境:分别保护 TestPyPI 与 PyPI 的 OIDC 发布权限(publish-testpypijob 声明id-token: write权限并使用pypa/gh-action-pypi-publish走 trusted publishing,不依赖 API token);- 当
sign关闭时,macOS 与 Windows 构建 job 的环境表达式为${{ inputs.sign == true && 'release' || '' }},即不绑定release环境——它们不需要审批、也无法访问签名密钥。
用 just 运行发布
release.just中定义的release模块封装了 prepare 脚本和release.yml的触发命令。release::prepare在本地运行且不带--ref;其余所有 recipe 都是派发release.yml,必须显式传入指向 release 分支的--ref参数。version变量自动取自.version文件(version := \cat .version``)。
| Recipe | 等价于 | 说明 |
|---|---|---|
just release::prepare --version <ver> | 本地运行python3 tools/prepare_release.py | 校验版本、检查 CI、更新.version并推送 |
just release::build --ref <branch> | gh workflow run release.yml+ 全部 false | 全平台构建,不签名不发布 |
just release::sign --ref <branch> | 同上 +sign=true | 构建并签名产物 |
just release::testpypi --ref <branch> | 同上 +publish-testpypi=true | 构建并发布到 TestPyPI |
just release::pypi --ref <branch> | 同上 +publish-testpypi=true, publish-pypi=true | 发布到 TestPyPI 和 PyPI |
just release::public --ref <branch> | 同上 +sign=true, draft-release=true, publish-testpypi=true, publish-pypi=true | 完整公开发布 |
just release::custom --ref <branch> --sign ... --draft-release ... --publish-testpypi ... --publish-pypi ... --skip-ci-check ... | 手动逐项指定所有开关 | 最灵活的触发方式 |
运行just --list --list-submodules可查看全部可用 recipe 及参数。
从特性分支测试发布工作流
release.yml可以从任意分支派发用于测试。发布门禁只在开启draft-release或publish-pypi时生效,因此测试构建非常安全:
# 1. 从你的分支派发 release.yml,所有布尔输入保持 false just release::build --ref <your-branch> # 2. 工作流按原样读取分支上的 .version(非发布运行忽略 version 输入),无需 prepare 步骤 # 3. 所有发布门禁(CI 检查、重复 tag 检查)都被跳过 # 4. 产物上传到 workflow run,但不会发布或打 tag带代码签名测试
# 1. 在仓库 Settings → Environments → release 中,临时把你的分支加入 allowed deployment branches # 2. 派发工作流 just release::sign --ref <your-branch> # 3. 出现环境部署提示时批准 # 4. 测试完成后,把分支从环境 allowed branches 中移除注意:
workflow_dispatch工作流只有在默认分支上存在该文件时才会出现在 GitHub Actions 的 UI 下拉菜单里。如果release.yml在你的分支上是新增或修改的,要用gh workflow run触发——合并回 main 之前 UI 里看不到它。
重要注意事项
- release 工作流构建
github.sha处的精确提交,它不写.version(那是 prepare 脚本的职责)。如果在 prepare 提交推送之前就派发 release,构建会使用派发时 HEAD 上的.version; draft-release=true+sign=false会被拒绝——草稿发布必须使用已签名的安装包;publish-pypi=true时,wheel 先发布到 TestPyPI,TestPyPI job 成功后再发 PyPI;若同时设置draft-release=true,PyPI 发布还会等待草稿 GitHub Release 成功;- 预发布版本(如
26.05b1)会在 GitHub 草稿 Release 上自动标记为 pre-release(preparejob 用tools/validate_version.py判断is_prerelease,releasejob 据此追加--prerelease标志)。
发布后的通告与收尾
- 草稿 Release 创建后,按需修改生成的 changelog,然后点击Publish release;
- 在 Anki 官方论坛的 Beta Testing 板块创建公告帖;稳定版发布后锁定该帖,并让用户到新帖反馈问题;
- 稳定版发布后,更新
ankitects/anki-landing-page仓库中的版本号。
音频包(anki-audio)的独立发布
音频播放与录制由独立的anki-audio包处理,位于 qt/audio(其pyproject.toml声明"Audio binaries (mpv, lame) for Anki",打包 mpv 与 lame 二进制)。它有自己的版本文件、构建脚本和发布工作流,与主发布管线解耦:
- 更新 Windows 预编译二进制:修改 build/configure/src/audio.rs 中的归档链接(该文件按平台给出 mpv v0.41.0 与 lame 3.100 的下载 URL 和 SHA256 校验值),然后运行
./tools/ninja audio_wheel验证; - 更新 macOS 构建脚本:修改 qt/audio/mpv.rb(Homebrew 打包脚本),本地用 qt/audio/build.sh 测试构建(该脚本也出现在
.github/workflows/publish-audio-package.yml的 macOS job 中,Windows 平台则调用tools\ninja audio_wheel); - 提升版本号:写入 qt/audio/.version——wheel 的版本号由 qt/audio/version.py 读取该文件得到;
- 提交 PR合入 main;
- 派发发布工作流
.github/workflows/publish-audio-package.yml发布到 PyPI:gh workflow run publish-audio-package.yml -f sign-macos=true -f publish-testpypi=true -f publish-pypi=true该工作流在四个平台(macOS ARM/Intel、Windows x64/ARM)上矩阵构建 wheel,
publish-pypi=true时强制要求sign-macos=true(macOS 二进制必须签名),同样走 TestPyPI → PyPI 的顺序; - 更新主项目锁定版本:包发布后,把 qt/pyproject.toml 中固定的
anki-audio依赖版本更新到新版本。
音频包之所以单独发布,是因为它在项目构建中作为外部依赖被引用(见build/configure/src/audio.rs的audio_wheel构建动作),主发布与音频包发布因此可以各自独立迭代、独立打版本。
结语
Anki 的发布体系把"版本校验、分支管理、翻译同步、CI 确认、跨平台构建、签名、多仓库发布、人工审批"全部串成一条可追溯的流水线:prepare 脚本保证发布的正确性(版本合法、无重复、CI 绿),release.yml的门禁与 job 拓扑保证产物的完整性与安全性(全平台覆盖、签名可控、OIDC 发布),而just release::*命令则把最常用的发布场景收敛成一行命令。无论是想理解日历版本发布的工程实践,还是需要为自己的开源项目搭建类似的自动化发布管线,这条链路都提供了非常完整的参考实现。
【免费下载链接】ankiAnki is a smart spaced repetition flashcard program项目地址: https://gitcode.com/GitHub_Trending/an/anki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考