CI/CDの「ビルド時間」はエンジニアの寿命である。GitHub Actionsキャッシュ戦略の深淵
テックリードとして現場を見渡すと、多くのプロジェクトで「とりあえず動く」だけのCI設定が、日々数千回の無駄なビルド時間を消費している光景を目にする。
`npm install` や `pnpm install` に毎回2〜3分費やしているなら、それは技術的負債だ。CIの待ち時間は開発者の集中力を削ぎ、フロー状態を破壊する。本稿では、GitHub Actionsにおけるキャッシュ戦略を、単なる公式ドキュメントの引用ではなく、パッケージマネージャーの内部挙動レベルで最適化する「プロの作法」を伝授する。
—
1. なぜ「キャッシュのミス」が起きるのか?
GitHub Actionsの `actions/setup-node` は非常に強力だが、デフォルト設定に甘んじていると、キャッシュの更新タイミングが不適切になり、かえってオーバーヘッドを招く。
キャッシュの最適化において重要なのは「完全な依存関係のハッシュ化」と「各マネージャー固有のキャッシュディレクトリの分離」だ。
pnpmにおける「Content-Addressable Store」の真実
特に `pnpm` を使う場合、キャッシュ戦略は劇的に変わる。`pnpm` はプロジェクト間での共有キャッシュ(ストア)を前提に設計されている。GitHub Actionsでこれを活かすには、ストアの場所を固定し、保存対象として定義する必要がある。
—
2. 【ベストプラクティス】最適化されたYAML構成
以下は、`pnpm` を使用したプロジェクトで、キャッシュのヒット率を最大化し、ビルド時間を最小化するためのGitHub Actionsの設定例だ。
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’
# 重要: setup-nodeのキャッシュ機構を使いつつ、pnpmのストアを別管理する
cache: ‘pnpm’
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
# pnpmのキャッシュディレクトリパスを動的に取得
echo “STORE_PATH=$(pnpm store path)” >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
# lockfileが変更されたときのみキャッシュを更新(ハッシュ値の追跡)
key: ${{ runner.os }}-pnpm-store-${{ hashFiles(‘/pnpm-lock.yaml’) }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
# –frozen-lockfileは必須。CI環境で勝手にlockfileを書き換えさせない
run: pnpm install –frozen-lockfile
ここがアーキテクトのこだわり:
- `restore-keys` の活用: 完璧なハッシュ一致がなくても、過去のキャッシュから近い状態を復元することで、差分インストール(`pnpm` の強み)を最大限に活かす。
- `–frozen-lockfile`: CIにおいてlockfileが改ざんされるリスクを排除し、再現性を担保する。これはチーム開発における「再現性のカオス」を防ぐための生命線だ。
—
3. 現場で「差」がつくプロのテクニック
開発時のCLI効率化:`pnpm` エイリアスとショートカット
ターミナルでのタイピングを減らすことは、脳の認知負荷を減らすことに直結する。`.zshrc` や `.bashrc` に以下の設定を入れてほしい。
pnpmのタイピングを極限まで減らす
alias pn=’pnpm’
alias pni=’pnpm install’
alias pnd=’pnpm dev’
alias pnb=’pnpm build’
依存関係を整理してキャッシュをクリーンに保つ(定期実行推奨)
alias pnp=’pnpm prune’
絶対に入れるべき神プラグイン:`lint-staged`
ビルドの前にローカルでこけるのは時間の無駄。`husky` と組み合わせて、コミット前に差分ファイルのみを対象にLint/Formatを走らせる設定を `package.json` に記述しておく。
“lint-staged”: {
“.{js,ts,jsx,tsx}”: [
“eslint –fix”,
“prettier –write”
]
}
これにより、CIで無駄なLinterエラーを検知して戻り作業が発生する確率がゼロになる。
—
4. チーム開発における「設定の共有化ルール」
テックリードとして、以下のルールをリポジトリの `CONTRIBUTING.md` に記載することを強く推奨する。
1. Lockfileのコミットは神聖な儀式である: 依存関係の変更は必ず1つのPRにまとめ、lockfileの差分を目視確認すること。
2. キャッシュの汚染を防ぐ: 開発環境の `node_modules` をクリーンに保つため、`pnpm store prune` を月1回実行する習慣を持つ。
3. CIのボトルネックを可視化する: GitHub Actionsのログで `Setup Node.js` や `Install dependencies` の時間が3分を超えたら即座に改善タスクを起票する。
最後に:エンジニアの時間は「投資」である
CI/CDのキャッシュ最適化は、単なる「速くするためのハック」ではない。ビルドを待っている間に失われる「エンジニアの思考の連鎖」を守るための防衛戦略だ。
ツールに振り回されるのではなく、その挙動を深く理解し、意図を持って制御する。その積み重ねこそが、最高峰のプロダクトを生み出すエンジニアの条件である。さあ、今すぐあなたのリポジトリのYAMLを開き、キャッシュの挙動をハックしてほしい。結果は、次のプッシュで必ず出るはずだ。