merge-briefing 是做什麼的?
概述
合併是不可撤銷的共享狀態,因此它取決於使用者針對每個 PR 的明確回答。這個門檻在形式上很容易滿足,也很容易虛假滿足:誠實的版本需要足夠寬廣的探查來支撐「沒有任何未解決問題」的說法,而實際存在阻擋性發現(finding)的表面——內聯審查執行緒——正是 gh pr view --json comments,reviews 這個看似正常的呼叫會悄悄忽略的那個。
這個技能是該流程的隨選中樞。Pre-Merge Reporting 規則 (ETHOS.md → Rules praxis carries) 維持著這個門檻;下方步驟 4 中的六部分列表是報告的格式;下方的步驟則說明了如何從「PR 看起來完成了」轉變為一個使用者可以用一個字回答的問題。
核心原則: 簡報是關於 PR 狀態的主張,因此其中的每一行都是在組成(compose)時探查的——而不是從 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 並非裝飾——一個由兩個 bot 處理過的 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 的 clone 中,--repo 必須在每次探查時指定 fork 的名稱:一個裸 gh pr 呼叫會解析到父倉庫(參見 worktree-merge-cleanup → Pre-call checklist)。
如果 CI 處於 pending 或 failing 狀態,在此停止——暫時不要詢問。一個驗證行仍在變動的簡報,是在要求使用者批准一個猜測。
在撰寫「已批准」之前,請閱讀每個人類批准所涵蓋的提交: reviewDecision: APPROVED 在後續推送後仍然存在。按審查者分組審查,並取每個人最新的 APPROVED 審查(較早的批准會被重新批准取代;來自同一審查者的後續 CHANGES_REQUESTED 或 DISMISSED 表示沒有當前的批准)。將該審查的 commit_id 與 headRefOid 進行比較,並在第 5 部分中列出它之後的非合併提交(git log --no-merges <commit_id>..<head>),作為「未經 審查」(參見失敗模式行的命令)。
合併狀態是一個允許清單,不是一個「沒有明顯問題」的檢查。 僅當 mergeable 為 MERGEABLE 且 mergeStateStatus 為 CLEAN 或 HAS_HOOKS 時才詢問——這兩個是唯一表示可合併且提交狀態通過的值。其他每個值都會停止詢問,並在簡報中說明自身:
mergeStateStatus | 含義 | 詢問? |
|---|---|---|
CLEAN | 可合併且提交狀態通過 | 是 |
HAS_HOOKS | 可合併,提交狀態通過,存在 pre-receive hooks | 是 |
UNSTABLE | 可合併,但提交狀態未通過 | 否——即上方失敗的 CI |
BLOCKED | 合併被阻止(保護規則,需要審查) | 否 |
BEHIND | 頭部引用已過時 | 否——請先 rebase |
DIRTY | 無法乾淨地建立合併提交 | 否——存在衝突 |
UNKNOWN | 目前無法確定狀態 | 否——重新輪詢 |
草稿狀態不在此枚舉中——它是單獨的 isDraft 欄位,因此一個草稿 PR 也可以報告 CLEAN。將 isDraft 加入 --json 列表,並將草稿視為尚未準備就緒,無論其合併狀態為何。
CLEAN 並未說明自分支建立以來落到 base 上的提交。 計算它們(git fetch origin 然後 git rev-list --count HEAD..origin/<base>),並列出它們所觸及且 PR 也觸及的檔案(git diff --name-only HEAD...origin/<base>)。對於每個共享檔案,在該 base diff 中 grep PR 所更改的符號,並執行 git merge-tree --write-tree HEAD origin/<base>——零個符號命中且退出碼為 0 即為「無語義重疊」的證據,需在第 5 部分中引用。當 pre-merge rebase 觸發器觸發時(3 個以上的 base 提交,一輪審查完成),使用 gh pr update-branch <N> 將 base 合併進來(這是一個合併提交,絕不是重寫),這會重新啟動 CI 和 bot 審查:等待兩者在新的頭部上完成,用已移動的頭部重新修訂錨點,然後針對新的頭部重新執行步驟 1–3 並重新建立簡報,之後再詢問——之前收集的發現、合併狀態和批准覆蓋範圍描述的是舊的頭部。
步驟 2:對所有三個表面上的每個發現進行評分
開啟的執行緒數量不等於阻擋項的數量,而執行緒表面也並非整個語料庫。根據裝飾(decoration),而非標籤(label),對步驟 1 收集到的每個發現(內聯執行緒、對話評論和審查主體)進行評分:
| 發現的第一行 | 會阻擋合併嗎? |
|---|---|
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),其計數總和須等於該總數。
未標籤的情況是常見情況,而非例外:bot 使用它們自己的詞彙。CodeRabbit 以它自己的嚴重性行開頭(_🎯 Functional Correctness_ | _🟠 Major_ | …),Copilot 和 BugBot 則沒有。將 bot 的嚴重性視為你評分的一個輸入,永遠不要將其作為評分本身——也永遠不要因為「它只是個 bot」而跳過該發現。
你評為阻擋性的執行緒,在合併前有一個正當的出口:它被修復並關閉,或者它被重新評分(regraded)——將評論編輯為 (non-blocking) 並發布 Revised — 通知。在不重新評分的情況下,將阻擋性發現擱置到後續追蹤,會導致分支在每個標籤都顯示非阻擋性時仍然被鎖定。
清除審查層級的 REQUEST_CHANGES 是一個單獨的、後續的步驟。 該狀態屬於審查,而審查可能包含多個發現,因此僅在其每個阻擋性發現都被修復並關閉或重新評分後才會被清除——絕不能僅憑其中一個就清除。重新評分最後一個發現才是解鎖它的關鍵;在此之前,該審查仍然正確地阻擋著。
步驟 3:承載錨點的 Unverified 缺口
打開 PR 的驗證錨點,閱讀其 Unverified 列表。一個寫成「머지 후 실전송으로 확인」的缺口會隨著 PR 一起結束,除非有什麼東西比它更長壽。
- 僅呈現那些指定了下一個行動的缺口——沒有說明下一個行動的缺口不予承載。
- 作為簡報的一部分,詢問是否要建立一個後續 issue。
- 在合併之前,將答案寫入錨點的最終修訂版中,格式為
Carried: #N或Carried: none — <사유>。 - 撰寫最終修訂版需要
gh——一個其唯一的 GitHub 表面是 MCP 伺服器的會話,當該伺服器活躍的工具列表沒有暴露任何評論update時,就無法更新評論主體(在假設之前先確認)。在這種情況下,將Carried:行改放在合併提交或 PR 主體中,並且永遠不要放在第二個錨點評論中。請參見anchor-comment-gate/spec.md→ Procedure for a gh-less session。 - 永遠不要自動歸檔後續事項:一個新的 issue 仍然需要它自己的實作方法審查。
- 當後續事項被記錄為評論時(
Carried: <comment URL>),將其放在一個在合併後保持開啟的 issue 上——PR 的Ref:issue(例如一個稍後由發布步驟關閉的 hub/umbrella issue),絕不能放在Closes #Nissue 上,後者會因合併本身而關閉,且無人會重新開啟。在 PR 開啟期間撰寫的範圍外筆記也同樣處理。 - 當承載 issue 正是合併後發布步驟會關閉的那個(一個由
/release關閉的 hub issue)時,發布會像Closes目標一樣掩埋承載。在合併報告中明確指出此衝突(立即發布會丟失待解決項目,或者在檢查完待解決項目後再發布),而不是默默地以任何一種方式解決它。另外,在合併後檢查Closes #N目標:GitHub 僅在 PR 合併到倉庫的預設分支時才會關閉它,因此合併到非預設 base(例如當預設是dev時合併到prod)會讓它保持開啟,等待發布步驟處理。
步驟 4:組成六部分簡報
六個部分,按此順序(此列表是規範格式;它最初源自作者的 dotfiles PR-workflow 模板):
- 變更內容——以一句樸實的句子開頭,說明 PR 做了什麼,以及合併與不合併對使用者分別有何影響,然後是範圍(檔案、邏輯變更),而不是再次說明 issue 標題。一個由計數和內部縮寫構成的第一部分,只會換來「它是做什麼的?」這個問題,而不是一個答案,這會重置批准視窗,並需要一次完整的重新簡報。
- 已驗證的內容——實際的輸出,附上所使用的命令。
- 未驗證的內容——跳過的、延遲的、被阻擋的、CI 等待中的項目。
- 風險/爆炸半徑——如果出錯,誰會受到影響。
- 待解決項目——步驟 2 評分後仍留下未解決的每個發現(在所有三個表面上);步驟 3 的缺口。
- 明確詢問——
Approve merge?獨佔一行,位於文字中,步驟 5 的AskUserQuestion呼叫緊隨其後(即使工具已經提出詢問,這一行仍然保留,因為它是門檻評分的項目)。
第 3 部分是壓力最大的部分,也是簡報存在的原因。
當一個未驗證的項目正是合併所觸發的步驟時(例如合併後的部署、CI 從未執行過的資產建置、你僅在一個平台上執行過的多平台建置),這是一個風險,而不是一個註腳:將其放在第 4 部分,說明如果該步驟失敗會發生什麼,以及它還會阻止誰(例如同一堆疊的後續部署)。對於其價值僅在於記帳(版本鎖定、目錄、文件)的 PR,第 4 部分也應有一行說明如果不合併會發生什麼——使用者在批准一個仍會觸發部署的低風險 PR 之前,會先問這個問題。
第 3 部分的反模式: 「완료했습니다, 머지할까요?」沒有任何證據;一個被跳過的檢查被降級為一個尾隨的附帶說明;在合併時報告本應在 PR 準備就緒時就出現的內容。
透過觀察來關閉第 3 部分的項目,而不是透過閱讀。 當使用者要求你檢查未驗證的項目時,逐一處理每個項目,並執行能解決該項目的最便宜的探查。
步驟 5:詢問
最後一步是提出 AskUserQuestion,並附上以下問題文字:
Approve merge?
發送該呼叫。使用者的回應是門檻檢查的對象。
在一個會話中,此技能可能被多次呼叫。在第二次及後續的呼叫中,僅在以下情況下才再次呈現完整的六部分結構:
- CI 從正在執行變為完成(任何結果),
- 新的審查到達,
- 新的評論被發布,
- 新的提交被推送,或者
- 使用者明確要求。
在其他情況下,提出一個簡短的跟進詢問,總結自從上次簡報以來發生的事情。
如果使用者的回應是合併批准,請執行 gh pr merge <N> --repo <owner>/<repo> --merge。
批量合併:多個 PR,一個答案
當使用者要求你處理一個集群——一系列相關的、連續的 PR,在一個會話中——時,步驟 4 的六部分結構會針對每個 PR 重複一次,但第五部分(詢問)在末尾只出現一次,列出其涵蓋的所有 PR。
### PR #12: 新增使用者驗證
...(第 1–5 部分,#12)...
### PR #14: 新增驗證測試
...(第 1–5 部分,#14)...
### 詢問
**Approve merge for PRs #12, #14?**
使用者回答 yes 即批准列表中的每一個。如果使用者指定一個子集,僅批准該子集。如果使用者拒絕任何一個,則整個批次都會被擱置。只對 PR 本身相關聯的項目進行批量合併——不要因為它們在同一個對話中出現就將其組合起來。當你提供批量合併簡報時,在你的初始訊息中明確說明這一點:
以下是一個批量合併簡報,涵蓋 PR #12 和 #14。批准將同時應用於兩者。你可以指定一個子集。
合併後報告
合併後,產生一個合併後報告:
- 包含一個「未經審查的提交」列表,顯示在合併前未經人類審查的提交及其作者。
- 計算上一個版本以來的未經審查提交(如果 PR 之前已被檢視過)。
- 如果任何合併後檢查(部署、滲透測試、金絲雀)失敗,請引導使用者返回 issue 追蹤器。
失敗模式
| 失敗模式 | 檢查 | 恢復 |
|---|---|---|
評級 suggestion (blocking): 為「非阻擋性」 | 審查主題行中的 (blocking) | 重新評分發現,不要重新標籤審查 |
| 在註記未驗證 UI 的同時批准合併 | 從未執行過端到端檢查 | 暫存此 PR,對 UI 類比執行端到端檢查,然後在通過時回來重新合併 |
| 將不同 PR 的批准合併到不同的 PR 中 | 錯誤訊息:收到對 PR #14 的 changes_requested 審查,但視為對 PR #15 的批准 | 撤銷兩個合併,重新評估審查的目標 PR |
| 批准未經審查的程式碼 | 審查 approved 標籤,同時驗證 commit_id | 使用 git log --no-merges <commit_id>..<head> 檢查未經審查的提交 |
| 錯誤地使用另一個使用者的批准 | 檢查審查者的使用者名稱或審查群組 | 檢查審查者並與使用者確認 |
被 gh pr view 100 個項目的限制欺騙 | 使用分頁的 REST 端點重新檢查 | 透過分頁 API 獲取完整的審查或評論列表 |
在 mergeStateStatus 為 UNSTABLE 時批准(非通過的提交狀態) | 檢查 mergeStateStatus;查看 CI 是否失敗 | 查看 CI 輸出 |
| 在 PR 是草稿時批准 | 在合併前檢查 isDraft | 檢查 PR 標題前的 Draft |
| 對「合併後部署」進行假批准 | 檢查未驗證的項目,例如「合併後部署」作為第 3 部分或第 4 部分的項目 | 提出 AskUserQuestion 以獲得批准 |
| 忽略合併後風險 | 將「合併後部署」放在第 4 部分 | 提出 AskUserQuestion 以獲得批准 |
| 在缺少使用者的 MergeMate 錨點的情況下批准 | 檢查 -> 合併 步驟 | 首先建立錨點 |