1311 文字
7 分

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-nodecache機能が入った当初、何をキャッシュするか明示されていなかっため、多くのユーザーがnode_modulesをキャッシュするものと誤解し混乱が生まれていた。

現在は README にCaching global packages dataと記載されているが、global packages dataが何を指しているのかが伝わらず、未だに誤解されている場合がある。

Global package data とは#

各パッケージマネージャーは、基本的にパッケージのインストールがリクエストされた場合、次のようなステップを踏む。

  1. グローバルストアにキャッシュが存在するか確認する。
  2. 存在しない場合、パッケージを特定のディレクトリにダウンロードする。
  3. 存在する又はダウンロードが完了したら、そのディレクトリから node_modules にコピーする。
    • pnpm の場合は、コピーではなくハードリンクを張る。

ここで言う特定のディレクトリが、グローバルパッケージデータの指すものである。Yarn においてパッケージキャッシュ、pnpm ではパッケージストアと呼称されている。

それぞれのパッケージマネージャーにおけるグローバルパッケージデータのディレクトリは次の通りになっている。

ディレクトリ名パスの取得方法
npm.npmnpm config get cache
yarn.cache/yarnyarn cache dir
yarn (v2~).yarn/cacheyarn config get cacheFolder
pnpmpnpm/storepnpm store path —silent

グローバルパッケージデータをキャッシュする戦略#

グローバルパッケージデータをキャッシュすることで、パッケージが存在していればダウンロードステップを省略することが出来る。

actions/setup-nodecache機能は、これを行っている。

actions/setup-nodecache機能を利用せずに、グローバルパッケージデータをキャッシュする場合、次のように記述すれば良い。

- 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 などで共通化して利用することをおすすめする。

Github ActionsにおけるNode.jsパッケージのキャッシュについて
https://blog.ohirunewani.com/posts/github-actions-setup-node-cache/
作者
hrdtbs
公開日
2024-09-21
ライセンス
CC BY-NC-SA 4.0