发布模型
YY.M.patch.build-type 方案,其中 YY 表示两位年份,M 表示发布月份 (不补前导零) ,patch 是该分支内的补丁号,build 是单调递增的构建编号,type 则为 stable 或 lts。
示例:25.3.8.23-lts — 2025 年 3 月的 LTS 版本,补丁 8,构建 23。
发布分为两条路线:
- 稳定版本大致每月发布一次。最近的三个稳定版本会接收补丁,因此每个版本大约有三个月的活跃支持期。
- **LTS (长期支持) **版本每年在 3 月和 8 月发布。系统会同时支持两个 LTS 版本,且每个版本的支持期至少为 12 个月。
回移策略
- 安全修复 — 始终回移。
- 严重缺陷修复 (异常 (逻辑错误) 、数据丢失、错误结果、RBAC 问题) — 根据通用回移规则自动纳入回移范围;通过
pr-critical-bugfix标签识别,该标签会自动添加pr-must-backport。 - 稳定性修复和回归修复 — 当变更本身的风险低于保留该缺陷不修复的风险时,会进行回移;通过由维护者手动添加的
pr-must-backport识别。 - 有可用变通方案的轻微缺陷修复 — 通常不回移,以避免影响发布分支的稳定性。
- 新功能、改进、性能优化工作 — 不回移。
pr-must-backport 是维护者用于将 PR 标记为需要回移的手动覆盖机制。标签 pr-critical-bugfix 会触发 CI 钩子自动添加 pr-must-backport (参见 pr_labels_and_category.py) 。
冲突处理。 当自动回移无法解决合并冲突时,仍必须创建一个 cherry-pick PR,并将其分配给原始 PR 的作者、合并者以及现有受分配人,以便由人工解决冲突并完成回移。
回移工具
tests/ci/cherry_pick.py 中的自动化工具实现。该工具以 GitHub Actions 工作流的形式运行在 ClickHouse 基础设施上,并满足全部要求:发现活跃的发布分支、筛选符合回移条件的拉取请求、执行两阶段的 cherry-pick 和回移流程、处理冲突、执行延迟策略,以及保持标签同步。
长期目标是将这套实现提炼为一个独立的开源 Python 工具,供其他项目采用。目标设计如下:
- 可配置 — 所有策略参数 (符合条件的标签、延迟窗口、过旧拉取请求阈值、滚动发布期间的行为等) 都在配置文件中定义,使该工具无需修改代码即可适配任何项目的回移需求。
- 可分发 — 打包为可从 PyPI 安装的自包含 Python wheel,不依赖 ClickHouse 的 CI 基础设施。
- 可编程 — 为拉取请求、标签和发布分支提供清晰的对象模型,使用户能够基于核心引擎编写自定义工作流脚本。
测试
- 一组可配置的分支,用于表示各条发布线,
- 带有各种回移标签组合的拉取请求,
- 带有
release标签并指向发布分支的发布 PR。
活跃的发布分支
release 标签) 在 GitHub 上仍处于打开状态的分支。回移自动化会在每次运行时动态发现这些分支,因此当切出新版本或旧版本到达生命周期终点时,无需修改配置。
在新版本部署期间,发布分支可能处于 rolling-out 状态 (即其发布 PR 带有 rolling-out 标签) 。为避免增加滚动发布的复杂性,处于 rolling-out 状态的分支会暂停常规回移。特定版本标签 (例如 v25.3-must-backport) 会覆盖这一行为,即使在滚动发布期间也会强制执行回移。
特定版本标签会设定该 PR 必须到达的最早发布版本:它会被回移到该发布版本以及之后每一个较新的活跃发布分支,而不只是标签中指定的那个版本。例如,合并到 development 分支的某个 PR 如果带有 v25.3-must-backport 标签,就会被回移到 25.3,以及之后每一个活跃发布版本 (25.4、25.5、……)。如果同时存在多个特定版本标签,则以最低版本为准,因为它已经涵盖了所有较新的版本。
标签中指定的发布版本本身不一定是活跃的。针对已到达生命周期终点的发布版本 (即没有打开的发布 PR 的版本) 的标签,仍会将该修复继续向前带到其后的每一个活跃发布版本中,从而确保从该版本升级时不会在不知不觉中丢失该修复。例如,某个 PR 上的 v25.12-must-backport 标签,即使在 25.12 本身已经到达生命周期终点之后,仍会继续将该修复回移到 26.1、26.2、……。
实现
概述
CherryPick GitHub Actions 工作流 (.github/workflows/cherry_pick.yml) 的形式每小时运行一次,具体实现在 tests/ci/cherry_pick.py 中。它通过 GitHub API 运行,并在自托管的 style-checker-aarch64 runner 上执行本地 git 操作。
对于每一对 (原始 PR、发布分支) ,该流程分为两个阶段:
- 会创建一个 cherry-pick PR,用于将冲突解决与实际合并目标分离。如果没有冲突,则会自动合并。
- 会针对实际的发布分支创建一个 回移 PR,并将 cherry-pick 得到的更改压缩为单个提交。
标签
分支和 PR 命名
N 和发布分支 release/X.Y:
- Cherry-pick 分支:
cherrypick/release/X.Y/N - backport 分支:
backport/release/X.Y/N - Cherry-pick PR 标题:
Cherry pick #N to release/X.Y: <original title> - Backport PR 标题:
Backport #N to release/X.Y: <original title>
逐步操作流程
1
查找活跃的发行版
BackportPRs.receive_release_prs 会查询 GitHub 中所有带有 release 标签的未关闭 PR。这些 PR 的 head ref 就是发布分支名称 (例如 release/25.3) 。基于这些信息,它会推导出需要搜索的特定版本标签集合:代码仓库中存在的所有 v{VER}-must-backport 标签,只要其版本不比最新的活跃发布更新,都会被纳入。即使某个发布已不再活跃,与之对应的较旧标签也会被包含在内 (比所有活跃发布都更新的标签会被跳过,因为它无法扩展到任何活跃分支) ,因此,只要仍有较新的发布处于活跃状态,被标记为面向已终止生命周期发布的 PR 仍然会被找到。2
查找要回移的 PR
BackportPRs.receive_prs_for_backport 使用 GitHub search API 查找满足以下条件的已合并 PR:- 带有至少一个回移标签 (
pr-must-backport、pr-must-backport-force、pr-critical-bugfix或特定版本标签) ,并且 - 尚未带有
pr-backports-created,并且 - 在任一发布分支上找到的最早提交日期之后完成合并,并且
- 在最近 90 天内有过更新 (以确保搜索查询的效率) 。
3
Rolling-out 分支的处理
当一个 release PR 带有
rolling-out 标签时,通用回移标签 (pr-must-backport、pr-critical-bugfix) 会跳过该分支。bot 会关闭此前为该分支创建的所有 cherry-pick PR 或 backport PR,并附上一条说明性评论。特定版本标签 (例如 v25.3-must-backport) 始终会覆盖这一行为——对于指定的发布版本,以及它所扩展到的每一个更新的活动发布分支,都是如此。pr-must-backport-force 会对所有分支绕过 rolling-out 检查。4
Cherry-pick 阶段 (ReleaseBranch.create_cherrypick)
对于每一组尚未存在 cherry-pick PR 的 (原始 PR、发布分支) 组合:
- 检出发布分支,并基于它创建一个 backport 分支 (
backport/release/X.Y/N) 。 - 对合并提交的第一个父提交执行
git merge -s ours,以创建一个不包含任何内容变更的合成合并基线。 - 强制创建一个直接指向原始 PR 的合并提交的 cherry-pick 分支 (
cherrypick/release/X.Y/N) 。 - 尝试使用
git merge --no-commit --no-ff将 cherry-pick 分支合并到 backport 分支:- 如果已是最新状态,则说明该变更已存在于发布分支中——标记为完成并跳过。
- 否则 (无论是否有冲突) ,重置并推送这两个分支。
- 创建一个 cherry-pick PR,将
cherrypick/release/X.Y/N合并到backport/release/X.Y/N,并添加pr-cherrypick和do not test标签。 - 如果适用,则从原始 PR 继承
pr-bugfix或pr-critical-bugfix。 - 此时不会设置受分配人;只有在检测到冲突时才会添加。
5
自动合并无冲突的 cherry-pick PR
如果 cherry-pick PR 可合并 (无冲突) ,bot 会通过 GitHub API 自动将其合并,并立即进入回移阶段。
6
回移阶段 (ReleaseBranch.create_backport)
在 cherry-pick PR 合并后:
- 检出并拉取 backport 分支。
- 找到发布分支与 backport 分支之间的 merge-base。
- 使用
git reset --soft重置到该 merge-base,将所有 cherry-picked 提交压缩为一个。 - 使用 backport PR 的标题作为提交消息进行提交。
- 强制推送 backport 分支,并创建一个以实际发布分支为目标的 backport PR。
- 为该 PR 添加
pr-backport标签 (以及在适用时添加pr-bugfix/pr-critical-bugfix) 。 - 将该 PR 分配给原始 PR 的作者、合并人和现有受分配人 (不包括机器人账户) 。
7
完成
当某个原始 PR 对应的所有发布分支都已完成回移时,bot 会为该原始 PR 添加
pr-backports-created。8
前置检查
在开始对 PR 进行任何处理之前,
ReleaseBranch.pre_check 会运行 git merge-base --is-ancestor,以验证该合并提交是否已经在发布分支的可达范围内。如果是,则该 PR 会被视为已回移并跳过。长时间未处理的 Cherry-pick PR 处理
CherryPickPRs 类会在每小时执行开始时运行,并处理以下两种情况:
- 孤立的 cherry-pick PR:如果某个 cherry-pick PR 所在的发布分支不再有处于打开状态的发布 PR (即该发布已关闭) ,则该 cherry-pick PR 会被自动关闭。
- 重新打开的 cherry-pick PR:如果某个原始 PR 已带有
pr-backports-created,但对应的 cherry-pick PR 仍处于打开状态,则会从原始 PR 中移除pr-backports-created标签,以便重新处理。
- 3 天未更新后,机器人会发布一条 ping 评论并提及受分配人。
- 7 天未更新后,机器人会发布一条关闭评论,并关闭该 PR。
冲突处理
- 从 cherry-pick PR 中移除
pr-cherrypick标签。 - 删除
cherrypick/...分支。 - 如果存在,请从原始 PR 中移除
pr-backports-created。
Backport PR 的 CI
BackportPR,定义在 ci/workflows/backport_branches.py 中) ,而不是标准的拉取请求工作流。该工作流会运行一组具有代表性的 CI 子集:ASan/UBSan 和 TSan 构建、发布构建、macOS 构建、在 ASan 下运行的功能测试、在 TSan 下运行的压力测试,以及集成测试。它还会验证 backport 分支是否包含 1 到 50 个提交,且至少有一个被更改的文件 (由 check_backport_branch.py 强制检查) 。
身份验证
ROBOT_CLICKHOUSE_SSH_KEY) 执行 git push 操作。GitHub API 调用通过 get_best_robot_token 进行身份验证;该函数会从存储在 SSM (/github-tokens) 中的令牌池中,选择剩余额度最多的令牌。ROBOT_CLICKHOUSE_COMMIT_TOKEN 由 Actions 工作流中的 checkout 步骤使用,不用于 API 调用。分配负责人时,会排除机器人账户 (robot-clickhouse、clickhouse-gh) 。
GitHub API 缓存
GitHubCache (来自 cache_utils.py) 会将 PyGithub 对象缓存持久化到 S3,以减少每小时运行时的 API 调用次数。缓存会在每次运行开始时下载,并在结束时上传。
错误处理
BackportException。在 CI 中,这会通过 CIBuddy 向团队聊天发送通知。