Zenn
🔍

GitHub ActionsでPRの変更ファイルを効率的に取得する4つの方法

2025/03/24に公開
1

GitHub Actions で PR の変更ファイルを効率的に取得する 4 つの方法

TL;DR

プロジェクト状況に応じて最適な変更ファイル検出方法を選ぼう:

  • 小~中規模リポジトリ、簡単実装重視fetch-depth: 0で全履歴取得(方法 1)
  • 大規模リポジトリ、高速化最優先:GitHub API 利用(方法 3)
  • 特殊な差分取得要件あり:ベース SHA 明示 fetch(方法 4)
  • フォーク PR 多い OSS:全履歴取得か、API 利用(方法 1 か 3)

方法選定の判断基準は「リポジトリサイズ」「実装シンプルさ」「フォーク PR 対応」「差分処理の詳細度」

はじめに

GitHub Actions のワークフローを効率化したいと思ったことはないだろうか。特に大規模プロジェクトでは、PR ごとに全ファイルに対して処理を実行するのは時間とリソースの無駄になりがちだ。

これまで様々なプロジェクトで CI/CD 最適化に携わってきたが、「変更されたファイルだけを処理対象にする」というアプローチは結構必要になることが多い。例えば、変更のあった TypeScript ファイルだけを lint したり、更新されたテストだけを実行したりすることで、ワークフローの実行時間を大幅に短縮できる。

本記事では、GitHub Actions で PR の変更ファイルを取得するための主要な 4 つの方法を紹介し、それぞれの特徴やユースケースについて解説していく。実践的な視点から、どのような状況でどの方法を選ぶべきかについても考察したい。

変更ファイル取得の 4 つの方法

方法 1: fetch-depth:0 を使った全履歴取得+diff

最もシンプルで直感的な方法。リポジトリの全履歴を取得し、Git の diff コマンドで PR の差分を抽出する。

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: 変更ファイル取得
        run: |
          git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD > changed_files.txt
          cat changed_files.txt

この方法の特徴は以下の通りだ:

  • 👍 シンプルさ: 実装が非常に簡単で理解しやすい
  • 👍 フォーク PR 対応: 追加設定なしでフォークからの PR にも対応可能
  • 👍 正確性: GitHub の PR ビューと同等の差分を取得できる
  • 👎 速度: 大規模リポジトリでは全履歴クローンに時間がかかる

個人的には、小~中規模のリポジトリではこの方法が最もコスパが良いと思っている。実装の簡潔さと確実性を考えると、最初に試すべき選択肢。

方法 2: ベースとヘッドで 2 回チェックアウト

次に、ベースブランチと PR のヘッドを別々にチェックアウトし、それらを比較する方法だ。

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: ベースをチェックアウト
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}
          path: base
          fetch-depth: 1

      - name: ヘッドをチェックアウト
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          path: head
          fetch-depth: 1

      - name: 差分取得
        run: |
          diff -rq base head | grep "Files" | awk '{print $4}' > changed_files.txt
          cat changed_files.txt

この方法の特徴:

  • 👍 速度: 浅いクローンを 2 回行うため、大規模リポジトリでも比較的高速
  • 👍 柔軟性: ベースとヘッドを別々に操作できるため応用が効く
  • 👎 フォーク PR: フォークからの PR では追加設定が必要
  • 👎 複雑さ: 実装がやや複雑でステップ数が増える

この方法は履歴量が多いリポジトリでfetch-depth: 0を避けたい場合に効果的。ただし、個人的な経験では実装の複雑さを考えると、方法 1 か方法 3 を選ぶことが多い。

方法 3: GitHub API でファイル一覧を取得

GitHub の REST API を使って変更ファイル一覧を取得する方法だ。便利な Action もあるので、実装は非常に簡単になる。

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: 変更ファイル取得
        id: changed-files
        uses: tj-actions/changed-files@v39

      - name: 変更ファイル出力
        run: |
          echo "変更ファイル: ${{ steps.changed-files.outputs.all_changed_files }}"

この方法の特徴:

  • 👍 高速性: API コールのみでクローン不要なため非常に高速(0.5 秒程度)
  • 👍 シンプルさ: Action 利用で実装が非常に簡単
  • 👍 フォーク PR 対応: そのままフォーク PR にも対応
  • 👎 Git 操作制限: ローカルにコードを取得しないため詳細な Git 操作はできない

大規模モノレポや複数の大きなサブモジュールを持つリポジトリでは、この方法が圧倒的に有利だと思う。クローン時間を気にしなくて良いのは大きなメリットだ。

方法 4: ベース SHA を明示的に fetch+diff

最後に、最小限の git 操作で効率的に差分を取得する方法。

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: 変更ファイル取得
        uses: ./.github/actions/detect-changed-files
        with:
          base-ref: ${{ github.event.pull_request.base.sha }}
name: "Detect Changed Files"
description: "PRで変更されたファイルを検出する"

inputs:
  base-ref:
    description: "比較対象のベースブランチ"
    required: true

outputs:
  changed-files:
    description: "変更されたファイル一覧(カンマ区切り)"
    value: ${{ steps.detect.outputs.changed_files }}

runs:
  using: "composite"
  steps:
    - name: 差分検出
      shell: bash
      id: detect
      run: |
        # ベースコミットを取得
        git fetch origin ${{ inputs.base-ref }}

        # 差分ファイルを取得
        CHANGED_FILES=$(git diff --name-only FETCH_HEAD HEAD || echo "")
        echo "変更ファイル: $CHANGED_FILES"

        # 出力形式に変換
        echo "changed_files=$(echo $CHANGED_FILES | tr '\n' ',')" >> $GITHUB_OUTPUT

この方法の特徴:

  • 👍 効率性: 必要最小限の Git 操作で差分を取得(高速)
  • 👍 正確性: PR 分岐点からの正確な差分を得られる
  • 👍 カスタマイズ性: 複雑な処理を組み込むことができる
  • 👎 実装コスト: Git コマンドと仕組みの理解が必要

実践的には、特定のファイルパターンを検出して何らかの処理を行いたい場合など、カスタム要件がある場合に強みを発揮する。

各方式の比較まとめ

実際のプロジェクトで方法を選定する際の参考に、各方式を比較した表を作成した。

観点 方法 1: fetch-depth:0 方法 2: 2 回 checkout 方法 3: GitHub API 方法 4: ベース SHA 明示
実行速度 遅い(全履歴クローン) やや速い(2 回浅いクローン) 非常に速い(API のみ) 速い(最小限 Git 操作)
差分正確性 高い(PR 表示と同等) 高い(設定次第) 高い(GitHub 管理差分) 高い(PR 分岐点使用)
実装簡潔さ 簡単(少ステップ) 普通(設定増加) 簡単(API Action 利用時) やや難(Git 理解必要)
フォーク PR ◎(デフォルトで対応) △(追加設定必要) ◎(問題なく対応) △(状況による)
Git 操作自由度 ◎(制限なし) ◎(ほぼ制限なし) △(API 情報のみ) ○(最小限取得済み)
ワークフロー負荷 大(全履歴分) 中(2 回クローン) 小(API 側に委任) 小(最小限データ)

実際には、リポジトリサイズや変更内容、runner 環境によって性能は変わってくるため、これはあくまで一般的な傾向だと理解してほしい。

ケース別おすすめ活用法

使い分けの参考として、具体的なケース別の推奨方法をまとめておく。

小~中規模リポジトリで簡単実装したい場合

方法 1(fetch-depth: 0全履歴取得)が最適だろう。実装がシンプルで、追加 Git 操作も自由にできる。履歴が増えてクローンに時間がかかるようになったら、他の方法に移行すればいい。

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0
  - name: 変更ファイルによる処理分岐
    run: |
      CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD)

      # TypeScriptファイルが変更されていたらlint実行
      if echo "$CHANGED_FILES" | grep -q "\.ts$"; then
        echo "TypeScriptファイルが変更されているため、lintを実行します"
        npm run lint
      else
        echo "TypeScriptファイルの変更なし。lintをスキップします"
      fi

大規模 Mono レポでクローン時間短縮が最優先

方法 3(GitHub API)が適している。API レスポンスは高速で、変更ファイルパスだけ取得したい場合は最適だ。

steps:
  - name: 変更ファイル取得
    id: changed-files
    uses: tj-actions/changed-files@v39

  - name: 変更ファイルに基づくジョブスキップ判定
    id: check
    run: |
      SHOULD_RUN_TESTS=false
      CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}"

      # テスト対象のファイルがあるかチェック
      if echo "$CHANGED_FILES" | grep -q -E '\.test\.js|\.spec\.js|src/'; then
        SHOULD_RUN_TESTS=true
      fi

      echo "run_tests=$SHOULD_RUN_TESTS" >> $GITHUB_OUTPUT

  - name: テスト実行
    if: steps.check.outputs.run_tests == 'true'
    run: npm test

この方法では、わずか数百ミリ秒でファイル変更情報を取得でき、それに基づいて後続のジョブやステップを実行するかどうか判断できる。巨大なモノレポでも効率的だ。

差分行レベルでの処理や特殊な差分取得が必要

方法 4(ベース SHA 明示 fetch)が力を発揮する。前述の LP 検出ロジックのように、特定パターンのファイル変更を検出する複雑なケースにも対応できる。

この方法の実践例として、上記で紹介した Composite Action が良い例だと思う。特定のファイルパターンを検出し、後続のジョブで利用できる形で出力している。

フォーク PR を多く受け付ける OSS プロジェクト

方法 1 か方法 3 が無難だ。特に方法 3(API)はパフォーマンス面で優れている。
API ベースの方法は、フォークからの PR でもトークン権限の問題が少なく、安全に実行できる点が OSS プロジェクトには適している。

実践的な Composite Action の作成

方法 4 のようなカスタム処理は、一度実装してしまえば複数のワークフローで再利用できるため、投資する価値がある。特に以下のようなケースでは Composite Action にする価値が高い:

  1. 複雑な検出ロジックがある(正規表現や複数条件など)
  2. 検出後の処理が条件分岐を含む
  3. 同じ処理を複数のワークフローで使いたい

Composite Action を実装する際のポイントは、入力と出力をシンプルに保ち、内部処理の詳細はカプセル化すること。これによりワークフローの可読性が向上し、メンテナンス性も高まる。

実装上のベタープラクティス

  1. リポジトリサイズに合わせた選択を: 小規模なら方法 1 でシンプルに、大規模なら方法 3 でパフォーマンスを優先。

  2. デバッグ情報を出力する: 差分検出は時に予想外の動作をすることがある。echo 文で中間結果を出力するの大事。

  3. Actions の導入は段階的に: まずはシンプルな方法で始め、必要に応じて最適化。最初から複雑な実装にするとデバッグで死ぬ。

  4. フォーク PR 対応を忘れずに: OSS では、フォークからの PR への対応は必須。方法 1 か方法 3 を選ぶと安全。

  5. キャッシュとの組み合わせを検討: 変更ファイル検出は、依存関係のキャッシュ戦略と組み合わせると効果的。例えば、特定ディレクトリの変更があった場合のみキャッシュを無効化するなど。

- name: 変更ファイル取得
  id: changed
  uses: tj-actions/changed-files@v39

- name: キャッシュ復元
  uses: actions/cache@v3
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    # package.jsonが変更されていない場合のみキャッシュを利用
    restore-keys: |
      ${{ runner.os }}-node-
  if: contains(steps.changed.outputs.all_changed_files, 'package.json') == false

まとめ

GitHub Actions での PR 変更ファイル検出は、CI/CD ワークフローを最適化するための重要な手法だ。適切な方法を選ぶことで、ビルド時間の短縮とリソース効率の向上が可能になる。

小規模プロジェクトでは全履歴クローン(方法 1)、大規模プロジェクトでは API 利用(方法 3)、特殊要件にはカスタム Composite Action(方法 4)と、状況に応じた使い分けができると良い。

最終的には、プロジェクトの規模、速度要件、実装の複雑さのバランスを見て最適な方法を選択することが重要だと思う。どの方法も一長一短あるが、変更ファイルに基づいた処理の最適化という考え方自体は、あらゆる CI/CD ワークフローで価値を発揮するだろう。

1

Discussion

ログインするとコメントできます