Github ActionsにおけるNode.jsパッケージのキャッシュについて
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 などで共通化して利用することをおすすめする。