背景
- GitHub ActionsでCI/CDの設定をしていたとき、
actions/setup-node@v4にcache: "npm"という設定があった - 「これ何をキャッシュしてるのだろう」と気になったので調べてみた
今回のワークフロー
調べるきっかけになったのは下記のymlファイル
name: GitHub Actions Demo
on: push
jobs:
build:
name: build application
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm" # ← これが何をしているのか
- name: Install dependencies
run: npm install
- name: Build application
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-artifact
path: dist
Node.jsをセットアップしてアプリケーションをビルドする、シンプルなワークフロー
ポイントはcache: "npm"の部分。これが何をキャッシュしているかを理解するには、まず~/.npmディレクトリを知る必要がある
~/.npmとは何か
npmがパッケージをダウンロードしたときに、そのtarball(圧縮ファイル)を一時的に保存しておく場所
macOS/Linuxでは~/.npm、Windowsでは%AppData%/npm-cacheに配置される
ディレクトリ構造
~/.npm/
└── _cacache/
├── content-v2/ # 実際のキャッシュデータ(パッケージのtarball等)
│ └── sha512/ # SHA-512ハッシュで管理
├── index-v5/ # パッケージ名・バージョン → コンテンツのマッピング
└── tmp/ # 一時ファイル
~/.npmとnode_modulesの違い
ここで混同しやすいのが~/.npmとnode_modulesの関係
~/.npm(キャッシュ) |
node_modules(インストール先) |
|
|---|---|---|
| 場所 | グローバル(ユーザーごとに1つ) | プロジェクトごと |
| 中身 | 圧縮されたtarball + インデックス | 展開済みのJSファイル等 |
| 用途 | 再ダウンロードを防ぐ | Node.jsのrequire()/importが参照する |
| プロジェクト間で共有 | される | されない |
| 削除しても大丈夫? | OK(次回再ダウンロードされる) | OK(npm installで再作成される) |
~/.npmはブラウザのキャッシュのようなものとイメージすると良いかもしれない
~/.npmにキャッシュが残る流れ
npm install axiosを実行した場合の流れを具体的に追ってみる
1. パッケージのメタデータを取得
npmがレジストリ(https://registry.npmjs.org/axios)に問い合わせて、利用可能なバージョン情報や依存関係を取得する
2. バージョン解決
semverの範囲指定に基づいて、インストールするバージョンを決定する。axiosの依存パッケージ(follow-redirects等)も同様に解決される
3. キャッシュの確認
各パッケージについて、~/.npm/_cacache/内を確認する
-
index-v5/でパッケージのSHA-512ハッシュを検索 - 対応するコンテンツが
content-v2/に存在すれば → ダウンロード不要 - 存在しなければ → ダウンロードが必要
4. ダウンロード(キャッシュミスの場合のみ)
レジストリからtarball(例: axios-1.7.9.tgz)をダウンロードする
5. キャッシュに保存
ダウンロードしたtarballを~/.npm/_cacache/に保存する
- コンテンツのSHA-512ハッシュを計算
-
content-v2/sha512/<ハッシュ先頭2文字>/<次の2文字>/<完全なハッシュ>に保存 -
index-v5/にインデックスエントリを作成 - 書き込み後に整合性を検証
6. node_modulesに展開
キャッシュから(もしくはダウンロードしたものから)tarballを./node_modules/axios/に展開する
7. プロジェクトファイルの更新
package.jsonとpackage-lock.jsonが更新される
GitHub Actionsのcache: "npm"が何をしているか
ここまでの知識を踏まえて、本題に戻る
actions/setup-node@v4のcache: "npm"は、内部でactions/cacheを使って**~/.npmディレクトリ全体をGitHubのキャッシュストレージに保存・復元している**
初回実行時(キャッシュなし)
-
setup-nodeが実行される → キャッシュが見つからない -
npm installが全パッケージをレジストリからダウンロード - ダウンロードされたtarballが
~/.npm/_cacache/に保存される -
ジョブ終了後:
~/.npm全体がGitHubのキャッシュストレージに保存される
2回目以降(キャッシュあり)
-
setup-nodeが実行される →package-lock.jsonのハッシュをキーにキャッシュを検索、ヒット - GitHubのキャッシュストレージから
~/.npmが復元される -
npm installが実行される →~/.npmにtarballがあるのでダウンロードをスキップ - tarballが
node_modules/に展開される
キャッシュキーの仕組み
キャッシュのキーはpackage-lock.jsonのハッシュ値から生成される
- lockfileが同じ → 同じキー → キャッシュヒット
- lockfileが変わった(依存パッケージの更新等) → 別のキー → キャッシュミス(再ダウンロード)
なぜnode_modulesではなく~/.npmをキャッシュするのか
node_modulesを直接キャッシュすれば良くね?と思うが、~/.npmをキャッシュする理由がある
-
node_modulesにはネイティブモジュール(C++でコンパイルされたバイナリ等)が含まれることがあり、OS・Node.jsバージョンが変わると動かない可能性がある -
~/.npmはtarball(圧縮された元データ)なので、環境に依存しないらしい
感想
- CI/CDの設定は「動けばOK」になりがちだったが、別のツールの理解になるなぁ