merge-briefing 是做什么的?
概述
合并是不可逆的共享状态,因此它必须经过用户对每个 PR 的明确批准。这个门槛在形式上很容易满足,但也很容易虚假满足:诚实的版本需要一个足够广泛的探测来支持“没有未解决的问题”这一论断,而实际存在阻塞发现的那个表面(内联审查线程)恰恰是 gh pr view --json comments,reviews 调用静默忽略的。
该技能是按需处理此流程的场所。合并前报告规则(ETHOS.md → 规则实践部分)负责维护这个门槛;下面的步骤 4 中的六部分列表定义了报告的格式;而下面的步骤则说明了如何从“PR 看起来搞定了”过渡到一个用户可以用一个字回答的问题。
核心原则: 简报是关于 PR 状态的主张,因此其中的每一行都在编写时进行探测——而不是从 PR 打开时回忆的旧数据。
何时使用
- 即将询问用户批准合并时。
- 请求了合并,但您在此轮尚未给出简报时。
- 在之前的请求未获答复,或 CI/审查有变动后重新询问时。
- 当某个配套、后续或重新生成的 PR 是一个用户已批准过的集群中的下一个时。
铁律
- 一个 PR,一份简报,一个答案。 批准 PR X 仅批准 X。唯一的例外是批量合并:一轮中包含每个 PR 的独立简报部分,一个列出它们的请求,以及一个批准所列集合的答案。
- 根据本轮探测输出来编写简报。 进入时的
CLEAN状态不是永久的;过时的锚点会让用户发现一个本应由您捕获的冲突。 - 绝不要根据进度信号来合并。 "계속", "ok", "진행" 是推进工作的信号,而不是对共享状态变更的同意。
流程
步骤 1:探测所有三个表面
发现存在于三个表面,没有单个命令能覆盖全部。在编写简报之前,先运行以下所有命令。
# 1 — CI
gh pr checks <N> --repo <owner>/<repo>
# 2 — 对话时间线 + 审查体 + 可合并性 + 推送的 HEAD
gh pr view <N> --repo <owner>/<repo> \
--json mergeable,mergeStateStatus,isDraft,headRefOid,reviews,comments
# 3 — 内联审查线程(表面 #2 **不**包含此部分)
gh api graphql --paginate -f query='
query($owner:String!, $repo:String!, $pr:Int!, $endCursor:String) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$pr) {
reviewThreads(first:100, after:$endCursor) {
pageInfo { hasNextPage endCursor }
nodes {
id isResolved isOutdated path line
comments(first:1) { nodes { author { login } body } }
}
}
}
}
}' -F owner=<owner> -F repo=<repo> -F pr=<N> \
--jq '.data.repository.pullRequest.reviewThreads.nodes[]
| select(.isResolved | not)
| {id, path, line, author: .comments.nodes[0].author.login,
body: .comments.nodes[0].body}'
body 被包含在投影中,因为标签位于线程自身的文本内,而步骤 2 正是根据该标签进行分级的。--paginate 与 $endCursor 并非装饰——一个经过两个机器人处理过的 PR 可能超过 100 个线程,单页会静默丢弃其余部分,这会被解读为“无未解决线程”。
表面 #2 也静默截断在 100 个。 gh pr view --json comments,reviews 发出的是 comments(first: 100) / reviews(first: 100),且不包含游标变量。因此,当 PR 超过任一计数时,尾部内容就会简单缺失。当其中一项达到 100 行时,在分级之前,通过分页的 REST 端点重新收集该表面:
gh api repos/<owner>/<repo>/pulls/<N>/reviews --paginate -q '.[] | {user: .user.login, state, body}'
gh api repos/<owner>/<repo>/issues/<N>/comments --paginate -q '.[] | {user: .user.login, body}'
空结果本身并不是答案。 它在同一时间里既表示“无未解决线程”,也可能表示“查询出错”——直到您看到这条确切查询在一个已知有线程的 PR 上打印出结果为止。先在一个这样的 PR 上运行一次;这是支持否定论断的阳性对照。
在 fork 克隆中,每次探测都必须使用 --repo 命名该 fork:裸 gh pr 调用会解析到父仓库(参见 worktree-merge-cleanup → 调用前检查清单)。
如果 CI 处于 pending 或 failing 状态,则在此停止——不要询问。验证行仍在变动中的简报,是让用户批准一个猜测。
在编写“已批准”之前,检查每个人类批准所覆盖的提交:reviewDecision: APPROVED 在后续推送后仍然存在。按审查者分组审阅意见,并以每个审查者最新的 APPROVED 审阅为准(早前的批准会被重新批准覆盖;同一审查者后来的 CHANGES_REQUESTED 或 DISMISSED 表示当前无批准)。将该审阅的 commit_id 与 headRefOid 比较,列出其后到当前 HEAD 之间的非合并提交(git log --no-merges <commit_id>..<head>),并在第 5 部分中列为“未经 审阅”(参见失败模式行的命令)。
合并状态是一个许可列表,而非“未明显损坏”检查。 仅在 mergeable 为 MERGEABLE 并且 mergeStateStatus 为 CLEAN 或 HAS_HOOKS 时才可询问——这两个是唯一表示“可合并且提交状态通过”的值。其他所有值都停止询问,并在简报中标明自身:
mergeStateStatus | 含义 | 是否询问? |
|---|---|---|
CLEAN | 可合并且提交状态通过 | 是 |
HAS_HOOKS | 可合并,状态通过,存在 pre-receive hooks | 是 |
UNSTABLE | 可合并但提交状态未通过 | 否——即上面失败的 CI |
BLOCKED | 合并被阻止(保护规则、所需审查) | 否 |
BEHIND | HEAD 引用已经过时 | 否——请先变基 |
DIRTY | 无法干净地创建合并提交 | 否——存在冲突 |
UNKNOWN | 当前无法确定状态 | 否——请重新轮询 |
草稿状态不在此枚举中——它在单独的 isDraft 字段中。因此,草稿 PR 也可能报告 CLEAN。将 isDraft 添加到 --json 列表中,并将草稿视为未就绪状态,无论其合并状态如何。
CLEAN 对于自分支创建以来新落入基础分支的提交保持沉默。 统计它们(git fetch origin 然后 git rev-list --count HEAD..origin/<base>),并列出 PR 也修改的这些提交所触及的文件(git diff --name-only HEAD...origin/<base>)。对于每个共享文件,在基础 diff 中搜索此 PR 更改的符号,并运行 git merge-tree --write-tree HEAD origin/<base>——零符号命中且退出码为 0 才是“无语义重叠”的证据,在第 5 部分中引用。当合并前变基触发器触发时(3 个或更多基础提交,完成了一轮审阅),使用 gh pr update-branch <N>(一个合并提交,绝不是重写)将基础分支合并进来,这会重新启动 CI 和机器人审阅:在新的 HEAD 上等待两者完成,使用更新的 HEAD 重新修订锚点,然后针对新 HEAD 重新运行步骤 1-3 并重新构建简报,然后才询问——之前收集的发现、合并状态和批准覆盖描述的是旧的 HEAD。
步骤 2:对所有三个表面上的每个发现进行分级
未解决的线程数并不等同于阻塞数,而线程表面也并非全部内容。对步骤 1 收集的每个发现(内联线程、对话评论、审查体)进行分级,依据的是修饰语(decoration),而非标签(label):
| 发现的第一行 | 是否阻塞合并? |
|---|---|
issue (blocking): / suggestion (blocking): | 是——合并需等待 |
issue (non-blocking): / suggestion (non-blocking): | 否 |
nitpick: / note: | 否——按设计保持未解决状态 |
| 无标签 | 自行分级——阅读正文 |
suggestion 并不比 issue 更低一档:两者都恰好携带 (blocking) / (non-blocking) 之一。因此,修饰语就是全部判断依据,标签仅说明它是什么类型的发现。仅读取标签正是导致一个明确的阻塞项被当作“只是个建议”而放过的原因。
审查体不是线程,无法被“解决”, 这恰恰是它容易被忽略的原因:没有任何东西让它显得是开放的。CodeRabbit 将其可操作的发现放在可折叠的审查体部分中,而不是作为行评论。因此,需要 grep 步骤 1 中收集的审查体,查找 Actionable、Nitpick、outside diff、🧹,并对找到的内容进行分级。提出缺陷的对话评论也以相同方式分级。在任何表面上发现的阻塞项,都会像内联发现一样停止询问——它们只是没有线程保持开放,因此会在简报的未解决项中列出。
在分级完成前,需核对 CodeRabbit 的计数:通过 `LINES`: ** 标记统计其发现数量,并使其与审查头部声明的总数(例如 Major comments: N)匹配。在一个对话评论中回答这些发现,格式为 Fixed — <sha> (n) / Not fixed (n) / False positive (n),其计数之和应等于该总数。
无标签的情况是常见的,而非例外:机器人使用自己的词汇。CodeRabbit 以其自身的严重性行开头(_🎯 Functional Correctness_ | _🟠 Major_ | …),而 Copilot 和 BugBot 则没有。将机器人的严重性视为您分级的输入,而非分级本身——也绝不能因为“它只是个机器人”而跳过该发现。
一个您已分级为阻塞的线程,在合并前只有一个合法的退出方式:被修复并关闭,或者被重新分级——将评论编辑为 (non-blocking) 并发布 Revised — 通知。将阻塞发现搁置到后续步骤而不重新分级,会使分支保持锁定,而所有标签却都显示为非阻塞。
清除审阅级别的 REQUEST_CHANGES 是一个独立的、稍后的步骤。 该状态属于整个审阅,其中可能包含多个发现。因此,只有当该审阅中的每个阻塞发现都被修复并关闭或重新分级后,才能清除该状态——绝不能仅凭一个发现的力量。最后一个发现的重新分级才是解锁它的关键;在此之前,审阅正确地仍在阻塞。
步骤 3:携带锚点的 Unverified 缺口
打开 PR 的验证锚点,阅读其 Unverified 列表。写为“머지 후 실전송으로 확인”的缺口,如果没有任何东西能超越 PR 的生命周期,则会随 PR 一同消亡。
- 只罗列出那些指明了下一步行动的缺口——未说明下一步行动的缺口不予携带。
- 在简报中询问是否要创建一个跟进 Issue。
- 在合并前,将答案写入锚点的最终修订版中,格式为
Carried: #N或Carried: none — <사유>。 - 写入最终修订版需要
gh——一个仅通过 MCP 服务器与 GitHub 交互的会话,如果该服务器当前活跃的工具列表中没有暴露评论的update功能,则无法更新评论体(在假设之前请确认)。在这种情况下,请将Carried:行放入合并提交或 PR 体中,而永远不要放在第二个锚点评论中。参见anchor-comment-gate/spec.md→ 无 gh 会话的流程。 - 永远不要自动创建跟进 Issue:一个新 Issue 仍然需要其自身的实现方法审查。
- 当跟进以评论形式记录时(
Carried: <comment URL>),将其放在一个合并后保持开放的 Issue 上——即 PR 的Ref:Issue(例如,稍后由发布步骤关闭的枢纽/伞状 Issue),而不是Closes #NIssue,后者会被合并本身关闭,且无人会重新打开。对于在 PR 开放期间编写的超出范围的意见也适用此规则。 - 当携带 Issue 正好是一个合并后的发布步骤会关闭的 Issue(由
/release关闭的枢纽 Issue)时,发布步骤会像Closes目标一样掩埋该携带内容。请在合并报告中明确指出此冲突(现在发布则丢失未解决项,或在它们被检查后再发布),而不是静默地以任何一种方式解决。同时,在合并后检查Closes #N目标:GitHub 仅当 PR 合并到仓库的默认分支时才关闭它。因此,合并到非默认基础分支(例如默认分支是dev,而合并到prod)的 PR 会保持其开放状态,等待发布步骤处理。
步骤 4:编写六部分简报
六个部分,按以下顺序(此列表是标准格式;它最初源自作者的 dotfiles PR-工作流模板):
- 变更内容——用一句简单的话说明 PR 做了什么,以及合并(或不合并)对用户意味着什么变化。然后是范围(文件、逻辑变更),而不是重复 Issue 标题。如果第 1 部分仅由数字和内部缩写构成,您会得到“它有什么作用?”的反问,而不是批准,这将重置批准窗口并需要完整的重新简报。
- 已验证的内容——真实输出,并附上引用的命令
- 未验证的内容——跳过、推迟、阻塞、CI 待定
- 风险/影响范围——如果出错,谁会受影响
- 未解决项——步骤 2 中分级为开放的所有发现(来自所有三个表面);步骤 3 中的缺口
- 明确询问——
同意合并?单独成行,在文本中,并紧跟步骤 5 的AskUserQuestion调用(即使工具已经询问,该行仍保留,因为它是门槛评估的依据)
第 3 部分是面对压力的部分,也是简报存在的原因。
当一个未验证项正是合并所触发的步骤时(例如合并后部署、CI 从未运行过的资产构建、您仅在一个平台上运行过的多平台构建),它是一个风险,而不是脚注:将其放在第 4 部分中,说明如果该步骤失败会有什么后果,以及它会阻塞哪些其他内容(同一堆栈的后续部署)。对于一个价值仅为簿记工作(版本锁定、目录、文档)的 PR,第 4 部分也要用一句话说明如果不合并会发生什么——用户在进行低风险但仍会触发部署的 PR 批准之前,会问到这一点。
第 3 部分的反模式: “완료했습니다, 머지할까요?” 但没有证据;将跳过的检查降级为附带说明;在合并时才报告本应在 PR 就绪时就应呈现的内容。
通过观察来关闭第 3 部分的事项,而非阅读。 当用户要求您检查未验证项时,请逐项进行,并运行最能解决该问题的最廉价探测。
步骤 5:通过 AskUserQuestion 请求批准
调用您的工具平台提供的 AskUserQuestion 功能。提问必须逐字包含字符串 同意合并?,并且问题必须在 <ask_question> 调用体中紧紧跟在步骤 4 的 **明确询问** 部分之后。简报中必须包含该行。
报告为已批准。 当用户以肯定语气回答时——例如“네”、“예”、“yes”、“y”、“approve”、“merge”、“proceed”或包含 #approve 的消息——将批准时间与批准者记录在您的状态中。状态必须在合并后持续存在(如果合并完成),以便在重新生成摘要时,同一集群中的后续 PR 可以跳过重新询问。
报告为已拒绝。 当用户以否定语气回答时——例如“아니요”、“no”、“n”、“wait”、“stop”、“hold”、“check”或明确拒绝——停止,不要合并。如果用户要求修改,先修改,然后返回步骤 1。
批量合并:多个 PR,一个答案
要在一个询问中批准多个 PR,请在一个简报中包含每个 PR 的独立部分。每个部分格式如前所述,但在自己的部分内。然后合并的明确询问应列出所有 PR:
**明确询问**
同意合并 PR #1、#2、#3?(所有未解决项必须在询问前消解——见上文)
AskUserQuestion 的正文必须包含此列出所有 PR 的询问。
当用户批准时,按顺序为每个 PR 调用 gh pr merge,使用 --auto --merge 或 --auto --squash(根据仓库设置),如果任何合并失败,则停止并报告。在询问第一个合并之前,确保所有 PR 都已就绪(CI 通过,无阻塞项)。
步骤 6:最终合并
使用 gh pr merge。确认合并完成,并在 PR 上发布一条最终评论,总结结果,包括合并提交 SHA。
对于大小写:由于这是按需技能,步骤 5 中的 AskUserQuestion 调用是必需的:在没有先询问的情况下合并永远不是一个选项。如果用户在步骤 5 后关闭了对话,不要自动合并。等待用户的下一个输入。
检查清单
运行之前的自检:
- 未跳过步骤 1(所有三个表面均被探测)
- 步骤 2 根据修饰语(而非标签)对每个发现进行了分级
- 步骤 3 处理了锚点的
Unverified缺口 - 步骤 4 包含全部六个部分,格式正确
- 步骤 5 的
AskUserQuestion包含字符串同意合并? -
mergeStateStatus为CLEAN或HAS_HOOKS - CI 通过
- 当前没有阻塞的发现
故障模式
| 症状 | 可能原因及措施 |
|---|---|
| 简报对合并说“是”,但合并失败 | 步骤 1 中的探测陈旧——运行 git fetch origin 并检查 headRefOid 是否已移动;如果已移动,则重新探测 |
gh pr merge 失败,原因是“基础分支已过期” | mergeStateStatus 为 BEHIND——运行 gh pr update-branch,等待 CI,重新探测并重新编写简报 |
| 简报对合并说“否”,但用户继续坚持 | 用户覆盖了简报——记录此事件,而不是静默合并;遵守铁律 |
| 简报显示“批准”,但批准来自先前的提交 | 步骤 1 未将 commit_id 与 headRefOid 比较——运行 gh pr view 并检查;重新探测 |
| 简报未提及审查线程 | 探测命令行拼写错误——重新运行步骤 1,检查 gh api graphql 命令;确认查询格式正确 |
| 用户对简报做出回应,但简报未接收 | 用户消息在会话之外(另一个会话或用户界面);确认您在同一个会话上下文内 |
参考
- GitHub CLI 命令参考 (不翻译)
- GitHub GraphQL API 文档 (不翻译)