GitHub Actions で Node.js プロジェクトの CI を構築する
Verified: checkout is at v7 (v7.0.1), setup-node at v7, cache at v5; Node 24 is Active LTS, 22 is Maintenance LTS, 20 is EOL. Rewriting accordingly.
概要
Node.js プロジェクトに GitHub Actions で CI を導入し、「Pull Request を出したら自動で Lint・型チェック・テストが走り、緑にならないとマージできない」状態をゼロから構築するハンズオンです。最小構成から始め、ジョブ分割による並列化、複数バージョンでのマトリクス実行、キャッシュによる高速化、そして必須チェック化まで段階的に進めます。
内容はパブリックに公開されている GitHub Actions・npm の仕様のみに基づいています。
環境
- Node.js(LTS 系。本記事では Active LTS の
24と Maintenance LTS の22を対象) - npm(
package-lock.jsonをコミット済みであること) - GitHub リポジトリ(Actions が有効)
package.json に以下のスクリプトがある前提で進めます。
{
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run"
}
}
なぜ CI を入れるのか
CI を入れる前は、品質チェックが「人の記憶」と「手作業」に依存します。手作業は必ず抜けます。結果として、別の Node バージョンや他メンバーの環境で壊れる、テスト実行を忘れてマージされ main が壊れる、「動作確認しました」の基準が人によってバラバラでレビューの担保にならない、といった状態が生まれます。
GitHub Actions は push / pull_request などのイベントを起点に、決まった環境で決まったチェックを必ず走らせます。これで抜けを構造的に潰します。
ステップ1: 最小構成
.github/workflows/ 配下に YAML を置くだけで CI が動きます。
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
- run: npm ci
- run: npm test
ポイントは3つです。on.pull_request で PR ごとに実行し push: branches: [main] で main 反映時にも実行すること、依存インストールは npm install ではなく npm ci を使うこと、アクションはメジャーバージョン(@v7)を固定することです。
ステップ2: ジョブを分けて並列化する
Lint・型チェック・テストをジョブに分けると並列実行され、GitHub の UI 上で「どの工程で落ちたか」が一目で分かります。
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- run: npm ci
- run: npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- run: npm ci
- run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- run: npm ci
- run: npm test
全体の所要時間は「一番遅いジョブ」に収束するため、直列実行より速く終わります。
ステップ3: 複数 Node バージョンでマトリクス実行
サポートしたいバージョンを横断でテストします。ここでは Active LTS の 24 と Maintenance LTS の 22 を対象にします。
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: ["24", "22"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm test
fail-fast: false にすると、片方のバージョンが落ちても、もう片方の結果まで最後まで確認できます。
ステップ4: キャッシュで高速化する
actions/setup-node の cache: "npm" を指定すると、~/.npm が package-lock.json のハッシュをキーに保存・復元され、依存が変わらない限り npm ci が速くなります。細かく制御したい場合は actions/cache を直接使います。
- uses: actions/cache@v5
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
基本は setup-node の cache オプションで十分で、モノレポなど特殊なケースで actions/cache の直接利用を検討する、という優先順位で問題ありません。
なぜ npm ci なのか
CI では npm install ではなく npm ci を使います。package-lock.json を厳密に尊重し、ロックと package.json が食い違うとエラーで止まる(暗黙のバージョン更新が起きない)こと、node_modules を毎回クリーンにするため再現性が高いこと、クリーンインストール用に最適化され高速であることが理由です。「ローカルでは動くのに CI で壊れる」の一因はロックファイルの無視なので、CI では npm ci を徹底します。
確認方法
ci.ymlを.github/workflows/に置いてコミット・push する- PR を作成する
- PR 画面下部の Checks 欄に各ジョブ(
lint/typecheck/test)が並び、緑(成功)/赤(失敗)で表示される - わざと
testを落とすコミットを混ぜ、赤くなり、マージ前に検知できることを確認する - Settings → Branches で
mainを保護し、これらのジョブを Require status checks to pass に指定すると、チェックが緑でない PR はマージできなくなる
必須チェック化まで行って初めて、「壊れたものがマージされない」状態になります。ここが CI 導入の実質的なゴールです。
注意点
- アクションのバージョン固定:
@v7のようにメジャーは固定する。厳密に信頼したい場合はコミットSHA固定も選択肢です。 - フォークからの PR: フォーク元の PR では
secretsが既定で渡りません。外部コントリビュータを受ける公開リポジトリでは設計に注意が必要です。 - シークレットのログ露出:
secretsはログにマスクされますが、echoなどで自前展開すると漏れます。ログに出さない運用を徹底します。 - 無料枠: パブリックリポジトリの実行は無料、プライベートは実行時間に無料上限があります。マトリクスの増やしすぎに注意します。
まとめ
.github/workflows/ci.ymlを置くだけで、PR 起点の自動チェックが構築できる- ジョブ分割で並列化と失敗箇所の可視化を同時に得る
npm ciとcache: "npm"で再現性と速度を両立する- マトリクスで複数 Node バージョンを横断チェックする
- 最後は Branch protection の必須チェック化まで行い、「壊れたものはマージされない」状態を作る
最小構成から始めて、チームの必要に応じて段階的に育てていくのが CI 構築の王道です。
