← 記事一覧に戻る

GitHub Actionsのactions/cacheがヒットしない6パターン:npm/pnpm/Dockerで確実にキャッシュを効かせる

GitHub ActionsでCI/CDパイプラインを高速化しようとactions/cacheを設定したのに、ジョブが毎回「Cache Miss」になって全くキャッシュが効かない。この問題にハマったことがある人は多いはずだ。

原因は1つではなく、よくあるパターンが6つある。それぞれの症状・原因・対策を解説する。


キャッシュの仕組みをおさらい

まずactions/cacheの基本動作を押さえておこう。

  1. Restore フェーズ: keyで完全一致を探す → なければrestore-keysで前方一致を試みる
  2. ジョブ実行
  3. Save フェーズ: Restoreでキャッシュがなかった場合、pathsの内容をkeyで保存する
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

重要ポイント: Restoreで完全一致HIT → Saveはスキップされる。完全一致なし → ジョブ終了後にSave。


パターン1: lockfileのパスが間違っている(最多)

症状

  • ローカルでは問題ないのに毎回Cache Miss
  • hashFilesが常に同じハッシュ値を返す、または変わり続ける

原因

hashFiles('**/package-lock.json')のglobパターンがファイルを見つけられていないか、パッケージマネージャと一致していない。

# NG: pnpmなのにpackage-lock.jsonを指定
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
# → pnpm-lock.yamlは無視 → 依存が変わってもキャッシュキーが変わらない

# NG: モノレポでルートのlockfileしか見ていない
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}

対策

# npm の場合
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

# pnpm の場合
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

# yarn の場合
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}

確認方法: ワークフローのログで表示されるCache Keyのハッシュ部分が、lockfile変更前後で変わっているか確認する。


パターン2: pathsがパッケージマネージャのキャッシュ先と一致していない

症状

  • キャッシュのHIT/MISSは正常に見える
  • しかしnpm installpnpm installが毎回フルで実行される(速くならない)

原因

pathに指定しているディレクトリが実際のキャッシュ保存先と違う。特にnode_modulesをキャッシュしようとしているのにnpm ciを使っているケースが多い。

# NG: node_modules をキャッシュしようとしているが...
- uses: actions/cache@v4
  with:
    path: node_modules

- run: npm ci  # npm ci は node_modules を削除してから再インストールする!
# → せっかくキャッシュした node_modules が消される

対策

# npm: グローバルキャッシュ(~/.npm)をキャッシュする
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

- run: npm ci  # ~/.npm があれば高速にインストールされる
# pnpm: pnpmのストアディレクトリをキャッシュする
- uses: actions/setup-node@v4
  with:
    node-version: 20

- name: pnpmストアのパスを取得
  id: pnpm-cache
  run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT

- uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-cache.outputs.store }}
    key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

- run: pnpm install --frozen-lockfile

注意: npm ciはnode_modulesを削除するので、node_modulesをキャッシュしても無意味。npmのグローバルキャッシュ(~/.npm)をキャッシュするのが正解。


パターン3: save-alwaysを設定していない(テスト失敗時にキャッシュが消える)

症状

  • 最初のジョブ実行は必ずCache Miss(これは正常)
  • テストが失敗した次のジョブ実行もCache Miss(これがおかしい)

原因

デフォルトでは、ジョブが失敗した場合にキャッシュのSaveフェーズがスキップされる。

# デフォルト動作
steps:
  - uses: actions/cache@v4
    with:
      path: ~/.npm
      key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

  - run: npm ci
  - run: npm test  # ← これが失敗すると...
  # Saveフェーズがスキップされる → 次回もnpm ciからやり直し

対策

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    save-always: true  # ← ジョブ失敗時もキャッシュを保存する

またはactions/setup-nodeの組み込みキャッシュを使う方法も有効(こちらはsave-always相当の動作):

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'  # 'npm' | 'yarn' | 'pnpm' が指定可能

パターン4: restore-keysの設定が機能していない

症状

  • lockfileを更新するたびに毎回Cache Miss → フルインストール
  • restore-keysを設定しているのに古いキャッシュを流用してくれない

原因

restore-keysの書き方が間違っているか、完全なキーと同じ内容を指定してしまっている。

# NG: restore-keysにkeyと同じ内容を指定(意味なし)
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
  ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
  # ↑ keyと完全一致 → restore-keysとして機能しない

# NG: プレフィックスが短すぎてOS間で混在する
restore-keys: |
  npm-
  # ↑ LinuxとWindowsのキャッシュが混ざる可能性がある

対策

key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
  ${{ runner.os }}-npm-
  # ↑ OSは一致させつつ、lockfileハッシュ部分を省略
  # → 同じOSの最新キャッシュがあれば部分流用できる

restore-keys前方一致で最新のキャッシュを取得する動作。完全一致を探して → なければrestore-keysの1行目で前方一致 → なければ2行目、という順序で処理される。


パターン5: ランナーOSのバージョン変更でキャッシュが全無効化される

症状

  • コードもlockfileも変えていないのに、ある日突然全ジョブがCache Missになる
  • 数週間〜数ヶ月に1回、定期的に発生する

原因

ubuntu-latestなどのエイリアスは定期的に実際のイメージが切り替わる(例: ubuntu-22.04 → ubuntu-24.04)。runner.osLinuxのままでも、Node.jsのバイナリ互換性などの問題でキャッシュが使えなくなることがある。

# キャッシュキーにランナーイメージバージョンが含まれていない
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
# runner.os = "Linux" は変わらない → OS切り替えを検知できない

対策

# ランナーバージョンを固定する
runs-on: ubuntu-22.04  # ubuntu-latest の代わりに固定バージョンを指定

# またはキャッシュキーに手動バージョン番号を入れる
key: ${{ runner.os }}-npm-v2-${{ hashFiles('**/package-lock.json') }}
#                            ↑ ここを v1 → v2 に変えると全キャッシュを強制クリアできる

互換性の問題でキャッシュを全クリアしたいときは、v1v2のように手動でキャッシュキーのバージョン部分を更新すれば、全ランナーのキャッシュを即座に無効化できる。


パターン6: DockerイメージのレイヤーキャッシュがCIで使われない

症状

  • ローカルのdocker buildは2回目以降高速なのに、CI上では毎回フルビルド
  • --cache-fromを指定しているのに効いていない

原因

DockerのレイヤーキャッシュとGitHub Actionsのキャッシュは仕組みが異なる。単純なdocker pullからの--cache-fromでは、前回CIでビルドしたレイヤーを活用できない。

# NG: このやり方ではキャッシュがほぼ使われない
- run: docker pull myapp:latest || true
- run: docker build -t myapp:latest --cache-from myapp:latest .
# ↑ CIランナーにはローカルのイメージ履歴がないため

対策: Docker Buildx + type=gha

- uses: docker/setup-buildx-action@v3

- uses: docker/build-push-action@v5
  with:
    context: .
    push: false
    tags: myapp:latest
    cache-from: type=gha            # GitHub Actionsキャッシュから読み込み
    cache-to: type=gha,mode=max     # GitHub Actionsキャッシュに書き込み

type=ghaを使うことで、DockerのビルドキャッシュをGitHub Actionsのキャッシュストレージに直接保存・読み込みできる。mode=maxにすると全中間レイヤーをキャッシュし、mode=min(デフォルト)は最終イメージのみ。


デバッグ方法:キャッシュが効いているか確認する

ログで確認する

GitHub Actionsのジョブログで以下の文字列を探す:

# HIT の場合
Cache restored from key: Linux-npm-abc123def456...

# MISS の場合(Saveが走る)
Cache not found for input keys: Linux-npm-abc123def456...
Saving cache for key: Linux-npm-abc123def456...

steps.outputs.cache-hit で確認する

- uses: actions/cache@v4
  id: npm-cache
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

- name: キャッシュヒット状況を表示
  run: echo "cache-hit=${{ steps.npm-cache.outputs.cache-hit }}"
  # cache-hit=true  → HIT
  # cache-hit=false または空 → MISS

この出力を使って「キャッシュMISSの場合だけ重いセットアップを実行する」といった条件分岐も書ける:

- name: キャッシュMISS時のみビルドキャッシュを生成
  if: steps.npm-cache.outputs.cache-hit != 'true'
  run: npm run build:cache

まとめ

パターン 症状 対策
lockfileパスの誤り 常にMISS パッケージマネージャに合ったlockfileを指定
pathが間違い installが毎回フル npmはグローバルキャッシュ(~/.npm)を指定
save-always未設定 失敗後もずっとMISS save-always: trueを追加
restore-keysの誤り 古いキャッシュを流用できない OSプレフィックスのみの前方一致キーを指定
ランナーOS切り替え 突然全MISSに バージョン固定 or キャッシュキーに手動番号
Dockerレイヤーキャッシュ ビルドが毎回フル Buildx + type=ghaを使う

actions/cacheはトラブル時にまずジョブログでHIT/MISSを確認し、MISSならキャッシュキーとパスの組み合わせを疑うのが定石だ。特に「パッケージマネージャのストアを正しく指定できているか」と「ジョブ失敗時にもSaveされているか」の2点を確認するだけで、大半のケースは解決できる。