Github ActionsにおけるNode.jsパッケージのキャッシュについて
Github ActionsにおけるNode.jsパッケージのキャッシュについて、actions/setup-nodeの推奨する方法やnode_modulesをキャッシュする方法、その周辺について紹介する。
actions/setup-nodeが紹介するキャッシュの設定
actions/setup-node
はv2以降、cache
に利用しているパッケージマネージャー名を指定すれば、グローバルパッケージデータをキャッシュすることが出来る。
# npmの場合
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
# yarnの場合
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "yarn"
- run: yarn install --frozen-lockfile
# pnpmの場合
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
しかし、意図的にこの設定を無視または理解せずに有効化した上でnode_modulesディレクトリを丸ごとキャッシュしている場合が多くみられる。
actions/setup-node
のキャッシュ機能の誤解
actions/setup-node
のcache
機能が入った当初、何をキャッシュするか明示されていなかっため、多くのユーザーがnode_modules
をキャッシュするものと誤解し混乱が生まれていた。
現在はREADMEにCaching global packages data
と記載されているが、global packages data
が何を指しているのかが伝わらず、未だに誤解されている場合がある。
Global package dataとは
各パッケージマネージャーは、基本的にパッケージのインストールがリクエストされた場合、次のようなステップを踏む。
- グローバルストアにキャッシュが存在するか確認する。
- 存在しない場合、パッケージを特定のディレクトリにダウンロードする。
- 存在する又はダウンロードが完了したら、そのディレクトリからnode_modulesにコピーする。
- pnpmの場合は、コピーではなくハードリンクを張る。
ここで言う特定のディレクトリが、グローバルパッケージデータの指すものである。Yarnにおいてパッケージキャッシュ、pnpmではパッケージストアと呼称されている。
それぞれのパッケージマネージャーにおけるグローバルパッケージデータのディレクトリは次の通りになっている。
ディレクトリ名 | パスの取得方法 | |
---|---|---|
npm | .npm | npm config get cache |
yarn | .cache/yarn | yarn cache dir |
yarn (v2~) | .yarn/cache | yarn config get cacheFolder |
pnpm | pnpm/store | pnpm store path —silent |
グローバルパッケージデータをキャッシュする戦略
グローバルパッケージデータをキャッシュすることで、パッケージが存在していればダウンロードステップを省略することが出来る。
actions/setup-node
のcache
機能は、これを行っている。
actions/setup-node
のcache
機能を利用せずに、グローバルパッケージデータをキャッシュする場合、次のように記述すれば良い。
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Get Yarn cache directory path
id: yarn-cache-dir
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install --frozen-lockfile
Global package dataをキャッシュする戦略が遅い場合
グローバルパッケージデータをキャッシュする戦略では、キャッシュされているパッケージが古くなっていないかの確認にかかる時間が支配的になる。
どのパッケージマネージャーでも--prefer-offline
フラグを利用することで、この確認ステップをバイパスすることが出来るので検討してみると良い。
ただしnpmではバグが報告されているため注意。
actions/setup-node
はなぜnode_modules
をキャッシュしないのか
前述の通り、actions/setup-node
はグローバルパッケージデータをキャッシュし、node_modules
をキャッシュしない。
これは次のような理由だと考えられる。
- グローバルパッケージデータをキャッシュする場合、異なるNode.jsバージョン間でキャッシュを再利用できる。
- 複数のnodeバージョンでテストが必要な場合などに有効。
node_modules
のキャッシュではパッケージの確認が行われないため、キャッシュの制御が不十分である場合、不整合を起こす可能性がある。- グローバルストアのキャッシュで十分な効果を得られる場合がある。
- 特にpnpmやyarn v2以降では、ハードリンクを張るだけであるため非常に有効である。
- npmの場合、あまり改善されない印象がある。
node_modules
をキャッシュする戦略
node_modules
をキャッシュする選択では、キャッシュがある場合、ダウンロードステップと確認ステップに加えてコピーステップもスキップできるため、大幅に実行時間が短縮できる。
次のように記述する。
- uses: actions/setup-node@v4
id: node
with:
node-version: 20
- uses: actions/cache@v4
id: cache
with:
key: ${{ runner.arch }}-${{ runner.os }}-node-${{ steps.node.outputs.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }}
path: |
node_modules
- run: yarn install --frozen-lockfile
if: steps.cache.outputs.cache-hit != 'true'
多くの人にとってグローバルパッケージデータを扱う方法よりも分かりやすいと思うが、次の点に注意した上で利用をした方がいい。
- キャッシュのkeyが曖昧な場合、不整合を起こす可能性がある。
- Github Actionsのキャッシュストレージを圧迫し、キャッシュミスを発生する可能性が高まる。
- keyが過度に厳密であったり、複数の環境でCIを走らせる必要がある場合、キャッシュが肥大化する。
- ブランチ毎にキャッシュは管理されるため、ブランチを大量に生やしてもリスクが高まる。
どちらを使えばいいのか
次のように考えているが、実際に利用する場合は自身で計測し利用してほしい。
グローバルパッケージデータが適しているケース
- pnpmなどハードリンクを張るパッケージマネージャーを利用している。
- 複数のNode.jsバージョンでテストを走らせる必要がある。
node_modulesがいいかもしれないケース
- npmやYarn v1などコピーを行うパッケージマネージャーを利用している。
node_modules
のキャッシュを取る戦略を採用する場合、十分に注意した上での利用が必要なため、複数のリポジトリに導入するのであればComposite Actionsなどで共通化して利用することをおすすめする。