GitHub Actions matrix strategy キャッシュヒット率が上がらない原因と対策|restore-keys・ブランチスコープ・LRU退去

GitHub Actions CI/CD DevOps キャッシュ pnpm
結論

キャッシュミスはブランチスコープ制限・restore-keys 未設定・matrixキーへのOS/バージョン不含の3原因のいずれかです。mainブランチ事前暖機 + restore-keys フォールバック設定で即日改善できます。

# 結論:matrix対応の最適化済みキャッシュ設定
- uses: actions/cache@v4
  with:
    path: ~/.pnpm-store
    key: ${{ runner.os }}-pnpm-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-${{ matrix.node-version }}-
      ${{ runner.os }}-pnpm-

キャッシュヒット率が上がらない3大原因

GitHub Actions の actions/cache は設定を誤ると、毎回キャッシュミスになりCIが遅いまま改善しません。原因は大きく3つに分類できます。

原因1:ブランチスコープ制限(最多)

GitHub Actions のキャッシュはセキュリティ上の理由(キャッシュポイズニング防止)からブランチ単位で分離されています。公式ドキュメントに明記されている参照ルールは以下のとおりです。

キャッシュ参照ルール説明
自ブランチ同一ブランチの過去ランで作成したキャッシュ ✅
PRのベースブランチPRが対象としているマージ先ブランチのキャッシュ ✅
デフォルトブランチ(main)mainブランチのキャッシュ ✅
兄弟featureブランチ別の feature/* ブランチのキャッシュ ❌ 参照不可

つまりmainブランチに一度もキャッシュが保存されていなければ、新規featureブランチは永遠にキャッシュミスになります。

原因2:restore-keys 未設定またはキーが広すぎる

key が完全一致しないとキャッシュミスになります。pnpm-lock.yaml が1行でも変われば hashFiles() は別のハッシュを返すため、依存関係を少し更新しただけで毎回完全ミスします。

# ❌ restore-keys なし:lockfileが1行変わるたびに完全ミス
key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}

# ✅ restore-keys あり:ハッシュが変わっても直近の部分一致キャッシュを使う
key: ${{ runner.os }}-pnpm-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
  ${{ runner.os }}-pnpm-${{ matrix.node-version }}-
  ${{ runner.os }}-pnpm-

restore-keys はプレフィックス前方一致で検索し、最も新しいキャッシュを返します。フォールバックが機能するため、lockfileの小さな変更でも前回のキャッシュを基底として pnpm install の差分だけインストールされます。

原因3:matrixキーにOS・バージョンを含めていない

matrix で複数OS・複数Node.jsバージョンを実行する場合、キャッシュキーが共通だとOS間・バージョン間でキャッシュが衝突します。Ubuntu でビルドした node_modules を Windows ランナーが復元しようとしてエラーになるのが典型的な障害です。

# ❌ OS・バージョンを含めていない:matrix間でキャッシュ衝突
key: node-${{ hashFiles('**/pnpm-lock.yaml') }}

# ✅ runner.os と matrix.node-version を含める
key: ${{ runner.os }}-pnpm-${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }}

根本解決:ヒット率を上げる手順

  1. キャッシュキーを修正するrunner.osmatrix.node-versionhashFiles() を結合したキーに変更し、restore-keys フォールバックを追加する
  2. mainブランチに事前暖機ワークフローを追加するpnpm-lock.yaml の変更時にmainブランチがキャッシュを作成し、featureブランチが継承できる基底を整備する
  3. monorepoの場合はキャッシュパスを全ワークスペース分列挙するnode_modules だけでなく packages/*/node_modules も含める
  4. gh api でキャッシュ実在を確認し ACTIONS_STEP_DEBUG でヒット判定ログを検証する

featureブランチが継承できる基底キャッシュをmainブランチに常備するのが最も効果的な対策です。専用のキャッシュ暖機ワークフローを追加します。

# .github/workflows/cache-prime.yml
name: Cache Prime (main)
on:
  push:
    branches:
      - main
    paths:
      - 'pnpm-lock.yaml'

jobs:
  prime:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x, 22.x]
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile

このワークフローは pnpm-lock.yaml が変更されたときだけ実行されます。mainブランチのキャッシュが常に最新の状態に保たれるため、featureブランチはPRを開いた初回から暖機済みキャッシュを利用できます。

完全な matrix CI ワークフロー設定例

# .github/workflows/ci.yml
name: CI

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

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false          # 1つ失敗しても他のmatrixは継続
      matrix:
        node-version: [20.x, 22.x]

    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          # setup-node の組み込みキャッシュはグローバルストアのみ管理
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run tests
        run: pnpm test

      - name: Build
        run: pnpm build

fail-fast: false にすることで、Node.js 20 でテストが落ちても Node.js 22 のジョブが中断されません。バージョン間の互換性問題を一度に把握できます。

キャッシュの種類と使い分け

方法キャッシュ対象向いている場面
setup-node cache: 'pnpm'~/.pnpm-store(グローバル)シングルパッケージ・シンプルなCI
actions/cache@v4 直接指定任意のパスmonorepo・複数 node_modules・カスタムパス
actions/cache/restore + save 分離任意のパス後続ジョブのみ保存・キャッシュの粒度を細かく制御したい場合

monorepoでは path: node_modules だとルートの node_modules しかキャッシュされません。

# ❌ monorepoで1箇所しかキャッシュされない
- uses: actions/cache@v4
  with:
    path: node_modules

# ✅ ワークスペース配下も含めてキャッシュ
- uses: actions/cache@v4
  with:
    path: |
      node_modules
      packages/*/node_modules
      apps/*/node_modules

既知の障害パターンとデバッグ手順

障害:lockfileがコミットされていないとキーが空になる

hashFiles('**/pnpm-lock.yaml') がマッチするファイルを見つけられない場合、戻り値は空文字列になります。キャッシュキーが ubuntu-latest-pnpm-20.x- のような不完全なものになり、意図しないキャッシュに衝突します。

pnpm-lock.yaml がリポジトリにコミットされているかを必ず確認してください。

障害:10GBの上限に達してLRU退去が起きる

GitHub Actions のキャッシュはリポジトリあたり標準10GBの上限があります(Enterprise等では拡張可能)。上限に達すると最後にアクセスされた日時が最も古いキャッシュ(LRU)が自動的に削除されます。また最終アクセスから7日間使用されなかったキャッシュも自動削除されます。

多数のブランチが存在する大規模プロジェクトでは、featureブランチのキャッシュが次々と退去され、常にキャッシュミスが発生する「キャッシュスラッシング」が起きます。

# キャッシュの実在を確認(GitHub CLI)
gh api /repos/<owner>/<repo>/actions/caches

# 特定のブランチのキャッシュ一覧
gh api /repos/<owner>/<repo>/actions/caches?ref=refs/heads/main

デバッグ:ACTIONS_STEP_DEBUG で詳細ログ

リポジトリの Secrets に ACTIONS_STEP_DEBUG = true を設定すると、キャッシュのルックアップ詳細がステップログに出力されます。「Cache hit」か「Cache not found」かを確認できます。

concurrency でキャッシュの無駄な保存を防ぐ

同じブランチへの連続プッシュで複数のCIが並走すると、同一キーのキャッシュが重複保存されます。concurrencycancel-in-progress: true を組み合わせると、古いランをキャンセルして無駄なキャッシュ生成を抑制できます。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

PRへの連続プッシュでCIが何本も走ってしまう問題も、この設定で解消できます。