GitHub Actionsのキャッシュが復元されない原因と確認手順|キー・範囲・保存条件を切り分ける

GitHub Actionsで「前回の実行ではキャッシュを保存したはずなのに、次の実行で復元されない」という場合、最初に確認すべきなのは権限ではありません。評価後のキャッシュキー、キャッシュバージョン、ブランチやPRの参照範囲、前回ジョブの保存結果を順番に照合します。

GitHub Actionsのキャッシュは、画面上で同じようなキーに見えても、pathや圧縮方式から作られるバージョン、ブランチのスコープが異なると復元されません。また、cache-hittrueになるのは、指定した主キーが完全一致した場合だけです。部分一致やrestore-keysで復元できていても、trueにはなりません。([GitHub Docs][1])

目次

GitHub Actionsのキャッシュが当たらないときに確認する項目

原因を効率よく切り分けるには、次の順番で確認します。

確認項目よくある原因確認する内容
キャッシュキーコミットや実行ごとに値が変わっている評価後のkeyを前回と今回で比較する
完全一致・部分一致復元できているのにcache-hittrueにならない主キー完全一致か、プレフィックス一致か、restore-keysかを分ける
キャッシュバージョンpathや実行OSが変わったキャッシュ対象、圧縮方式、OSを照合する
参照範囲PR、ブランチ、タグのスコープが異なるgithub.ref、イベント名、保存元ブランチを確認する
保存条件前回ジョブが失敗して保存されなかったジョブ終端の保存ログを確認する
アクセスモード低信頼トリガーが読み取り専用だった保存拒否の警告とイベント種別を確認する
キャッシュの存在削除または容量超過で退避されたActionsのキャッシュ一覧を確認する

同時に複数の設定を変更すると、どの変更で直ったのか分からなくなります。まずはキー、次にバージョン、スコープ、保存条件という順序で一つずつ照合するのが確実です。

キャッシュの探索順を理解する

actions/cacheは、単純に「同じキーがあるか」だけを調べているわけではありません。基本的には次の順番で候補を探します。

  1. 実行中のブランチまたは参照範囲で、keyとキャッシュバージョンが完全一致するもの
  2. keyをプレフィックスとして部分一致するもの
  3. restore-keysを上から順に検索し、完全一致またはプレフィックス一致するもの
  4. 現在の参照範囲で見つからなければ、アクセス可能な既定ブランチなどで同様に検索する

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-hittrueになります。

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-key
  • lock-hash
  • event
  • ref
  • os
  • cache-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-hittrueになることは期待できません。

依存関係のキャッシュでは、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-hitfalse部分一致または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-hittrueでなくても、部分一致で復元されていないか
  • 保存時と復元時のpathが同じか
  • runner.osや圧縮方式が変わっていないか
  • github.event_namegithub.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”

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次