GitHub Actionsで「前回の実行ではキャッシュを保存したはずなのに、次の実行で復元されない」という場合、最初に確認すべきなのは権限ではありません。評価後のキャッシュキー、キャッシュバージョン、ブランチやPRの参照範囲、前回ジョブの保存結果を順番に照合します。
GitHub Actionsのキャッシュは、画面上で同じようなキーに見えても、pathや圧縮方式から作られるバージョン、ブランチのスコープが異なると復元されません。また、cache-hitがtrueになるのは、指定した主キーが完全一致した場合だけです。部分一致やrestore-keysで復元できていても、trueにはなりません。([GitHub Docs][1])
GitHub Actionsのキャッシュが当たらないときに確認する項目
原因を効率よく切り分けるには、次の順番で確認します。
| 確認項目 | よくある原因 | 確認する内容 |
|---|---|---|
| キャッシュキー | コミットや実行ごとに値が変わっている | 評価後のkeyを前回と今回で比較する |
| 完全一致・部分一致 | 復元できているのにcache-hitがtrueにならない | 主キー完全一致か、プレフィックス一致か、restore-keysかを分ける |
| キャッシュバージョン | pathや実行OSが変わった | キャッシュ対象、圧縮方式、OSを照合する |
| 参照範囲 | PR、ブランチ、タグのスコープが異なる | github.ref、イベント名、保存元ブランチを確認する |
| 保存条件 | 前回ジョブが失敗して保存されなかった | ジョブ終端の保存ログを確認する |
| アクセスモード | 低信頼トリガーが読み取り専用だった | 保存拒否の警告とイベント種別を確認する |
| キャッシュの存在 | 削除または容量超過で退避された | Actionsのキャッシュ一覧を確認する |
同時に複数の設定を変更すると、どの変更で直ったのか分からなくなります。まずはキー、次にバージョン、スコープ、保存条件という順序で一つずつ照合するのが確実です。
キャッシュの探索順を理解する
actions/cacheは、単純に「同じキーがあるか」だけを調べているわけではありません。基本的には次の順番で候補を探します。
- 実行中のブランチまたは参照範囲で、
keyとキャッシュバージョンが完全一致するもの keyをプレフィックスとして部分一致するものrestore-keysを上から順に検索し、完全一致またはプレフィックス一致するもの- 現在の参照範囲で見つからなければ、アクセス可能な既定ブランチなどで同様に検索する
restore-keysのプレフィックスに複数のキャッシュが一致した場合は、通常は最も新しく作成された候補が復元されます。([GitHub Docs][1])
たとえば、次の設定を使用しているとします。
with:
path: ~/.npm
key: npm-Linux-a1b2c3d4
restore-keys: |
npm-Linux-
npm-
探索される候補は、概ね次のようになります。
npm-Linux-a1b2c3d4
npm-Linux-で始まるキー
npm-で始まるキー
このうちnpm-Linux-a1b2c3d4が完全一致した場合だけ、cache-hitがtrueになります。
cache-hitがtrueでなくても復元されている場合がある
cache-hitは、キャッシュが何らかの形で復元されたかどうかではなく、主キーが完全一致したかどうかを判定するための出力です。
| 状態 | キャッシュの復元 | cache-hitの扱い |
|---|---|---|
keyが完全一致 | あり | true |
keyのプレフィックス一致 | あり | true以外 |
restore-keysで一致 | あり | true以外 |
| 一致するキャッシュなし | なし | 空文字など、true以外 |
そのため、後続処理では次のように判定するのが安全です。
if: steps.dependency-cache.outputs.cache-hit != 'true'
== 'false'だけで判定すると、キャッシュミス時の空文字を正しく扱えない場合があります。公式のactions/cacheでも、完全一致時のみtrueとなり、ミス時は空文字になることが示されています。([GitHub][2])
最短で原因を切り分ける手順
評価後のキーと実行条件をログに出す
YAMLに書かれた式だけを目視しても、実際にどの値へ展開されたかは分かりません。保存時と復元時の実行ログに、評価後のキー、イベント、参照、OS、ロックファイルのハッシュを出します。
以下はnpmのダウンロードキャッシュを確認する例です。利用しているアクションのメジャーバージョンは、リポジトリや組織で採用中のものに合わせてください。
steps:
- uses: actions/checkout@v4
- name: Calculate cache key
id: cache-key
shell: bash
env:
CACHE_KEY: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
LOCK_HASH: ${{ hashFiles('**/package-lock.json') }}
run: |
echo "key=$CACHE_KEY" >> "$GITHUB_OUTPUT"
echo "event=${{ github.event_name }}"
echo "ref=${{ github.ref }}"
echo "os=${{ runner.os }}"
echo "lock-hash=$LOCK_HASH"
echo "cache-key=$CACHE_KEY"
- name: Restore npm cache
id: dependency-cache
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ steps.cache-key.outputs.key }}
restore-keys: |
npm-${{ runner.os }}-
- name: Show cache result
shell: bash
run: |
echo "cache-hit=${{ steps.dependency-cache.outputs.cache-hit }}"
- name: Install dependencies
run: npm ci
ログで比較するのは、主に次の値です。
cache-keylock-hasheventrefoscache-hit
トークン、秘密鍵、接続文字列、回復キーなどはログに出してはいけません。キャッシュキーに秘密情報を含めることも避けます。
hashFilesが空になっていないか確認する
hashFiles()は、指定したパターンに一致するファイルがない場合、空文字を返します。たとえば次の状態では、期待したハッシュが生成されません。
actions/checkoutより前にキャッシュキーを評価しているpackage-lock.jsonの保存場所とパターンが合っていない- モノレポ化によってロックファイルの場所が変わった
- ファイル名を
package-lock.jsonから別の形式へ変更した - 対象ファイルが
GITHUB_WORKSPACEの外にある
hashFiles()のパスはGITHUB_WORKSPACEを基準として評価され、一致するファイルがない場合は空文字になります。([GitHub Docs][3])
たとえばハッシュが空になると、実際のキーは次のようになります。
npm-Linux-
この状態では、異なる依存関係を持つ実行が同じキーを使ったり、意図しない部分一致が発生したりする可能性があります。まずlock-hashが空でないことを確認してください。
毎回変わる値をキーに含めていないか確認する
次の値を主キーに含めると、実行ごとに完全一致しなくなります。
key: cache-${{ github.sha }}
key: cache-${{ github.run_id }}
key: cache-${{ github.run_number }}
コミットSHAや実行IDを含める設計では、次の実行時に主キーが必ず変わります。restore-keysによる部分一致で復元できる可能性はありますが、cache-hitがtrueになることは期待できません。
依存関係のキャッシュでは、OSとロックファイルのハッシュを組み合わせる方法が扱いやすくなります。
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
ビルドツールのバージョンやNode.jsのメジャーバージョンによってキャッシュ内容が変わる場合は、その値もキーへ含めます。
key: npm-${{ runner.os }}-node20-${{ hashFiles('**/package-lock.json') }}
重要なのは、キャッシュ内容を変える条件はキーに含め、単に実行ごとに変わる値は含めないことです。
pathと圧縮方式によるキャッシュバージョンを確認する
GitHub Actionsのキャッシュは、表示されるkeyだけでは識別されません。キャッシュ作成時のpathと圧縮ツールなどを基に、内部的なキャッシュバージョンが作られます。
そのため、次の2つは同じkeyでも別のキャッシュとして扱われる可能性があります。
path: ~/.npm
key: npm-Linux-abc123
path: |
~/.npm
~/.cache
key: npm-Linux-abc123
また、WindowsとLinuxでは圧縮方式が異なる場合があります。同じキーを指定しても、キャッシュバージョンが異なれば復元候補にはなりません。公式リファレンスでも、キャッシュバージョンにはpathと圧縮ツールの情報が含まれると説明されています。([GitHub Docs][1])
複数OSでワークフローを動かす場合は、原則としてキーにrunner.osを含めます。
key: dependencies-${{ runner.os }}-${{ hashFiles('**/lockfile') }}
restore-keysもOS単位にしておくと、異なるOS用のキャッシュを誤って候補に含めにくくなります。
restore-keys: |
dependencies-${{ runner.os }}-
Windowsと他のOSの間でキャッシュを共有するenableCrossOsArchiveもありますが、対象ファイル自体がOS間で互換性を持つことを確認したうえで使用します。単にキャッシュを当てるためだけに有効化するのは避けた方が安全です。
ブランチ・タグ・PRのキャッシュ範囲を確認する
キャッシュにはブランチや参照ごとのスコープがあります。現在の実行からアクセスできないスコープに保存されたキャッシュは、キーが同じでも復元されません。
| 実行元 | 主な挙動 |
|---|---|
既定ブランチへのpush | 既定ブランチの範囲で保存・復元される |
featureブランチへのpush | 自分のブランチと、アクセス可能な既定ブランチのキャッシュを参照できる |
pull_request | ベースブランチのキャッシュを参照できるが、新規キャッシュはPRのマージ参照に限定される |
| 別のfeatureブランチ | 兄弟ブランチのキャッシュは基本的に参照できない |
| タグ実行 | 異なるタグ名のキャッシュは参照できない |
特に注意したいのがpull_requestです。PR実行で作られたキャッシュは、通常refs/pull/.../mergeというマージ参照の範囲に作成されます。そのため、同じPRの再実行では利用できても、既定ブランチや別のPRからは復元できません。([GitHub Docs][1])
保存時と復元時のログで、次の値を比較してください。
- name: Show cache scope
shell: bash
run: |
echo "event=${{ github.event_name }}"
echo "ref=${{ github.ref }}"
echo "ref-name=${{ github.ref_name }}"
echo "base-ref=${{ github.base_ref }}"
echo "head-ref=${{ github.head_ref }}"
「PRで保存したキャッシュを、マージ後のmainで使いたい」という設計は、そのままでは成立しない場合があります。既定ブランチ用のキャッシュは、既定ブランチへのpushなど、信頼できる実行から作成する方が明確です。
前回ジョブが実際に保存まで完了したか確認する
actions/cacheは、復元時に一致するキャッシュがなかった場合、ジョブが正常終了した後に新しいキャッシュを作成します。途中のビルドやテストが失敗した場合、「Cache not found」のログは見えていても、新しいキャッシュが保存されていないことがあります。
また、既存のキャッシュは上書きされません。同じ主キーで完全一致した場合、その実行から同じキーの内容を更新することはできません。内容を更新したい場合は、新しいキーを使用します。([GitHub Docs][1])
確認するポイントは次のとおりです。
- 前回のジョブ全体が成功しているか
- ジョブ末尾のキャッシュ保存処理まで実行されているか
- 保存拒否やアップロード失敗の警告がないか
pathに実際のファイルが作成されているか- 同じキーのキャッシュがすでに存在していなかったか
Ubuntuランナーであれば、保存前に対象ディレクトリを確認できます。
- name: Inspect cache path
shell: bash
run: |
if [ -d "$HOME/.npm" ]; then
du -sh "$HOME/.npm"
find "$HOME/.npm" -maxdepth 2 -type f | head -n 20
else
echo "Cache path does not exist"
fi
対象パスが空だったり、実際に生成される場所と異なっていたりすると、期待するキャッシュにはなりません。
低信頼トリガーの保存警告をジョブ失敗と混同しない
フォークからの実行や、外部の利用者が開始できるイベントなどでは、キャッシュポイズニングを防ぐため、既定ブランチのキャッシュへの書き込みが制限されることがあります。
低信頼の実行が読み取り専用になっている場合、キャッシュ保存は拒否されます。ただし、保存できなかったことは警告として記録され、キャッシュステップやジョブ自体は成功扱いのまま継続する場合があります。([GitHub Docs][1])
このため、次の状態は矛盾していません。
ジョブ結果: 成功
キャッシュ復元: 成功
新しいキャッシュ保存: 拒否
保存警告を消すためだけに、低信頼トリガーへ書き込み権限を広げるのは避けてください。書き込み可能なcache-modeを安易に指定すると、信頼されていないコードから悪意あるキャッシュを作成される危険があります。
安全な構成は次のどちらかです。
- 既定ブランチへの
pushなど、信頼できるワークフローでキャッシュを更新する - 低信頼のワークフローでは復元専用の
actions/cache/restoreを使用する
復元専用の例は次のとおりです。
- name: Restore dependency cache
id: dependency-cache
uses: actions/cache/restore@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
権限を広げる前に、キー、対象パス、イベント、ブランチスコープを修正できないか確認することが重要です。
キャッシュ一覧で本当に保存されているか確認する
実行ログだけで判断できない場合は、リポジトリのキャッシュ一覧を確認します。現行のGitHubの画面では、リポジトリの「Actions」から「Caches」を開くと、キャッシュキー、対象ブランチ、作成時刻、最終利用時刻、使用容量などを確認できます。キーによる絞り込みも可能です。([GitHub Docs][4])
確認結果によって、原因を次のように絞れます。
| キャッシュ一覧の状態 | 疑う原因 |
|---|---|
| 該当キーが存在しない | 前回ジョブ失敗、保存拒否、保存処理未実行、削除済み |
| 同じキーが別ブランチにある | ブランチまたはPRスコープの不一致 |
| 同じキーがあるのに復元されない | キャッシュバージョン、OS、圧縮方式の不一致 |
| 古いキャッシュしかない | 主キーが毎回変わっている、保存が継続して失敗している |
| PR用キャッシュが大量にある | 容量上限によるキャッシュ退避が発生している可能性 |
必要に応じてREST APIやgh cacheコマンドを使用すると、キャッシュのバージョンや参照をさらに詳しく確認できます。
保存済みキャッシュが削除されている場合もある
GitHub.comの現行ドキュメントでは、長期間アクセスされていないキャッシュは削除対象になります。また、リポジトリの保存容量が上限へ達した場合は、最終アクセスが古いキャッシュから削除されます。
PRごとに大量のキャッシュを作成しているリポジトリでは、既定ブランチのキャッシュが頻繁に作成・削除される「キャッシュスラッシング」が起きる場合があります。容量上限や管理設定はアカウント、組織、Enterpriseの設定によって異なる可能性があるため、実際の管理画面で確認してください。([GitHub Docs][1])
よくある症状と対処方法
| 症状 | 主な原因 | 対処 |
|---|---|---|
| コミットするたびにキャッシュミスになる | github.shaを主キーに含めている | ロックファイルのハッシュなど、内容を表す値へ変更する |
ファイルは復元されたのにcache-hitがfalse | 部分一致またはrestore-keysで復元された | 完全一致ではないため正常。依存関係の更新処理を実行する |
| 同じキーなのに復元されない | pathや圧縮方式によるバージョン違い | 保存時と復元時のpath、OSを比較する |
PRでは当たるがmainでは当たらない | PRのマージ参照に保存されている | mainへのpushで既定ブランチ用キャッシュを作る |
| featureブランチ間で共有できない | 兄弟ブランチのスコープ外 | 既定ブランチのキャッシュを共通の復元元にする |
| ジョブ成功なのに保存警告がある | 読み取り専用の低信頼実行 | 警告内容を確認し、信頼できるワークフローで保存する |
| Windowsだけキャッシュミスになる | OSや圧縮方式が異なる | キーにrunner.osを含める |
| 古い内容が復元される | restore-keysが広すぎる | より具体的なプレフィックスへ絞る |
| 突然すべてミスするようになった | キャッシュ削除、容量上限、管理設定 | キャッシュ一覧とリポジトリ設定を確認する |
restore-keysは具体的なものから並べる
restore-keysは、上から順番に検索されます。広すぎるキーを先に置くと、別用途の古いキャッシュが復元される可能性があります。
推奨しやすい構成は、OSなどの互換性条件を残したまま、ハッシュ部分だけを外す形です。
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
次のように最後まで広げる構成は、複数OSや複数プロジェクトのキャッシュが混在する場合に注意が必要です。
restore-keys: |
npm-${{ runner.os }}-
npm-
複数アプリを含むモノレポでは、アプリ名も含めます。
key: npm-frontend-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }}
restore-keys: |
npm-frontend-${{ runner.os }}-
バックエンド用のキャッシュと混ざらないため、意図しない復元を防ぎやすくなります。
キャッシュが復元されないときの最終チェックリスト
最後に、次の順番で確認してください。
- 前回と今回の評価後の
keyが一致しているか hashFiles()が空文字になっていないかcache-hitがtrueでなくても、部分一致で復元されていないか- 保存時と復元時の
pathが同じか runner.osや圧縮方式が変わっていないかgithub.event_nameとgithub.refが前回と同じ範囲か- PRのマージ参照にだけ保存されていないか
- 前回ジョブが最後まで成功したか
- ジョブ終端に保存拒否やアップロード失敗の警告がないか
- Actionsのキャッシュ一覧に該当キーが存在するか
- 容量上限や削除によってキャッシュが消えていないか
最も重要なのは、完全一致キー、部分一致、restore-keysによる復元を分けて考えることです。そのうえで、同じキーに見えても異なる可能性があるキャッシュバージョンと、ブランチ・PRのスコープを照合します。
低信頼の実行で保存警告が出た場合も、すぐに権限を広げてはいけません。信頼できるワークフローでキャッシュを更新し、低信頼側は復元専用にする構成を優先してください。
[1]: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching “Dependency caching reference – GitHub Docs”
[2]: https://github.com/actions/cache “GitHub – actions/cache: Cache dependencies and build outputs in GitHub Actions · GitHub”
[3]: https://docs.github.com/en/actions/reference/workflows-and-actions/expressions “Evaluate expressions in workflows and actions – GitHub Docs”
[4]: https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manage-caches “Managing caches – GitHub Docs”

コメント