GitHub ActionsでCI/CDパイプラインを高速化しようとactions/cacheを設定したのに、ジョブが毎回「Cache Miss」になって全くキャッシュが効かない。この問題にハマったことがある人は多いはずだ。
原因は1つではなく、よくあるパターンが6つある。それぞれの症状・原因・対策を解説する。
キャッシュの仕組みをおさらい
まずactions/cacheの基本動作を押さえておこう。
- Restore フェーズ:
keyで完全一致を探す → なければrestore-keysで前方一致を試みる - ジョブ実行
- 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 installやpnpm 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.osはLinuxのままでも、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 に変えると全キャッシュを強制クリアできる
互換性の問題でキャッシュを全クリアしたいときは、v1→v2のように手動でキャッシュキーのバージョン部分を更新すれば、全ランナーのキャッシュを即座に無効化できる。
パターン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点を確認するだけで、大半のケースは解決できる。
