T-CREATOR

GitHub Actions のキャッシュがヒットしない原因 10 と対処レシピ

GitHub Actions のキャッシュがヒットしない原因 10 と対処レシピ

CI/CD パイプラインを高速化するために GitHub Actions のキャッシュ機能を導入したものの、思ったようにキャッシュがヒットせず、ビルド時間が短縮されないという経験はありませんか。キャッシュは正しく設定すれば劇的な効果を発揮しますが、わずかな設定ミスで全く機能しなくなることもあります。

本記事では、GitHub Actions のキャッシュがヒットしない主な原因 10 個を具体的なエラーコードや症状とともに解説し、それぞれに対する実践的な対処レシピをご紹介します。これらを理解することで、確実にキャッシュを活用できるようになるでしょう。

背景

GitHub Actions のキャッシュ機能は、依存関係やビルド成果物を保存して再利用することで、ワークフローの実行時間を大幅に短縮できる強力な機能です。

mermaidflowchart LR
  workflow["ワークフロー開始"] -->|キャッシュキー確認| check{キャッシュ<br/>存在?}
  check -->|ヒット| restore["キャッシュ復元"]
  check -->|ミス| install["依存関係<br/>インストール"]
  restore --> build["ビルド実行"]
  install --> build
  build --> save["新規キャッシュ<br/>保存"]
  save --> workflow_end["ワークフロー完了"]

上図は GitHub Actions のキャッシュの基本フローを示しています。キャッシュキーが一致する既存キャッシュがあれば復元され、なければ新規にインストールして保存されます。

GitHub Actions キャッシュの仕組み

GitHub Actions では actions​/​cache アクションを使ってキャッシュを管理します。キャッシュは以下の要素で構成されています。

#要素説明
1キャッシュキーキャッシュを一意に識別する文字列
2パスキャッシュする対象のファイルやディレクトリ
3リストアキーキャッシュキーが完全一致しない場合の代替キー
4保存期間最大 7 日間(アクセスがない場合は削除)
5容量制限リポジトリあたり 10GB まで

キャッシュの仕組みは一見シンプルですが、実際には様々な要因でキャッシュがヒットしないことがあります。次の章では、よくある課題について見ていきましょう。

課題

GitHub Actions のキャッシュがヒットしない問題は、ワークフローのログで確認できます。以下のような症状が現れたら、キャッシュに問題がある可能性が高いです。

キャッシュミスの症状

mermaidflowchart TD
  start["ワークフロー実行"] --> log_check["ログ確認"]
  log_check --> symptom1["Cache not found<br/>for input keys"]
  log_check --> symptom2["Post job cleanup<br/>Cache saved"]
  log_check --> symptom3["毎回フル<br/>インストール実行"]

  symptom1 --> issue["キャッシュ<br/>ヒット失敗"]
  symptom2 --> issue
  symptom3 --> issue

  issue --> impact1["ビルド時間<br/>増加"]
  issue --> impact2["API 制限<br/>リスク"]
  issue --> impact3["開発効率<br/>低下"]

キャッシュがヒットしない場合、ワークフローログに「Cache not found for input keys」というメッセージが表示されます。また、毎回依存関係の完全インストールが実行され、ビルド時間が一向に短縮されません。

主な問題パターン

実際のプロジェクトで遭遇するキャッシュ問題は、大きく以下のカテゴリに分類できます。

#カテゴリ影響度発生頻度
1キャッシュキーの設計ミス★★★
2パス指定の誤り★★★
3ブランチやワークフロー間の分離★★☆
4キャッシュ容量の超過★★☆
5タイミングや並列実行の問題★☆☆

これらの問題を理解し、適切に対処することで、キャッシュの効果を最大限に引き出すことができます。次章では、具体的な原因と解決策を詳しく見ていきましょう。

解決策

ここからは、GitHub Actions のキャッシュがヒットしない主な原因 10 個と、それぞれの対処レシピを詳しく解説していきます。

原因 1: キャッシュキーのハッシュ値が一致しない

最も頻繁に発生する原因は、依存関係ファイルのハッシュ値がキャッシュキーに正確に反映されていないことです。

エラーメッセージ

cssCache not found for input keys: node-modules-d41d8cd98f00b204e9800998ecf8427e

発生条件

  • package-lock.jsonyarn.lock などのロックファイルが更新された
  • ハッシュ関数の対象ファイルが間違っている
  • 複数のロックファイルが存在するが一部しか参照していない

解決方法

hashFiles() 関数を使って、正しい依存関係ファイルのハッシュ値をキャッシュキーに含めます。

修正前の例

yaml- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-cache-${{ runner.os }}

上記の設定では OS のみがキー要素となっており、依存関係が更新されてもキャッシュキーが変わりません。これでは古いキャッシュが使い続けられてしまいます。

修正後の例

yaml- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-cache-${{ runner.os }}-

hashFiles() 関数は指定されたパターンに一致するファイルの内容から SHA-256 ハッシュを生成します。依存関係が変わればハッシュ値も変わるため、適切にキャッシュが更新されます。

複数のパッケージマネージャーに対応する例

yaml- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ~/.yarn
      node_modules
    key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}
    restore-keys: |
      deps-${{ runner.os }}-

モノレポ構成や複数のパッケージマネージャーを使用している場合は、すべてのロックファイルをハッシュ対象に含めることが重要です。

原因 2: restore-keys の優先順位が不適切

restore-keys は完全一致するキャッシュが見つからない場合のフォールバックとして機能しますが、設定順序が不適切だとキャッシュヒット率が低下します。

エラーメッセージ

vbnetCache restored from key: npm-cache-Linux
Post job cleanup: Cache saved with key: npm-cache-Linux-a1b2c3d4e5f6

上記のログは、部分一致のキャッシュが復元され、最終的に新しいキャッシュが保存されたことを示しています。これ自体は正常な動作ですが、より適切な設定で効率を上げられます。

発生条件

  • restore-keys の指定順序が最適化されていない
  • フォールバック戦略が明確でない
  • 古いキャッシュが優先的に使われている

解決方法

キャッシュキーは具体的なものから汎用的なものへと段階的に設定します。

推奨設定パターン

yaml- name: Cache Node modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      node-modules-${{ runner.os }}-
      node-modules-

このように設定することで、以下の優先順位でキャッシュが検索されます。

#検索キー説明マッチ精度
1node-modules-Linux-a1b2c3d4完全一致(OS + ハッシュ)★★★
2node-modules-Linux-OS のみ一致★★☆
3node-modules-プレフィックスのみ★☆☆

バージョン管理を含む例

yamlkey: cache-${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
  cache-${{ runner.os }}-node-${{ matrix.node-version }}-
  cache-${{ runner.os }}-node-
  cache-${{ runner.os }}-

Node.js のバージョンごとにキャッシュを分離したい場合は、マトリックス変数も含めます。これにより、異なるバージョン間での互換性問題を回避できます。

原因 3: キャッシュ対象パスが存在しない

キャッシュの保存や復元時に、指定されたパスが存在しないとキャッシュが機能しません。

エラーメッセージ

javascriptWarning: Path Validation Error: Path(s) specified in the action do not exist:
/home/runner/.npm

発生条件

  • キャッシュパスのディレクトリがまだ作成されていない
  • パスの指定が間違っている(typo や環境変数の展開ミス)
  • OS ごとにパスが異なるのに固定値を指定している

解決方法

環境変数や条件分岐を使って、正しいパスを動的に指定します。

パッケージマネージャーのキャッシュディレクトリを取得する例

yaml- name: Get npm cache directory
  id: npm-cache-dir
  shell: bash
  run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT

まず、npm のキャッシュディレクトリの実際のパスを取得します。npm config get cache コマンドは環境に応じた正しいパスを返してくれます。

yaml- name: Cache npm dependencies
  uses: actions/cache@v4
  with:
    path: ${{ steps.npm-cache-dir.outputs.dir }}
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

取得したパスを使ってキャッシュを設定することで、環境に依存しない確実なキャッシュが実現できます。

Yarn の場合

yaml- name: Get Yarn cache directory
  id: yarn-cache-dir
  run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT

- name: Cache Yarn dependencies
  uses: actions/cache@v4
  with:
    path: ${{ steps.yarn-cache-dir.outputs.dir }}
    key: yarn-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}

Yarn も同様に、yarn cache dir コマンドで実際のキャッシュディレクトリを取得できます。

複数パスを指定する例

yaml- name: Cache dependencies and build
  uses: actions/cache@v4
  with:
    path: |
      ${{ steps.npm-cache-dir.outputs.dir }}
      node_modules
      .next/cache
    key: build-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

Next.js など、複数のディレクトリをキャッシュする場合は、パイプ記号(|)で複数行に分けて記述します。

原因 4: ブランチ間でキャッシュが共有されない

GitHub Actions のキャッシュには、ブランチごとのスコープ制限があります。

エラーメッセージ

cssCache not found for input keys: deps-Linux-a1b2c3d4e5f6
(feature ブランチでの実行時)

発生条件

  • デフォルトブランチ(main/master)以外でワークフローを実行している
  • feature ブランチで作成されたキャッシュは同ブランチでしか使えない
  • キャッシュのスコープルールを理解していない

キャッシュのスコープルール

GitHub Actions のキャッシュは以下のルールで共有されます。

mermaidflowchart TB
  main["main ブランチ<br/>キャッシュ"] --> feature1["feature/A<br/>ブランチ"]
  main --> feature2["feature/B<br/>ブランチ"]
  main --> develop["develop<br/>ブランチ"]

  feature1 -.->|参照不可| feature2
  feature2 -.->|参照不可| feature1
  develop --> feature3["feature/C<br/>(develop から派生)"]

  style main fill:#90EE90
  style feature1 fill:#FFB6C1
  style feature2 fill:#FFB6C1
  style develop fill:#87CEEB
  style feature3 fill:#DDA0DD

上図は、キャッシュがブランチ間でどのように共有されるかを示しています。デフォルトブランチのキャッシュはすべてのブランチから参照できますが、feature ブランチ同士では共有されません。

解決方法

方法 1: デフォルトブランチでキャッシュを事前作成

yamlname: Warmup Cache

on:
  push:
    branches:
      - main
      - develop

jobs:
  cache:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install dependencies
        run: npm ci

デフォルトブランチでキャッシュを作成するワークフローを用意することで、すべての feature ブランチから利用可能なキャッシュが準備できます。

方法 2: キャッシュキーからブランチ固有の要素を除外

yaml- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: node_modules
    # ❌ ブランチ名を含めない
    # key: deps-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }}

    # ✅ ブランチ非依存のキー
    key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

ブランチ名をキャッシュキーに含めると、ブランチごとに別のキャッシュが作成されてしまいます。ブランチ非依存のキーを使うことで共有しやすくなります。

原因 5: ワークフロー間でキャッシュが共有されない

同じリポジトリ内でも、異なるワークフローファイル間ではデフォルトでキャッシュが共有されません。

エラーメッセージ

cssCache not found for input keys: build-cache-a1b2c3d4
(test.yml ワークフローでの実行時)

発生条件

  • CI と CD で別々のワークフローファイルを使用している
  • ワークフローごとに異なるキャッシュキーを使用している
  • キャッシュの命名規則が統一されていない

解決方法

ワークフロー間でキャッシュを共有するには、同じキャッシュキーを使用します。

共有キャッシュキーの定義例(.github/workflows/ci.yml)

yamlname: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          # 共通のキャッシュキー
          key: shared-deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

CI ワークフローでキャッシュを作成します。キー名に "shared-" プレフィックスを付けることで、共有目的であることが明確になります。

同じキーで復元する例(.github/workflows/deploy.yml)

yamlname: Deploy

on:
  workflow_run:
    workflows: ['CI']
    types:
      - completed

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Restore cached dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          # CI と同じキャッシュキー
          key: shared-deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

デプロイワークフローでも同じキャッシュキーを使うことで、CI で作成したキャッシュを再利用できます。

再利用可能なワークフローを使う例

yaml# .github/workflows/reusable-cache.yml
name: Reusable Cache Setup

on:
  workflow_call:
    outputs:
      cache-hit:
        description: 'Whether cache was hit'
        value: ${{ jobs.cache.outputs.cache-hit }}

jobs:
  cache:
    runs-on: ubuntu-latest
    outputs:
      cache-hit: ${{ steps.cache.outputs.cache-hit }}
    steps:
      - uses: actions/checkout@v4

      - name: Cache dependencies
        id: cache
        uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

再利用可能なワークフローとしてキャッシュ設定を定義すれば、複数のワークフローから一貫した方法でキャッシュを利用できます。

原因 6: キャッシュサイズの上限超過

GitHub Actions のキャッシュには、リポジトリごとに 10GB の容量制限があります。

エラーメッセージ

vbnetError: Cache size of ~10000 MB (10485760000 B) is over the 10GB limit
Cache save failed.

発生条件

  • node_modules 全体など、大容量のディレクトリをキャッシュしている
  • 複数のキャッシュキーが蓄積して容量を圧迫している
  • 不要なファイルもキャッシュ対象に含まれている

キャッシュ容量の確認方法

リポジトリの「Actions」タブから「Management」→「Caches」で現在のキャッシュ使用状況を確認できます。

解決方法

方法 1: パッケージマネージャーのキャッシュのみを保存

yaml# ❌ 容量が大きい
- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ hashFiles('**/package-lock.json') }}

node_modules ディレクトリは非常に大きくなりがちで、キャッシュの容量制限を圧迫します。

yaml# ✅ パッケージマネージャーのキャッシュのみ
- name: Get npm cache directory
  id: npm-cache-dir
  run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT

- name: Cache npm
  uses: actions/cache@v4
  with:
    path: ${{ steps.npm-cache-dir.outputs.dir }}
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

- name: Install dependencies
  run: npm ci

npm のグローバルキャッシュディレクトリのみをキャッシュし、npm ci で高速にインストールする方が効率的です。

方法 2: 不要なファイルを除外

yaml- name: Cache build output
  uses: actions/cache@v4
  with:
    path: |
      .next/cache
      !.next/cache/webpack
      !.next/cache/**/*.map
    key: nextjs-${{ hashFiles('**/package-lock.json') }}

キャッシュパスの指定で ! を使うと、特定のファイルやディレクトリを除外できます。ソースマップなど、再現性があり容量の大きいファイルは除外しましょう。

方法 3: キャッシュの有効期限を短縮

yaml- name: Cache with shorter retention
  uses: actions/cache@v4
  with:
    path: dist
    key: build-${{ github.sha }}
    # 7日間アクセスがないと自動削除(デフォルト動作)

GitHub Actions のキャッシュは、7 日間アクセスされないと自動的に削除されます。この仕組みを活用して、古いキャッシュを自然に削除できます。

方法 4: キャッシュキーに日付を含める

yaml- name: Cache with date rotation
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ runner.os }}-${{ hashFiles('**/requirements.txt') }}-${{ github.run_id }}
    restore-keys: |
      pip-${{ runner.os }}-${{ hashFiles('**/requirements.txt') }}-
      pip-${{ runner.os }}-

github.run_id を含めることで、実行ごとに新しいキャッシュが作成されますが、restore-keys で以前のキャッシュも利用できます。

原因 7: 並列ジョブでのキャッシュ競合

複数のジョブが同時に実行される場合、キャッシュの保存と復元でタイミング問題が発生することがあります。

エラーメッセージ

vbnetError: Unable to reserve cache with key deps-Linux-a1b2c3d4, another job may be creating this cache.

発生条件

  • matrix strategy で複数のジョブが並列実行されている
  • 複数のジョブが同じキャッシュキーを使用している
  • 同時に複数のジョブがキャッシュを保存しようとしている

解決方法

方法 1: マトリックス変数をキーに含める

yamlstrategy:
  matrix:
    node-version: [18, 20, 22]
    os: [ubuntu-latest, windows-latest]

steps:
  - name: Cache dependencies
    uses: actions/cache@v4
    with:
      path: node_modules
      # マトリックス変数を含めて一意にする
      key: deps-${{ matrix.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}

マトリックス変数をキャッシュキーに含めることで、各ジョブが独自のキャッシュを持つようになります。

方法 2: 依存関係グラフで順序制御

yamljobs:
  cache:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ hashFiles('**/package-lock.json') }}
      - run: npm ci

  test:
    needs: cache
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - name: Restore cache
        uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ hashFiles('**/package-lock.json') }}

needs を使って依存関係を定義し、キャッシュ作成ジョブを先に実行させることで競合を回避できます。

方法 3: cache-dependency-path を活用

yaml- name: Setup Node with caching
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'
    cache-dependency-path: '**/package-lock.json'

actions​/​setup-node のビルトインキャッシュ機能を使えば、内部で競合が適切に処理されます。

原因 8: キャッシュキーに可変値を含めている

実行ごとに変わる値をキャッシュキーに含めると、キャッシュが再利用されません。

エラーメッセージ

cssCache not found for input keys: cache-2024-11-01-14-30-45-a1b2c3d4
(毎回異なるタイムスタンプで新規キャッシュが作成される)

発生条件

  • タイムスタンプや github.run_number などの可変値をキャッシュキーに含めている
  • 環境変数の値が実行ごとに変わる
  • ランダムな値を生成してキーに含めている

解決方法

❌ 避けるべき例

yaml- name: Cache with timestamp
  uses: actions/cache@v4
  with:
    path: build
    # タイムスタンプは実行ごとに変わる
    key: build-${{ github.run_number }}-${{ github.run_attempt }}

github.run_numbergithub.run_attempt は実行ごとに変わるため、キャッシュが再利用されません。

✅ 推奨例

yaml- name: Cache build output
  uses: actions/cache@v4
  with:
    path: build
    # ソースコードの内容ベースでキャッシュ
    key: build-${{ runner.os }}-${{ hashFiles('src/**/*.ts', 'package.json') }}
    restore-keys: |
      build-${{ runner.os }}-

ソースコードや設定ファイルのハッシュ値など、内容ベースのキーを使うことで、実際に変更があった場合のみキャッシュが更新されます。

条件付きで可変値を使う例

yaml- name: Cache with optional invalidation
  uses: actions/cache@v4
  with:
    path: dist
    key: dist-${{ hashFiles('src/**') }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || '' }}

手動実行(workflow_dispatch)の場合のみ強制的に新しいキャッシュを作成し、通常の push や PR では再利用する、といった使い分けも可能です。

原因 9: OS やランタイムバージョンの不一致

異なる OS やランタイムバージョン間でキャッシュを共有しようとすると、互換性の問題が発生します。

エラーメッセージ

javascriptError: The process '/usr/bin/node' failed with exit code 127
(Linux でキャッシュされた node_modules を Windows で使おうとした場合)

発生条件

  • マトリックスビルドで異なる OS を使用している
  • Windows と Linux でバイナリの形式が異なる
  • Node.js のバージョンによってネイティブモジュールの互換性がない

解決方法

OS とバージョンをキーに含める

yamlstrategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]

steps:
  - name: Setup Node
    uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}

  - name: Cache dependencies
    uses: actions/cache@v4
    with:
      path: node_modules
      # OS とバージョンの両方を含める
      key: deps-${{ matrix.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
        deps-${{ matrix.os }}-node${{ matrix.node }}-

OS とランタイムバージョンをキャッシュキーに含めることで、環境ごとに適切なキャッシュが作成・利用されます。

ネイティブモジュールを含む場合の追加対策

yaml- name: Cache for native modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: deps-${{ runner.os }}-${{ runner.arch }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}

runner.arch(CPU アーキテクチャ)も含めることで、ARM や x86 などのアーキテクチャ違いにも対応できます。

パッケージマネージャーキャッシュのみを使う場合

yaml- name: Get npm cache directory
  id: npm-cache-dir
  shell: bash
  run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT

- name: Cache npm (OS independent)
  uses: actions/cache@v4
  with:
    path: ${{ steps.npm-cache-dir.outputs.dir }}
    # パッケージマネージャーのキャッシュは OS 間で共有可能
    key: npm-cache-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-cache-

npm のグローバルキャッシュは OS 間で共有できるため、node_modules ではなくパッケージマネージャーのキャッシュを使えば OS 固有の問題を回避できます。

原因 10: アクセス権限やセキュリティ制約

プライベートリポジトリや Fork からの PR では、キャッシュアクセスに制限がかかることがあります。

エラーメッセージ

vbnetError: Unable to download cache: Forbidden
Warning: Failed to restore cache: Cache not found

発生条件

  • Fork されたリポジトリからの Pull Request
  • GITHUB_TOKEN の権限が不足している
  • プライベートリポジトリでのキャッシュアクセス制限

制限の仕組み

mermaidflowchart TB
  origin["オリジナル<br/>リポジトリ"] -->|キャッシュ作成| cache1["キャッシュ保存"]

  fork["Fork<br/>リポジトリ"] -->|PR 作成| pr["Pull Request"]

  pr -->|読み取り| cache1
  pr -.->|書き込み不可| cache1

  pr -->|独自キャッシュ| cache2["Fork 用<br/>キャッシュ"]

  style origin fill:#90EE90
  style fork fill:#FFB6C1
  style cache1 fill:#87CEEB
  style cache2 fill:#DDA0DD

Fork からの PR は、セキュリティ上の理由からオリジナルリポジトリのキャッシュを読み取ることはできますが、書き込むことはできません。

解決方法

方法 1: pull_request_target を使用(注意が必要)

yamlname: CI for Forks

on:
  pull_request_target:
    types: [opened, synchronize]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ hashFiles('**/package-lock.json') }}

pull_request_target を使うと、ベースリポジトリのコンテキストで実行されるため、キャッシュへの書き込み権限が得られます。ただし、セキュリティリスクがあるため、信頼できないコードを実行しないよう注意が必要です。

方法 2: キャッシュミスを許容する設計

yaml- name: Restore cache
  id: cache
  uses: actions/cache@v4
  with:
    path: node_modules
    key: deps-${{ hashFiles('**/package-lock.json') }}
  continue-on-error: true

- name: Install dependencies
  if: steps.cache.outputs.cache-hit != 'true'
  run: npm ci

continue-on-error: true を設定し、キャッシュがヒットしなくても処理を継続できるようにします。Fork からの PR では毎回インストールが実行されますが、エラーにはなりません。

方法 3: 明示的な権限設定

yamlname: Build

on: [push, pull_request]

permissions:
  actions: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Cache dependencies
        uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ hashFiles('**/package-lock.json') }}

ワークフローレベルで permissions を明示的に設定することで、必要な権限を確保します。

方法 4: キャッシュの失敗を検出して通知

yaml- name: Cache dependencies
  id: cache
  uses: actions/cache@v4
  with:
    path: node_modules
    key: deps-${{ hashFiles('**/package-lock.json') }}

- name: Check cache status
  if: steps.cache.outputs.cache-hit != 'true'
  run: |
    echo "::warning::Cache miss detected. This is expected for fork PRs."
    echo "cache-status=miss" >> $GITHUB_OUTPUT

キャッシュがヒットしなかった場合に警告メッセージを出力することで、問題の原因が権限であることを明確にします。

具体例

ここでは、実際のプロジェクトでよく使われる構成におけるキャッシュ設定の具体例をご紹介します。

例 1: Next.js プロジェクトの最適化

Next.js アプリケーションでは、複数のキャッシュポイントを活用することで大幅な高速化が可能です。

yamlname: Next.js CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

最初に、基本的なセットアップを行います。Node.js のバージョンは LTS 版を指定しましょう。

yaml- name: Get npm cache directory
  id: npm-cache-dir
  shell: bash
  run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT

- name: Cache npm dependencies
  uses: actions/cache@v4
  with:
    path: ${{ steps.npm-cache-dir.outputs.dir }}
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

npm のグローバルキャッシュをキャッシュすることで、依存関係のダウンロード時間を短縮します。

yaml- name: Cache Next.js build
  uses: actions/cache@v4
  with:
    path: |
      .next/cache
      !.next/cache/webpack
    key: nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
    restore-keys: |
      nextjs-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-
      nextjs-${{ runner.os }}-

Next.js のビルドキャッシュも保存します。Webpack キャッシュは容量が大きいため除外し、ページキャッシュのみを対象とします。

yaml- name: Install dependencies
  run: npm ci

- name: Build Next.js app
  run: npm run build

- name: Run tests
  run: npm test

キャッシュを復元した後、依存関係のインストール、ビルド、テストを実行します。キャッシュがヒットすれば、これらの処理が大幅に高速化されます。

例 2: モノレポ構成での選択的キャッシュ

モノレポでは、変更されたパッケージのみを効率的にキャッシュすることが重要です。

yamlname: Monorepo CI

on: [push, pull_request]

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.changes.outputs.packages }}
    steps:
      - uses: actions/checkout@v4

      - name: Detect changed packages
        id: changes
        run: |
          PACKAGES=$(git diff --name-only HEAD^ HEAD | grep '^packages/' | cut -d'/' -f2 | sort -u | jq -R -s -c 'split("\n")[:-1]')
          echo "packages=$PACKAGES" >> $GITHUB_OUTPUT

まず、変更されたパッケージを検出します。git diff を使って変更ファイルを抽出し、パッケージ名のリストを作成します。

yamlbuild:
  needs: detect-changes
  runs-on: ubuntu-latest
  strategy:
    matrix:
      package: ${{ fromJSON(needs.detect-changes.outputs.packages) }}

  steps:
    - uses: actions/checkout@v4

    - name: Cache package dependencies
      uses: actions/cache@v4
      with:
        path: packages/${{ matrix.package }}/node_modules
        key: deps-${{ matrix.package }}-${{ hashFiles(format('packages/{0}/package-lock.json', matrix.package)) }}
        restore-keys: |
          deps-${{ matrix.package }}-

変更されたパッケージごとにマトリックスビルドを実行し、パッケージ別にキャッシュを管理します。

yaml- name: Cache Turborepo
  uses: actions/cache@v4
  with:
    path: .turbo
    key: turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ github.sha }}
    restore-keys: |
      turbo-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-
      turbo-${{ runner.os }}-

Turborepo を使用している場合、.turbo ディレクトリのキャッシュも重要です。これにより、タスクの実行結果がキャッシュされます。

yaml- name: Install dependencies
  run: npm ci

- name: Build package
  run: npm run build --workspace=packages/${{ matrix.package }}

各パッケージのビルドを実行します。Turborepo のキャッシュにより、依存関係のないパッケージは前回のビルド結果が再利用されます。

例 3: Docker イメージビルドのキャッシュ

Docker を使ったビルドプロセスでも、レイヤーキャッシュを活用できます。

yamlname: Docker Build

on:
  push:
    branches: [main]

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

Docker Buildx を使用することで、高度なキャッシュ機能が利用できるようになります。

yaml- name: Cache Docker layers
  uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: docker-${{ runner.os }}-${{ hashFiles('**/Dockerfile', '**/package-lock.json') }}
    restore-keys: |
      docker-${{ runner.os }}-

Docker のビルドキャッシュを保存します。Dockerfile と依存関係ファイルの両方をハッシュ対象に含めることで、適切なキャッシュ管理ができます。

yaml- name: Build Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    push: false
    tags: myapp:latest
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

cache-fromcache-to を使って、Docker のレイヤーキャッシュを制御します。mode=max を指定することで、すべてのレイヤーがキャッシュされます。

yaml- name: Move cache
  run: |
    rm -rf /tmp/.buildx-cache
    mv /tmp/.buildx-cache-new /tmp/.buildx-cache

キャッシュディレクトリを更新します。この手順により、古いキャッシュが削除され、最新のビルド結果が次回のキャッシュとして保存されます。

例 4: マルチステージビルドでの段階的キャッシュ

複雑なビルドプロセスを段階的にキャッシュすることで、最大限の効率化が図れます。

yamlname: Multi-stage Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

actions​/​setup-node のビルトインキャッシュ機能を使うことで、npm のキャッシュが自動的に処理されます。

yaml- name: Cache TypeScript compilation
  uses: actions/cache@v4
  with:
    path: |
      **/*.tsbuildinfo
      dist
    key: tsc-${{ runner.os }}-${{ hashFiles('tsconfig.json', 'src/**/*.ts') }}
    restore-keys: |
      tsc-${{ runner.os }}-

TypeScript のインクリメンタルビルド情報(.tsbuildinfo)をキャッシュすることで、再コンパイルを最小限に抑えられます。

yaml- name: Cache ESLint results
  uses: actions/cache@v4
  with:
    path: .eslintcache
    key: eslint-${{ runner.os }}-${{ hashFiles('.eslintrc.js', 'src/**/*.ts', 'src/**/*.tsx') }}

ESLint のキャッシュも保存します。これにより、変更されていないファイルの linting がスキップされます。

yaml- name: Install dependencies
  run: npm ci

- name: Lint
  run: npm run lint -- --cache --cache-location .eslintcache

- name: Type check
  run: npm run type-check -- --incremental

- name: Build
  run: npm run build

- name: Test
  run: npm test -- --coverage --cache

各ステップで適切なキャッシュオプションを指定することで、繰り返し実行時のパフォーマンスが向上します。

キャッシュ効果の測定

キャッシュが正しく機能しているかを確認するため、ワークフローの実行時間を測定しましょう。

yaml- name: Cache summary
  if: always()
  run: |
    echo "### Cache Status Report" >> $GITHUB_STEP_SUMMARY
    echo "- npm cache: ${{ steps.npm-cache.outputs.cache-hit && '✅ Hit' || '❌ Miss' }}" >> $GITHUB_STEP_SUMMARY
    echo "- Next.js cache: ${{ steps.nextjs-cache.outputs.cache-hit && '✅ Hit' || '❌ Miss' }}" >> $GITHUB_STEP_SUMMARY

各キャッシュステップに id を設定し、キャッシュヒット状況をサマリーに出力することで、効果を可視化できます。

まとめ

GitHub Actions のキャッシュは、CI/CD パイプラインを高速化する強力な機能ですが、正しく設定しなければその効果を発揮できません。本記事では、キャッシュがヒットしない主な原因 10 個と、それぞれの対処レシピをご紹介しました。

キャッシュ設定のベストプラクティス

最後に、効果的なキャッシュ設定のポイントをまとめます。

#ポイント重要度
1hashFiles() で依存関係ファイルのハッシュを正確に取得する★★★
2restore-keys を具体的なものから汎用的なものへ段階的に設定する★★★
3パッケージマネージャーのキャッシュディレクトリを動的に取得する★★★
4ブランチ間のキャッシュ共有ルールを理解する★★☆
5OS やバージョンをキャッシュキーに含めて環境を分離する★★☆
6容量制限(10GB)を意識して不要なファイルを除外する★★☆
7並列ジョブではマトリックス変数をキーに含めて競合を防ぐ★☆☆
8タイムスタンプなど可変値をキーに含めない★★☆
9Fork からの PR では権限制限があることを考慮する★☆☆
10キャッシュヒット率を測定して効果を検証する★★☆

これらのポイントを押さえることで、GitHub Actions のキャッシュを最大限に活用でき、開発チームの生産性向上につながります。

トラブルシューティングの流れ

キャッシュがヒットしない場合は、以下の順序で確認していきましょう。

mermaidflowchart TD
  start["キャッシュミス発生"] --> check1{"キャッシュキーに<br/>hashFiles() 使用?"}
  check1 -->|No| fix1["原因1: hashFiles() を追加"]
  check1 -->|Yes| check2{"対象パスは<br/>存在する?"}

  check2 -->|No| fix2["原因3: パスを修正"]
  check2 -->|Yes| check3{"ブランチ間で<br/>共有可能?"}

  check3 -->|No| fix3["原因4: スコープ確認"]
  check3 -->|Yes| check4{"キャッシュ容量は<br/>上限以内?"}

  check4 -->|No| fix4["原因6: 容量削減"]
  check4 -->|Yes| check5{"並列ジョブで<br/>競合している?"}

  check5 -->|Yes| fix5["原因7: キーを一意化"]
  check5 -->|No| other["その他の原因を<br/>個別確認"]

  fix1 --> resolved["解決"]
  fix2 --> resolved
  fix3 --> resolved
  fix4 --> resolved
  fix5 --> resolved
  other --> resolved

上図のフローに沿って、順番に原因を特定していくことで、効率的にトラブルシューティングができます。

キャッシュの問題は、ログを丁寧に読み解くことで必ず原因を特定できます。「Cache not found」や「Cache saved」といったメッセージに注目し、本記事で紹介した対処法を試してみてください。

適切に設定されたキャッシュは、ビルド時間を 50% 以上短縮することも珍しくありません。ぜひ、この記事を参考に、あなたのプロジェクトのキャッシュ設定を最適化してみてくださいね。

関連リンク