GitHub Actions の auto-merge が「no merge base」で落ちる原因 — shallow fetch の罠

  • #GitHub Actions
  • #CI/CD
  • #Git
  • #自動化
  • #トラブルシューティング
GitHub Actions の auto-merge が「no merge base」で落ちる原因 — shallow fetch の罠

今回やったこと

GitHub Actions で稼働している自動マージおよびプルリクエスト(PR)検証ワークフローにおいて、fatal: origin/main...HEAD: no merge base(exit code 128)という致命的エラーでジョブが断続的に失敗する障害を調査・改修した。

原因は、ワークフロー冒頭の actions/checkoutfetch-depth: 0(完全クローン)を指定していたにもかかわらず、後続の差分検出ステップでベースブランチの取得に git fetch origin <base_ref> --depth=1 を指定していたことにあった。Git において、完全なコミット履歴を持つリポジトリに対して --depth=1 の浅いフェッチを実行すると、該当ブランチの履歴が切り詰められ(shallow 化)、ブランチ間の共通先祖(merge base)が失われてしまう。

不要な --depth=1 オプションを撤去し、ベースブランチも完全な履歴としてフェッチするように修正することで、shallow fetch に起因するマージ基底の切断エラーを解消した。高速化のための「浅いフェッチ」が引き起こす盲点と、CI環境における Git 履歴の取り扱い設計を記録する。

発生していたエラーと調査経緯

複数サービスの自動運用パイプラインでは、日次のコンテンツ更新や自動生成されたプルリクエストを機械検証し、要件を満たしたものを即座にマージするワークフロー(auto-merge.yml や PR 検証ワークフロー)を稼働させている。

しかし、特定のブランチや更新頻度の高い時間帯において、PRの変更対象ファイルを特定するステップで以下のエラーが発生し、パイプラインが停止する事象が頻発していた。

fatal: origin/main...HEAD: no merge base
##[error]Process completed with exit code 128.

このステップでは、ベースブランチとPR作業ブランチの差分ファイル一覧を取得するために、3点リーダー記法(...)を用いた以下のコマンドを実行していた。

changed=$(git diff --name-only "origin/$BASE_REF"...HEAD)

Git における A...B 記法は、「A と B の共通先祖(merge base)から B までの差分」を計算する構文である。したがって、このエラーは「Git が origin/mainHEAD の共通の祖先コミットを見つけられなかった」ことを意味している。

なぜ merge base が消滅したのか

ワークフローのチェックアウトステップを確認すると、設定は以下のようになっていた。

- name: Checkout repository
  uses: actions/checkout@v4
  with:
    fetch-depth: 0

fetch-depth: 0 を指定しているため、チェックアウト時点では全ブランチ・全タグを含む完全なコミットグラフがローカルに存在していたはずである。それにもかかわらず、直後のステップで Git が「共通先祖がない」と主張する矛盾が生じていた。

そこで、差分取得の直前に実行されていた以下のスクリプトに着目した。

# 問題のあった実装
git fetch origin "$BASE_REF" --depth=1
changed=$(git diff --name-only "origin/$BASE_REF"...HEAD)

ここで「最新のベースブランチだけを素早く取得したい」という意図から付与されていた --depth=1 が真犯人であった。

shallow fetch がコミットグラフを破壊する仕組み

Git のシャロー機能(浅いクローン・フェッチ)は、指定した深さまでのコミットしか持たない状態を作る。

すでに完全なリポジトリ(unshallow な状態)が存在する環境であっても、リモートブランチに対して --depth=1 を指定して git fetch を実行すると、Git はそのリモート追跡参照(origin/main など)の先端1コミットのみを独立して取得し、ローカルの履歴グラフから上流へのリンクを切断してしまう。

履歴切断の模式図

【fetch-depth: 0 直後】
commit A --- commit B --- commit C (origin/main)
                \
                 --- commit D --- commit E (HEAD: feature branch)
※ merge base は commit B。git diff origin/main...HEAD は正常に計算可能。

【git fetch origin main --depth=1 実行後】
[切断] ------------------- commit C-new (origin/main: depth=1 により過去親が消失)
                 
commit A --- commit B --- commit D --- commit E (HEAD)
※ origin/main の親履歴が存在しないため、HEAD との間に共通先祖が見つからない
→ fatal: origin/main...HEAD: no merge base (exit 128)

ベースブランチが進んで新しいコミットが作られていた場合、--depth=1 でフェッチするとその親コミット情報が取得されない。その結果、作業ブランチが分岐した地点(コミットB)とベースブランチの先端をつなぐ線が切れ、Git から見ると「全く別の2つの独立したリポジトリのブランチ」に見えてしまう。

これにより、ベースブランチが先行している場合に、マージ基底の解決を前提とする git diff A...Bgit merge-base が確実に失敗する状態に陥っていた。

修正内容と対策

対策は明快である。完全なクローン環境において、ベースブランチの最新コミットを安全に追従するためには、履歴の深さを制限する --depth=1 を外し、通常のインクリメンタルフェッチを行うことである。

ワークフロー修正の差分

ワークフロー定義の改修において、以下のように差分を修正した。

       - name: Determine changed scope
         id: scope
         shell: bash
         run: |
-          git fetch origin "$BASE_REF" --depth=1
+          # NOTE: --depth=1 を付けると full clone (fetch-depth: 0) の履歴が shallow 化され
+          # merge base が消えて `no merge base` (exit 128) になる。full fetch のままにすること
+          git fetch origin "$BASE_REF"
           changed=$(git diff --name-only "origin/$BASE_REF"...HEAD)
           echo "changed files:"
           echo "$changed"

すでに fetch-depth: 0 で大半の履歴が手元にある状態であれば、通常の git fetch origin <ref> を実行しても、転送されるのはチェックアウト後にリモート側で追加された差分コミット(数KB〜数十KB)のみである。--depth=1 を指定したところで短縮できる通信時間はミリ秒単位であり、履歴グラフの破損リスクに見合わない。

フェッチ方式の比較

CIパイプラインでブランチ間の差分比較を行う際のフェッチ設計を整理する。

フェッチ方式転送量履歴の健全性git diff A...B の可否推奨ユースケース
checkout(depth: 1) のみ最小最先端1件のみ不可(差分比較不可)静的アセットのビルド・単体テスト
checkout(depth: 0) + fetch(—depth=1)局所的に破壊不可(merge base 消失)アンチパターン(厳禁)
checkout(depth: 0) + 通常fetch最適完全可能(確実)PR差分検証・自動マージ判定
checkout(depth: 2) 等の固定値極小不安定不安定(コミット数依存)PRのコミット数が1件と保証される場合のみ

運用してみてわかったこと

CI ワークフローの高速化施策として「とりあえず --depth=1 を付与する」というプラクティスは広く知られているが、Git の履歴操作においては以下の落とし穴に留意する必要がある。

  1. shallow fetch は後からリポジトリを破壊する: 初回クローンだけでなく、その後の git fetch に指定した --depth もリポジトリ全体の履歴深度に影響を与える。不要に履歴を切断するオプションを混ぜるべきではない。
  2. 2点リーダー(..)と3点リーダー(…)の挙動差: git diff A..B(単純な2つのツリーの直接比較)であれば merge base を探索しないため --depth=1 でもエラーにならない場合がある。しかし、PR の正確な変更分を検出するためには共通先祖からの差分を見る A...B が必須であり、そのためには完全な履歴グラフが不可欠である。
  3. ワークフロー内のコメントによる再発防止: 「最適化できそうに見える箇所」は、後から別のエンジニアやAIエージェントが善意で --depth=1 を再追加してしまうリスクが高い。コード内に明示的に「なぜ --depth=1 を付けてはいけないのか」をインラインコメントとして固定しておくことが、CIの長期的な安定稼働につながる。

更新履歴

  • 2026-09-06: 初版起稿。GitHub Actions における shallow fetch による merge base 消失障害と修正手順を記録。