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

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 }} という構文は、現在の値を参照しています。最初の実行では 18、2 回目は 20、3 回目は 22 です。

初めて使った時、疑問がありました。これらのタスクは直列か並列か?答えはデフォルトで並列です。コードを 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 つのオペレーティングシステム [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 という動作があります。気づいていないかもしれません。

これはどういう意味か?マトリックス内のいずれかのタスクが失敗すると、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 組み合わせはバッチで実行され、各バッチ 6 タスクです。メリットは Runner リソースが制御可能で、一度に枠を使い果たさないことです。デメリットは総時間が長くなることです。

シナリオに応じた選択を簡単な意思決定表にまとめました:

シナリオfail-fastmax-parallel理由
PR チェックtrue制限なし高速フィードバック、1 つ失敗したら停止、時間節約
Nightly テストfalse4-6完全な問題レポート収集、すべてのバグ特定
大規模マトリックス(>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 つのオペレーティングシステム(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 }}:オペレーティングシステムも動的で、各タスクはマトリックスの組み合わせに基づいて対応する 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 で追加する必要あり

配列とオブジェクト配列の使用を推奨、可読性が向上します。

6 min read · 公開日: 2026年4月8日 · 更新日: 2026年4月8日

コメント

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

関連記事