將堆疊式 PR 合併落地到基礎分支
當多個 PR 形成鏈式堆疊(PR N 基於 PR N-1 的分支)且需要合併到真正的基礎分支時使用。 關鍵原則:中間分支的 CI 不是關卡,基礎分支的保護才是。 因此,在合併前一刻將每一層的目標分支重新設定為基礎分支,讓原生 CI 驗證它。 絕對不要依賴將中間分支名稱硬編碼到 CI 觸發條件中。
儲存庫:將 -R owner/repo 傳遞給 gh,或從 clone 的目錄執行(gh repo view --json nameWithOwner -q .nameWithOwner
會自動解析)。以下,R 即為該 owner/repo。
原則
- 合併順序由真正的依賴關係決定(內容依賴 + 被多層編輯的檔案)。先確定此順序;不要為了方便而重新排序。
- 每一層只能合併到基礎分支,一次合併一層。絕對不要將子層合併到舊的先決條件分支。
- 在一層的 CI 生效前,先將目前基礎分支合併到該層,使其 diff 縮減為僅包含自身的變更,並讓它看到所有已合併的內容。
- 頂端分支包含所有層的累積變更;在頂端分支(重新設定目標到基礎分支後)執行一次 CI 即可驗證整個堆疊。可將其作為可選的預先檢查。
步驟 0:確定順序並檢測衝突
確認儲存庫真正的基礎分支(從 git 遠端取得的預設分支,例如 develop 或 main;使用者的代理指令檔案(AGENTS.md / CLAUDE.md)可能會列出已知的基礎分支)。
對於每個 PR,針對基礎分支檢測衝突,而不是針對其過時的堆疊基礎分支(後者會顯示為 CLEAN 而誤導你):
git fetch origin <base> <head> --quiet
git merge-tree --write-tree origin/<base> origin/<head> | grep -iE 'CONFLICT|changed in both' || echo "clean vs base"
順序經驗法則:先決條件內容排在需要它的關卡之前(例如,先做 lint 清理,再做 lint 啟用);當兩層編輯同一個檔案時,先編輯的層優先合併。
步驟 1..N:依序處理每一層
PR=<number>
gh pr edit $PR -R $R --base <base> # 重新設定目標,從堆疊分支改為基礎分支
# 為 head 分支建立一個獨立的工作目錄,讓你的主要 checkout 保持乾淨:
WT=../<repo>-<head> # 任何位於主 checkout 目錄外的路徑
git worktree add "$WT" <head> 2>/dev/null || true
git -C "$WT" fetch origin --quiet
git -C "$WT" merge origin/<base> # 同步基礎分支;解決衝突(見下文)
git -C "$WT" push
gh pr ready $PR -R $R # 當它是當前處理的層時
gh pr checks $PR -R $R --watch # 原生 CI,base=<base>
# 僅在審核批准後:
gh pr merge $PR -R $R --squash --delete-branch
# 然後移至下一層(其重新設定目標 + 同步基礎分支會吸收此層)
GitHub 原生堆疊(merge-async)
使用 GitHub 原生堆疊功能建立的 PR 會拒絕上述標準步驟:
gh pr edit --base:GraphQL: Cannot change the base branch because the pull request is part of a stack. (updatePullRequest)gh pr merge --auto:GraphQL: Auto-merge is not supported for stacked pull requests.gh pr merge:This pull request is part of a stack and must be merged using the asynchronous merge REST API.
使用非同步 API,從底層 PR 開始(GitHub 會自動將下一個 PR 的目標重新設定為基礎分支,等待約 30 秒):
sha=$(gh pr view $PR -R $R --json headRefOid -q .headRefOid)
u=$(gh api -X PUT repos/$R/pulls/$PR/merge-async -f merge_method=squash -f sha=$sha \
-f merge_action=direct_merge --jq '.details.uuid // .status')
gh api repos/$R/pulls/$PR/merge-async/$u --jq '.status+" "+(.details.message//"")' # 輪詢直到狀態不再是 "pending"
- 用這種方式合併一個堆疊 PR 也會合併所有位於它下方的未關閉 PR,所以只合併你真正想落地的那一層。
- 每次 squash 合併後,下一層在兩者都觸及的檔案上(例如 ADR、版本列表)通常會變為 DIRTY 狀態:將基礎分支合併到該層,解決衝突,推送,等待 CI,然後再用 merge-async 合併它。
- 在不撤銷過時審核的儲存庫中,審核批准在基礎分支同步後仍然有效(請檢查分支保護中的
required_pull_request_reviews.dismiss_stale_reviews)。真正的關卡通常是對每個觸及路徑的 CODEOWNERS 批准,而不是批准數量(pr-approval-gate會列出差距)。 - 為了讓每個未關閉的 PR 在合併落地期間與其基礎分支保持同步,請執行基礎同步掃描:將基礎分支合併到每個未關閉 PR 的分支中並推送(使用一般合併,不要強制推送)。
衝突解決
- 被多層編輯的工作流程檔案(可重複使用的建置工作流程、CI 編排工作流程): 保留雙方的編輯,不要選邊站。
- 格式掃描衝突(例如 Spotless/Prettier 的套用掃描層與基礎分支上的內容變更衝突):
對於衝突的檔案,採用基礎分支的內容,然後對其重新執行格式化工具
(
mvn -pl <module> -am spotless:apply或pnpm fmt)。採用掃描方的變更會遺失真正的 變更;採用基礎分支的內容而不重新格式化則會導致格式檢查失敗。格式化工具執行需要 建置來解析(有些儲存庫從需要雲端登入的私有註冊表拉取依賴項;如果建置無法取得依賴項,請重新整理登入狀態)。
安全
- 不要強制推送、改寫歷史或推送到受保護的分支。
- 遵循儲存庫的提交作者慣例(有些禁止 AI 共同作者標記)。
- 在每次合併到基礎分支之前,暫停並等待人工審核批准。
- 如果存在中間分支 CI 允許清單的 PR(此技能旨在取代的反模式),建議關閉它;如果它已經合併,則還原新增的分支名稱和事件類型。
相關技能
opening-a-stacked-pr-sequence:開啟堆疊(基礎分支、合併順序)以及每種合併類型的重建堆疊修復(stack.sh restack)。pr-approval-gate:每一層還需要哪些程式碼擁有者的批准。resolve-pr-threads-and-conflicts-and-merge:在處理完討論串和批准後落地單一層。