積み上げられたPRをベースブランチにランディングする
複数のPRがチェーン状になっている場合(PR N が PR N-1 のブランチに基づいている)、それらを本来のベースブランチに到達させる必要があります。重要な原則は:中間ブランチのCIはゲートではありません。ベースブランチのプロテクションがゲートです。 そのため、各レイヤーをベースブランチにジャストインタイムでリターゲットし、ネイティブCIで検証します。中間ブランチ名をCIトリガーにハードコードすることには決して依存しないでください。
リポジトリ:-R owner/repo を gh に渡すか、クローンから実行します(gh repo view --json nameWithOwner -q .nameWithOwner で解決されます)。以下、R はその owner/repo です。
原則
- マージ順序は実際の依存関係(コンテンツ依存 + 複数のレイヤーで編集されたファイル)によって決まります。最初にその順序を確立し、便利だからといって並べ替えないでください。
- レイヤーをマージするのはベースブランチにのみ、一度に1つずつです。子を古い前提ブランチにマージしないでください。
- レイヤーのCIが重要になる前に、現在のベースをそのレイヤーにマージして、差分が自身の変更のみになり、既にランディングされたすべての変更が見えるようにします。
- 先端ブランチにはすべてのレイヤーが累積的に含まれています。先端ブランチ(ベースにリターゲット済み)で1回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-cleanup の前に lint-enable)。2つのレイヤーが同じファイルを編集する場合、先にマージする方が先に適用されます。
ステップ1..N:レイヤーごとに、順番に
PR=<number>
gh pr edit $PR -R $R --base <base> # 積み上げブランチからリターゲット
# ヘッドブランチ用の別のワークツリーを使うと、メインチェックアウトをクリーンに保てます:
WT=../<repo>-<head> # メインチェックアウト外の任意のパス
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もマージされるため、ランディングしたいレイヤーのみをマージしてください。
- 各スカッシュの後、次のレイヤーは多くの場合、両方で触られたファイル(ADR、バージョンリストなど)でDIRTYになります。ベースをそれにマージし、解決し、プッシュし、CIを待ち、それから merge-async します。
- 承認は、古いレビューを却下しないリポジトリではベース同期後も維持されます(ブランチプロテクションで
required_pull_request_reviews.dismiss_stale_reviewsを確認)。本当のゲートは多くの場合、承認数ではなく、各タッチされたパスに対するCODEOWNERSの承認です(pr-approval-gateでギャップをリスト表示します)。 - ランディングの間にすべてのオープンPRをベースに対して最新に保つには、ベース同期パスを実行します:各オープンPRブランチにベースをマージしてプッシュします(通常のマージ、force-pushは禁止)。
コンフリクト解決
- 複数のレイヤーで編集されたワークフローファイル(再利用可能なビルドワークフロー、CIオーケストレーターワークフロー):どちらかの編集を選ぶのではなく、両方の編集を保持します。
- フォーマットスイープの衝突(Spotless/Prettier の適用スイープレイヤーとベース上のコンテンツ変更):コンフリクトしたファイルについてはベースのコンテンツを採用し、その後フォーマッターを再実行します(
mvn -pl <module> -am spotless:applyまたはpnpm fmt)。スイープ側を採用すると実際の変更が失われます。再フォーマットせずにベースを採用するとフォーマットゲートに合格しません。フォーマッターの実行にはビルドの解決が必要です(一部のリポジトリはプライベートレジストリから依存関係をプルするため、最初にクラウドログインが必要な場合があります。ビルドが依存関係をフェッチできない場合はリフレッシュしてください)。
安全性
- force-push、履歴の書き換え、プロテクトされたブランチへのプッシュは絶対に行わないでください。
- リポジトリのコミット作成者規則に従ってください(AI共同作成者トレーラーを禁止する場合があります)。
- ベースブランチへのマージの前には、必ず人間のレビュー承認を待ってください。
- 中間ブランチCI許可リストPR(このスキルが置き換えるアンチパターン)が存在する場合は、クローズを推奨します。既にマージされている場合は、追加されたブランチ名とイベントタイプを元に戻してください。
関連
opening-a-stacked-pr-sequence:スタックの開始(ベース、マージ順序)およびマージタイプごとのリスタック修正(stack.sh restack)。pr-approval-gate:各レイヤーにまだ必要なコードオーナー。resolve-pr-threads-and-conflicts-and-merge:スレッドと承認が完了した後の1レイヤーのランディング。