GitHub Actions matrix strategy キャッシュヒット率が上がらない原因と対策|restore-keys・ブランチスコープ・LRU退去
キャッシュミスはブランチスコープ制限・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') }}
根本解決:ヒット率を上げる手順
- キャッシュキーを修正する —
runner.osとmatrix.node-versionとhashFiles()を結合したキーに変更し、restore-keysフォールバックを追加する - mainブランチに事前暖機ワークフローを追加する —
pnpm-lock.yamlの変更時にmainブランチがキャッシュを作成し、featureブランチが継承できる基底を整備する - monorepoの場合はキャッシュパスを全ワークスペース分列挙する —
node_modulesだけでなくpackages/*/node_modulesも含める 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が並走すると、同一キーのキャッシュが重複保存されます。concurrency と cancel-in-progress: true を組み合わせると、古いランをキャンセルして無駄なキャッシュ生成を抑制できます。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
PRへの連続プッシュでCIが何本も走ってしまう問題も、この設定で解消できます。