言語を切り替える
テーマを切り替える

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 }}

私が一度ハマったのは、excludeinclude の優先順位です。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-fastmax-parallel理由
PR チェックtrue制限なし高速フィードバック、1 つ失敗したら停止、時間節約
Nightly テストfalse4-6完全な問題レポートを収集し、すべての bug を特定
大規模マトリックス(>20 組み合わせ)true4リソース消費を抑え、枠の爆発を回避
実験的バージョンテスト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.jsontest:unittest: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

    ステップ1: Workflow ファイルの作成

    プロジェクトルートに `.github/workflows/test.yml` ファイルを作成:

    • ディレクトリ構造が正しいことを確認:`.github/workflows/`
    • ファイル名はカスタマイズ可、`test.yml` または `ci.yml` を推奨
  2. 2

    ステップ2: トリガー条件の構成

    いつテストをトリガーするか定義:

    ```yaml
    on:
    push:
    branches: [main]
    pull_request:
    ```

    • main ブランチへのプッシュでトリガー
    • PR の作成または更新でトリガー
  3. 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

    ステップ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

    ステップ5: コミットして検証

    コードをプッシュしてテストをトリガー:

    • main ブランチへコミット、または PR を作成
    • GitHub Actions ページで並列タスクの実行状況を確認
    • 各バージョンのテスト結果が正常か確認

FAQ

Matrix ビルドはより多くの Runner 分数を消費しますか?
はい。マトリックス展開後の各タスクは独立して課金されます。たとえば 3 バージョン x 2 プラットフォーム = 6 タスクで、各タスクが 5 分実行されると、合計 30 分消費します(5 分ではありません)。ただしタスクは並列実行されるため、総待機時間は大幅に短縮されます。
fail-fast は true と false のどちらに設定すべきですか?
場面によります:

• PR チェック:true を推奨(高速失敗、即フィードバック)
• Nightly テスト:false を推奨(完全なレポートを収集)
• 実験的バージョン:false を推奨(全体判断への影響を回避)

デフォルトは true で、ほとんどの PR 場面では十分です。
exclude と include はどちらが先に実行されますか?
先に include で組み合わせを追加し、その後 exclude で削除します。したがって include で追加した組み合わせを exclude で除外すると、その組み合わせは出現しません。すべての組み合わせを紙に書き出してから、どれを削除するか決めることをお勧めします。
max-parallel はいくつに設定するのが適切ですか?
参考の目安:

• 小規模マトリックス(<10 組み合わせ):設定不要、デフォルト値を使用
• 中規模マトリックス(10-20 組み合わせ):6-8 に設定
• 大規模マトリックス(>20 組み合わせ):4-6 に設定

小さすぎると総待機時間が延び、大きすぎると一度に Runner リソースを使い果たす可能性があります。
Matrix でキャッシュを使って高速化するには?
`actions/setup-node` に `cache: 'npm'` パラメータを追加するだけで、依存が自動的にキャッシュされます。package-lock.json のハッシュに基づいてキャッシュヒットを判定します。実測で依存インストール時間を 50%以上削減できます。pnpm や yarn を使う場合は、`cache: 'pnpm'` または `cache: 'yarn'` に変更してください。
Matrix はどの変数タイプをサポートしていますか?
3 つのタイプをサポートします:

• 配列:`[18, 20, 22]`
• オブジェクト配列:`[{name: 'a', value: 1}, {name: 'b', value: 2}]`
• 文字列:include で追加する必要あり

可読性の点から、配列とオブジェクト配列の使用を推奨します。

5分で読めます · 公開日: 2026年4月8日 · 更新日: 2026年6月8日

シリーズの読書導線 第 1 / 9 記事

GitHub Actions 完全ガイド

このページはシリーズの最初の記事です。次の記事へ進むか、シリーズ全体ページで全体像を確認できます。

シリーズ全体を見る

関連記事

コメント

GitHubアカウントでログインしてコメントできます