GitHub Actions Matrix ビルド:マルチバージョン並列テスト実践
先週プロジェクトをリリースして、午後にもならないうちにユーザーからフィードバックが届きました。Node 16 環境でページが真っ白になるというのです。その瞬間、頭の中がカッと熱くなりました。
調査にまるまる 2 時間かかりました。ログを一通り読み返し、コードを 3 回見比べた末に、ある API が Node 16 で JSON.stringify() を処理する挙動が Node 20 と違うことが分かりました。古いバージョンは循環参照に出会うと例外をスローし、新しいバージョンは黙って処理します。CI パイプラインは Node 20 だけをテストしていたので、この互換性の問題はまったく食い止められなかったのです。
事後の振り返りで考えました。もし最初からマルチバージョン並列テストを設定していたら、こうした問題はリリース前に発見できていたはずだと。それ以来、私は GitHub Actions の Matrix ビルドを真剣に研究し始めました。この言葉は少し物々しく聞こえますが、要するに 1 つのタスクを自動的に複数に分割し、異なるバージョンや異なるプラットフォームで同時に走らせることです。
この記事では、Matrix を基礎から応用まで一通り解説します。基本構文、exclude/include によるフィルタリングの組み合わせ、fail-fast 戦略の選び方、max-parallel によるリソース制御を扱います。最後に、そのままコピーして使える完全な Node.js マルチバージョンテストのテンプレートをお渡しします。読むのに約 10 分。読み終えたらすぐ、CI をマルチバージョン並列実行に変えられます。
Matrix の基礎 — 5 分でマスター
Matrix は一言で理解できます。ジョブを 1 つ書けば、GitHub Actions が自動的に複数の並列タスクへ展開してくれます。
例を挙げましょう。3 つの Node.js バージョン [18, 20, 22] を定義すると、Matrix は 3 つの独立したテストタスクを作り、それぞれ Node 18、Node 20、Node 22 の環境で実行します。この 3 つのタスクは同時に起動して同時に走り、互いに干渉しません。
最もシンプルな構成は次のとおりです:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci && npm test
注目すべきは strategy.matrix の部分です。node-version は自分で定義した変数名で、その後の配列 [18, 20, 22] がこの変数の取りうる値です。GitHub Actions はこの配列を走査し、毎回 1 つの値を matrix.node-version に代入して、対応するジョブインスタンスを作成します。
${{ matrix.node-version }} という構文は、現在の値を参照しています。1 回目の実行では 18、2 回目は 20、3 回目は 22 です。
初めて使ったとき、ひとつ疑問がありました。この 3 つのタスクは直列か並列か?答えはデフォルトで並列です。コードを 1 回プッシュすると、GitHub は 3 つの Runner を同時に起動し、3 つのタスクをまとめて走らせます。実測では、以前は 3 バージョンを直列でテストするのに 15 分かかっていましたが、Matrix に変えたら 5 分で済むようになりました。3 つのタスクが同時に走るので、時間は最も遅いタスクの所要時間まで圧縮されるのです。
ただし注意点があります。並列実行はより多くの Runner 分数を消費します。3 つのタスクなら 3 倍の時間消費です。無料枠(月 2000 分)を使っている場合、大きなマトリックスはあっという間に枠を食い尽くしかねません。これについては後で max-parallel のところで詳しく説明します。
exclude/include フィルタリング — テストマトリックスの細かな制御
複数のディメンションを組み合わせ始めると、Matrix の組み合わせ数は指数関数的に増えていきます。
たとえば 3 つの Node バージョン [16, 18, 20] と 3 つの OS [ubuntu, windows, macos] を組み合わせると、3 x 3 = 9 タスクです。さらにテストスイート [unit, integration, e2e] を加えると 27 になります。これは多いか少ないか?オープンソースプロジェクトならまだ良いかもしれませんが、小さなチームにとっては Runner 分数がそのままコストです。
しかも、組み合わせの中には初めから意味のないものもあります。Node 16 はすでに EOL(End of Life)で、Windows や macOS でテストするのは時間の無駄です。こういうときは exclude でこれらの組み合わせを取り除きます。
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]
exclude:
- node-version: 16
os: windows-latest
- node-version: 16
os: macos-latest
exclude の書き方は、除外したい組み合わせを並べるだけです。上の構成は Node 16 + Windows と Node 16 + macOS の 2 つの組み合わせを削除します。もともと 9 タスクだったのが 7 タスクになり、Runner 分数を 22% 直接節約できます。
include は逆の操作で、特定の組み合わせや追加変数を加えます。たとえば Node 23 の実験的バージョンテストを追加したいけれど、Ubuntu だけで走らせたい場合:
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest]
include:
- node-version: 23
os: ubuntu-latest
experimental: true
ここに細かいポイントがあります。include は組み合わせを追加するだけでなく、特定の組み合わせに追加変数を持たせられます。上の experimental: true は Node 23 のタスクにだけ存在します。後続のステップでこの変数を判定すれば、たとえば実験的バージョンが失敗してもワークフロー全体を止めないようにできます:
- name: Run tests
run: npm test
continue-on-error: ${{ matrix.experimental == true }}
私が一度ハマったのは、exclude と include の優先順位です。GitHub Actions はまず include で組み合わせを追加し、その後 exclude で削除します。ですから、ある組み合わせを include しておきながら exclude でも除外すると、その組み合わせは出現しません。順序を取り違えると、予想と違う結果になることがあります。
この 2 つの使い方をまとめると:
exclude:不要な組み合わせを削除し、コストと時間を節約include:特別な組み合わせを補い、さらに追加変数で差別化処理も可能
fail-fast と max-parallel — 並列戦略のチューニング
Matrix にはデフォルトで、あまり気づかれていない挙動があります。fail-fast: true です。
どういう意味か?マトリックス内のどれか 1 つのタスクが失敗すると、GitHub Actions はただちに他の実行中タスクをキャンセルします。たとえば 10 タスクを並列実行していて、3 番目のタスクが 1 分で失敗すると、残りの 7 タスクは即座に打ち切られます。
この挙動は良いのか悪いのか?場面によります。
PR チェックのときは、fail-fast は良いことです。誰かがコードを提出して Node 18 のテストが落ちたなら、他のバージョンの完了を待つ必要はありません。すぐに作者へフィードバックして直してもらえます。時間もリソースも節約できます。
しかし Nightly テストや定期的な回帰テストでは、fail-fast は良くないかもしれません。欲しいのは完全なテストレポートだからです。どのバージョンに問題があり、どのバージョンは問題ないのか。Node 18 が落ちた時点で他のタスクを止めてしまうと、Node 20 にも同じ問題があるのか分かりません。このときは fail-fast: false を設定すべきです。
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
max-parallel は、同時に走らせるタスク数を制御します。デフォルトは無制限で、GitHub は可能な限りすべてのタスクを同時に起動します。しかし大きなマトリックス、たとえば 30 組み合わせの場合、一度にすべての Runner リソースを食い尽くしたくないこともあります。
strategy:
fail-fast: true
max-parallel: 6
matrix:
node-version: [16, 18, 20, 22]
test-suite: [unit, integration, e2e]
上の構成は、同時実行を最大 6 タスクに制限します。30 組み合わせはバッチで実行され、1 バッチあたり 6 タスクです。利点は Runner リソースが制御でき、一度に枠を食い尽くさないこと。欠点は総時間が長くなることです。
場面に応じて選べるよう、簡単な意思決定表にまとめました:
| 場面 | fail-fast | max-parallel | 理由 |
|---|---|---|---|
| PR チェック | true | 制限なし | 高速フィードバック、1 つ失敗したら停止、時間節約 |
| Nightly テスト | false | 4-6 | 完全な問題レポートを収集し、すべての bug を特定 |
| 大規模マトリックス(>20 組み合わせ) | true | 4 | リソース消費を抑え、枠の爆発を回避 |
| 実験的バージョンテスト | false | 制限なし | 実験的バージョンの失敗が全体判断に影響しない |
正直に言うと、ほとんどの場合はデフォルトの fail-fast: true で十分です。完全な診断が必要なときだけ false に変えます。そして max-parallel は小規模マトリックス(10 以内)にはほとんど影響せず、大規模マトリックスでこそ真剣に考える価値があります。
ひとつ注意しておきます。max-parallel は GitHub Actions が同時に起動するタスク数を制限するだけで、Runner の数を制限するものではありません。self-hosted runner(自前の Runner)を使っている場合、小さく設定しすぎると逆にキューで待たされ、全体の時間が延びます。パブリック Runner の場合にこそ、この設定が意味を持ちます。
完全な実践テンプレート — Node.js マルチバージョン並列テストパイプライン
ここまでは断片的な知識点でした。この章では、そのままコピーして使える完全な構成テンプレートをお渡しします。
このテンプレートに含まれるもの:
- 3 つの Node バージョン(16、18、20)
- 2 つのテスト(unit と integration)
- 2 つの OS(Ubuntu と Windows)
- 依存インストールを高速化する自動キャッシュ
- Node 16 の Windows テストを除外(EOL バージョン)
name: Multi-Version Test Matrix
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 6
matrix:
node-version: [16, 18, 20]
test-suite: [unit, integration]
os: [ubuntu-latest, windows-latest]
exclude:
- node-version: 16
os: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ${{ matrix.test-suite }} tests
run: npm run test:${{ matrix.test-suite }}
いくつかの要点を説明します:
runs-on: ${{ matrix.os }}:OS も動的で、各タスクはマトリックスの組み合わせに応じて対応する Runner を選びます。
cache: 'npm':これは setup-node 標準のキャッシュ機能です。package-lock.json のハッシュに基づいて npm 依存をキャッシュし、2 回目の実行ではそのまま再利用して再ダウンロードしません。実測では、このキャッシュで依存インストール時間を 50%以上削減できます。
fail-fast: false:ここでは意図的に false に設定しています。マルチバージョンテストの目的は、すべての問題を発見することだからです。1 つのバージョンが落ちても、他のバージョンは最後まで走らせます。
npm run test:${{ matrix.test-suite }}:package.json に test:unit と test:integration の 2 つのコマンドが定義されている前提で、Matrix がそれぞれ呼び出します。
この構成で合計いくつのタスクが生まれるでしょうか?
3 バージョン x 2 テスト x 2 OS = 12 タスク。そこから除外した Node 16 + Windows(テストスイート 2 つ分)を引いて、残り 10 タスクです。
実測データ:この構成でいくつかのプロジェクトを走らせたところ、キャッシュと組み合わせて、CI 時間は以前の直列 25 分から 8 分前後まで圧縮できました。時間が節約できたのは、主に並列実行と依存キャッシュの 2 点です。
プロジェクトがさらに大きく、組み合わせが増える場合は、次を検討できます:
max-parallelの上限を上げる(たとえば 8 や 10)- e2e テストを別ジョブに切り出し、全体の足を引っ張らないようにする
continue-on-errorで実験的バージョンの失敗を処理する
このテンプレートをあなたの .github/workflows/test.yml にコピーし、実際の状況に合わせてバージョン番号とテストスイート名を調整すれば、たいてい動くはずです。
まとめ
ずいぶん語りましたが、核心的なポイントをまとめます:
Matrix は本質的に、1 つのジョブを複数の並列タスクへ自動展開するものです。構成はシンプルで、効果は直接的です。1 回のプッシュで 3 つのバージョンが同時に走り、CI 時間は最も遅いタスクの所要時間まで圧縮されます。
exclude/include は細かな制御の手段です。組み合わせが多すぎるときは exclude で不要なタスクを削れば、Runner 分数を 20%以上直接節約できます。include は特別な組み合わせを補い、さらに追加変数で差別化処理もできます。
fail-fast はデフォルトで true、1 つ失敗したら他のタスクを止めます。PR チェックはデフォルトのままで良く、Nightly テストは false に変えて完全なレポートを収集します。max-parallel は並列数の上限を制御し、大規模マトリックスでこそ注目すべき設定です。
キャッシュは標準装備です。setup-node の cache 機能は、たった 1 行で依存インストール時間を半分節約できます。
次のステップの提案:上の完全なテンプレートをプロジェクトの .github/workflows/ ディレクトリにコピーし、まず Node.js 3 バージョンテストを走らせてみてください。問題がなければ、徐々にマルチプラットフォームとマルチテストスイートへ拡張していきます。キャッシュ構成で詰まったら、シリーズ記事『GitHub Actions キャッシュ戦略:CI/CD パイプラインを 5 倍高速化』を見てください。より細かいテクニックが載っています。
マルチバージョンテストというのは、早く設定するほど良いものです。リリースして問題が出てから後悔しないように。あの真っ白な画面のバグは、今思い出しても頭が痛くなります。
GitHub Actions Matrix マルチバージョンテストの構成
ゼロからマルチバージョン並列テストパイプラインを構築し、Node.js 16/18/20 バージョンと Ubuntu/Windows プラットフォームをカバー
⏱️ 目安時間: 15 分
- 1
ステップ1: Workflow ファイルの作成
プロジェクトルートに `.github/workflows/test.yml` ファイルを作成:
• ディレクトリ構造が正しいことを確認:`.github/workflows/`
• ファイル名はカスタマイズ可、`test.yml` または `ci.yml` を推奨 - 2
ステップ2: トリガー条件の構成
いつテストをトリガーするか定義:
```yaml
on:
push:
branches: [main]
pull_request:
```
• main ブランチへのプッシュでトリガー
• PR の作成または更新でトリガー - 3
ステップ3: Matrix マトリックスの定義
バージョン、プラットフォーム、テストスイートを構成:
```yaml
strategy:
fail-fast: false
max-parallel: 6
matrix:
node-version: [16, 18, 20]
test-suite: [unit, integration]
os: [ubuntu-latest, windows-latest]
exclude:
- node-version: 16
os: windows-latest
```
• fail-fast: false で完全なテスト結果を収集
• max-parallel: 6 で並列上限を制御
• exclude で無効な組み合わせを除外 - 4
ステップ4: テストステップの構成
具体的なテスト実行ステップを定義:
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:${{ matrix.test-suite }}
```
• cache: 'npm' で依存キャッシュを有効化
• matrix 変数で動的にバージョンを構成 - 5
ステップ5: コミットして検証
コードをプッシュしてテストをトリガー:
• main ブランチへコミット、または PR を作成
• GitHub Actions ページで並列タスクの実行状況を確認
• 各バージョンのテスト結果が正常か確認
FAQ
Matrix ビルドはより多くの Runner 分数を消費しますか?
fail-fast は true と false のどちらに設定すべきですか?
• PR チェック:true を推奨(高速失敗、即フィードバック)
• Nightly テスト:false を推奨(完全なレポートを収集)
• 実験的バージョン:false を推奨(全体判断への影響を回避)
デフォルトは true で、ほとんどの PR 場面では十分です。
exclude と include はどちらが先に実行されますか?
max-parallel はいくつに設定するのが適切ですか?
• 小規模マトリックス(<10 組み合わせ):設定不要、デフォルト値を使用
• 中規模マトリックス(10-20 組み合わせ):6-8 に設定
• 大規模マトリックス(>20 組み合わせ):4-6 に設定
小さすぎると総待機時間が延び、大きすぎると一度に Runner リソースを使い果たす可能性があります。
Matrix でキャッシュを使って高速化するには?
Matrix はどの変数タイプをサポートしていますか?
• 配列:`[18, 20, 22]`
• オブジェクト配列:`[{name: 'a', value: 1}, {name: 'b', value: 2}]`
• 文字列:include で追加する必要あり
可読性の点から、配列とオブジェクト配列の使用を推奨します。
5分で読めます · 公開日: 2026年4月8日 · 更新日: 2026年6月8日
GitHub Actions 完全ガイド
このページはシリーズの最初の記事です。次の記事へ進むか、シリーズ全体ページで全体像を確認できます。
関連記事
GitHub Actions入門:YAMLワークフローの基礎とトリガー設定
GitHub Actions入門:YAMLワークフローの基礎とトリガー設定
GitHub Actions セルフホスト Runner:プライベート環境デプロイ完全ガイド
GitHub Actions セルフホスト Runner:プライベート環境デプロイ完全ガイド
GitHub Actions キャッシュ戦略:CI/CD パイプラインを 5 倍高速化
コメント
GitHubアカウントでログインしてコメントできます