> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-mintlify-fbfa8bee.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> ClickHouse 回移策略及其自动化系统概述

# 回移系统

本文档介绍了 ClickHouse 的回移策略，以及用于实施该策略的自动化系统。

<div id="release-model">
  ## 发布模型
</div>

ClickHouse 版本遵循 `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 个月。

建议在生产环境中运行工作负载的用户使用最新的稳定版本或 LTS 版本，并及时升级到新的补丁版本，因为补丁版本不会引入破坏性变更。

<div id="backport-policy">
  ## 回移策略
</div>

并非所有更改都会回移。回移的目标是保持发布分支稳定，因此回移范围被有意控制在较小范围内：

* **安全修复** — 始终回移。
* **严重缺陷修复** (异常 (逻辑错误) 、数据丢失、错误结果、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 的作者、合并者以及现有受分配人，以便由人工解决冲突并完成回移。

<div id="backport-tool">
  ## 回移工具
</div>

上述回移策略由 `tests/ci/cherry_pick.py` 中的自动化工具实现。该工具以 GitHub Actions 工作流的形式运行在 ClickHouse 基础设施上，并满足全部要求：发现活跃的发布分支、筛选符合回移条件的拉取请求、执行两阶段的 `cherry-pick` 和回移流程、处理冲突、执行延迟策略，以及保持标签同步。

长期目标是将这套实现提炼为一个独立的开源 Python 工具，供其他项目采用。目标设计如下：

* **可配置** — 所有策略参数 (符合条件的标签、延迟窗口、过旧拉取请求阈值、滚动发布期间的行为等) 都在配置文件中定义，使该工具无需修改代码即可适配任何项目的回移需求。
* **可分发** — 打包为可从 PyPI 安装的自包含 Python wheel，不依赖 ClickHouse 的 CI 基础设施。
* **可编程** — 为拉取请求、标签和发布分支提供清晰的对象模型，使用户能够基于核心引擎编写自定义工作流脚本。

<div id="testing">
  ### 测试
</div>

该独立工具计划包含一个专用测试套件和一套轻量级测试基础设施。该基础设施将能够创建临时 GitHub 仓库 (或本地等效环境) ，并预置以下内容：

* 一组可配置的分支，用于表示各条发布线，
* 带有各种回移标签组合的拉取请求，
* 带有 `release` 标签并指向发布分支的发布 PR。

这样一来，测试就可以在不影响生产状态的情况下，针对一个真实但可丢弃的仓库，覆盖完整的自动化流程——标签检测、cherry-pick 分支创建、冲突处理、回移 PR 创建、指派逻辑、跳过 rolling-out 状态以及延迟策略。相同的基础设施还可在策略变更部署前复用于回归测试。

<div id="active-release-branches">
  ## 活跃的发布分支
</div>

活跃的发布分支，是指其对应的发布 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`、……。

<div id="implementation">
  ## 实现
</div>

<div id="overview">
  ### 概述
</div>

回移自动化以 `CherryPick` GitHub Actions 工作流 (`.github/workflows/cherry_pick.yml`) 的形式每小时运行一次，具体实现在 `tests/ci/cherry_pick.py` 中。它通过 GitHub API 运行，并在自托管的 `style-checker-aarch64` runner 上执行本地 git 操作。

对于每一对 (原始 PR、发布分支) ，该流程分为两个阶段：

1. 会创建一个 **cherry-pick PR**，用于将冲突解决与实际合并目标分离。如果没有冲突，则会自动合并。
2. 会针对实际的发布分支创建一个 **回移 PR**，并将 cherry-pick 得到的更改压缩为单个提交。

<div id="labels">
  ### 标签
</div>

原始 PR 上的标签决定是否进行回移，以及回移到哪些位置。

| 标签                                                | 作用                                                                                                                             |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `pr-must-backport`                                | 回移到所有活跃的发布分支 (跳过标记为 `rolling-out` 的分支)                                                                                         |
| `pr-must-backport-force`                          | 回移到所有活跃的发布分支，忽略 `rolling-out` 限制                                                                                               |
| `pr-critical-bugfix`                              | 自动触发 `pr-must-backport` (通过 `pr_labels_and_category.py` 中的 `AUTO_BACKPORT`)                                                    |
| `v{VER}-must-backport` (例如 `v25.3-must-backport`) | 回移到该发布分支 **以及所有更新的活跃发布分支** —— 该版本号标记了此 PR 必须到达的*最旧*发布版本，即使所命名的发布版本本身已经结束生命周期也是如此。若存在多个此类标签，以最低版本为准。会覆盖这些分支的 `rolling-out` 跳过规则 |
| `pr-backports-created`                            | 当所有必需的回移 PR 都已创建时由机器人设置；如果某个 cherry-pick PR 被重新打开，则会被清除                                                                        |
| `pr-cherrypick`                                   | 用于标记由机器人创建的 cherry-pick PR                                                                                                     |
| `pr-backport`                                     | 用于标记由机器人创建的回移 PR                                                                                                               |
| `do not test`                                     | 用于标记 cherry-pick PR，以便 CI 不会在其上运行                                                                                              |
| `rolling-out`                                     | 设置在 **发布 PR** 上，表示其分支当前正在滚动发布；常规回移会跳过该分支                                                                                       |

<div id="branch-and-pr-naming">
  ### 分支和 PR 命名
</div>

对于每个原始 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>`

<div id="step-by-step-process">
  ### 逐步操作流程
</div>

<div id="discover-active-releases">
  #### 1. 发现活跃的发布分支
</div>

`BackportPRs.receive_release_prs` 会在 GitHub 上查询所有带有 `release` 标签的未关闭 PR。这些 PR 的 head ref 就是发布分支名 (例如 `release/25.3`) 。系统会根据这些分支推导出一组需要搜索的特定版本标签：仓库中所有版本不高于最新活跃发布分支的 `v{VER}-must-backport` 标签都会包含在内。即使某个较旧标签对应的发布分支已不再活跃，它也会被纳入 (而比所有活跃发布分支都新的标签会被跳过，因为它不可能扩展到任何活跃分支)，因此，只要仍有较新的发布分支处于活跃状态，带有已终止支持发布分支标签的 PR 仍然会被找到。

<div id="find-prs-to-backport">
  #### 2. 查找需要回移的 PR
</div>

`BackportPRs.receive_prs_for_backport` 使用 GitHub 搜索 API 查找满足以下条件的已合并 PR：

* 带有至少一个回移标签 (`pr-must-backport`、`pr-must-backport-force`、`pr-critical-bugfix` 或特定版本标签) ，且
* **不**带有 `pr-backports-created`，且
* 合并时间晚于任一发布分支上找到的最早提交日期，且
* 在过去 90 天内有更新 (以提高搜索查询效率) 。

<div id="rolling-out-branch-handling">
  #### 3. `rolling-out` 分支处理
</div>

当发布 PR 带有 `rolling-out` 标签时，通用的回移标签 (`pr-must-backport`、`pr-critical-bugfix`) 会跳过该分支。机器人会关闭此前为该分支创建的所有 cherry-pick 或回移 PR，并附上说明性注释。特定版本的标签 (例如 `v25.3-must-backport`) 始终会覆盖这一行为——对于指定的 release，以及它扩展到的每个更新且仍处于活动状态的发布分支，都是如此。`pr-must-backport-force` 会绕过对所有分支的 `rolling-out` 检查。

<div id="cherry-pick-stage">
  #### 4. Cherry-pick 阶段 (`ReleaseBranch.create_cherrypick`)
</div>

对于每一组尚未存在 cherry-pick PR 的 (原始 PR、发布分支) 组合：

1. 检出发布分支，并基于它创建一个 **backport 分支** (`backport/release/X.Y/N`) 。
2. 对合并提交的第一个父提交执行 `git merge -s ours`，以创建一个不包含任何内容变更的合成合并基线。
3. 强制创建一个直接指向原始 PR 的合并提交的 **cherry-pick 分支** (`cherrypick/release/X.Y/N`) 。
4. 尝试使用 `git merge --no-commit --no-ff` 将 cherry-pick 分支合并到 backport 分支：
   * 如果已是最新状态，则说明该变更已存在于发布分支中——标记为完成并跳过。
   * 否则 (无论是否有冲突) ，重置并推送这两个分支。
5. 创建一个 cherry-pick PR，将 `cherrypick/release/X.Y/N` 合并到 `backport/release/X.Y/N`，并添加 `pr-cherrypick` 和 `do not test` 标签。
6. 如果适用，则从原始 PR 继承 `pr-bugfix` 或 `pr-critical-bugfix`。
7. 此时**不会**设置受分配人；只有在检测到冲突时才会添加。

<div id="auto-merge-conflict-free-cherry-pick-prs">
  #### 5. 无冲突的 cherry-pick PR 自动合并
</div>

如果 cherry-pick PR 可合并 (无冲突) ，机器人会通过 GitHub API 自动合并它，并立即进入回移阶段。

<div id="backport-stage">
  #### 6. 回移阶段 (`ReleaseBranch.create_backport`)
</div>

在 cherry-pick PR 合并后：

1. 检出并拉取 backport 分支。
2. 找到 release branch 与 backport 分支之间的 merge-base。
3. 执行 `git reset --soft` 回退到该 merge-base，将所有 cherry-pick 过的提交 squash 为一个提交。
4. 使用回移 PR 的标题作为提交消息进行提交。
5. 强制推送 backport 分支，并创建一个以实际 release branch 为目标分支的回移 PR。
6. 为该 PR 添加 `pr-backport` label (如适用，还需添加 `pr-bugfix` / `pr-critical-bugfix`) 。
7. 将该 PR 分配给原始 PR 的作者、合并者以及现有受分配人 (不包括机器人账户) 。

<div id="completion">
  #### 7. 完成
</div>

当某个原始 PR 对应的所有发布分支都已完成回移后，机器人会在该原始 PR 上添加 `pr-backports-created`。

<div id="pre-check">
  #### 8. 预检查
</div>

在开始处理某个 PR 之前，`ReleaseBranch.pre_check` 会运行 `git merge-base --is-ancestor`，以确认该合并提交尚未包含在发布分支的历史中。如果已经包含，则该 PR 会被视为已回移并跳过。

<div id="stale-cherry-pick-pr-handling">
  ### 长时间未处理的 Cherry-pick PR 处理
</div>

`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` 标签，以便重新处理。

对于等待人工解决冲突的 cherry-pick PR：

* **3 天**未更新后，机器人会发布一条 ping 评论并提及受分配人。
* **7 天**未更新后，机器人会发布一条关闭评论，并关闭该 PR。

<div id="conflict-resolution">
  ### 冲突处理
</div>

当 cherry-pick 发生冲突时，相应的 cherry-pick PR 会保持打开状态，等待人工处理。机器人会将其分配给原始 PR 的作者、合并者和受分配人。冲突解决并且 cherry-pick PR 合并后，机器人会在下一次每小时运行时创建回移 PR。

如果要完全放弃某次回移，请关闭 cherry-pick PR。机器人会将其视为有意跳过。

要从头重新创建一个损坏的 cherry-pick PR：

1. 从 cherry-pick PR 中移除 `pr-cherrypick` 标签。
2. 删除 `cherrypick/...` 分支。
3. 如果存在，请从原始 PR 中移除 `pr-backports-created`。

<div id="ci-for-backport-prs">
  ### Backport PR 的 CI
</div>

Backport PR 面向发布分支，因此使用专门的 CI 工作流 (`BackportPR`，定义在 `ci/workflows/backport_branches.py` 中) ，而不是标准的拉取请求工作流。该工作流会运行一组具有代表性的 CI 子集：ASan/UBSan 和 TSan 构建、发布构建、macOS 构建、在 ASan 下运行的功能测试、在 TSan 下运行的压力测试，以及集成测试。它还会验证 backport 分支是否包含 1 到 50 个提交，且至少有一个被更改的文件 (由 `check_backport_branch.py` 强制检查) 。

<div id="authentication">
  ### 身份验证
</div>

该工作流使用 SSH 密钥 (`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`) 。

<div id="github-api-cache">
  ### GitHub API 缓存
</div>

`GitHubCache` (来自 `cache_utils.py`) 会将 PyGithub 对象缓存持久化到 S3，以减少每小时运行时的 API 调用次数。缓存会在每次运行开始时下载，并在结束时上传。

<div id="error-handling">
  ### 错误处理
</div>

处理各个 PR 时产生的错误会被捕获并记录，但不会导致本次运行停止。所有 PR 处理完毕后，如果期间出现过任何错误，则会抛出 `BackportException`。在 CI 中，这会通过 `CIBuddy` 向团队聊天发送通知。
