切换语言
切换主题

GitHub Actions Matrix 矩阵构建:多平台多版本并行测试实战指南

去年有个开源项目找到我,说他们的 CI 配置文件已经膨胀到 800 多行了。我打开一看,密密麻麻全是重复的 job 定义——Node 16 在 Ubuntu 上跑、Node 16 在 Windows 上跑、Node 16 在 macOS 上跑……然后 Node 18 再来一遍,Node 20 又来一遍。改个测试命令?得改 12 处。加个新版本?复制粘贴十几分钟。

那一刻我突然意识到,很多人还在用手写的方式维护多版本多平台的测试。

GitHub Actions 的 Matrix 功能,说白了就是帮你自动展开这些重复配置。你定义几个操作系统、几个运行时版本,它自动帮你把所有组合跑一遍。听起来挺简单,但真正用起来,坑还不少——组合数爆炸导致账单爆炸、一个任务失败全链路挂掉、想排除特定组合不知道怎么写……这些问题我都踩过。

这篇文章会从最基础的 Matrix 语法讲起,一步步带你掌握 exclude/include 精准控制、fail-fast 策略选择、max-parallel 并发限制、以及动态 Matrix 生成这些高级技巧。最后给你 5 个可以直接复制到项目里的生产级 workflow 模板,覆盖从个人项目到企业级应用的各种场景。

2. Matrix 核心概念:一键展开多任务

Matrix 的核心逻辑很简单:你定义几个维度,GitHub Actions 自动帮你做笛卡尔积展开。

比如说,你的项目需要在 Ubuntu、Windows、macOS 三个系统上测试,同时要兼容 Node.js 18、20、22 三个版本。传统写法你得手写 9 个 job,每个 job 都要重复定义运行环境、安装步骤、测试命令。用 Matrix 的话,只需要这样:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]

runs-on: ${{ matrix.os }}

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node }}
  - run: npm test

这 10 行配置,GitHub Actions 会自动展开成 3 x 3 = 9 个并行任务。每个任务都会拿到不同的 matrix.osmatrix.node 值,跑完所有组合。

我之前那个 800 行配置文件的项目,用 Matrix 重构之后缩减到了 120 行左右——减少了 60%+ 的代码量。维护成本也下来了,加个新版本只需要在数组里加一个数字,不用再复制粘贴一堆 job 定义。

Matrix 能帮你做到的事

  • 一键生成多平台多版本的测试组合
  • 自动展开所有配置,避免手写重复代码
  • 通过 exclude 排除已知问题的组合
  • 通过 include 添加特殊配置的用例
  • 控制并发数量,平衡速度和成本

但它解决不了的问题

  • 测试本身写得烂,Matrix 也救不了
  • 组合太多导致账单爆炸——这个得靠你自己控制维度数量
  • 依赖安装慢——得配合缓存策略

说实话,Matrix 本身不难理解,难的是怎么用它解决实际工程问题。接下来我们从基础语法开始,一步步拆解。

3. 基础语法:os x version 组合原理

Matrix 的组合规则其实就是数学里的笛卡尔积。你定义的每个维度,都会和其他维度做全排列组合。

一个维度,N 个值 -> N 个任务

两个维度,M x N 个值 -> M x N 个任务

三个维度,A x B x C 个值 -> A x B x C 个任务

来个具体的例子。假设你有一个 Python 项目,需要在 Linux 和 Windows 上测试 Python 3.9、3.10、3.11、3.12 四个版本,同时要测试 PostgreSQL 和 MySQL 两种数据库:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.9', '3.10', '3.11', '3.12']
        database: [postgresql, mysql]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Setup ${{ matrix.database }}
        run: |
          # 启动对应的数据库服务
          if [ "${{ matrix.database }}" = "postgresql" ]; then
            docker run -d -p 5432:5432 postgres
          else
            docker run -d -p 3306:3306 mysql
          fi
        shell: bash
      - run: pip install -r requirements.txt
      - run: pytest

这个配置会生成 2 x 4 x 2 = 16 个任务。每个任务都是独立的运行环境,互不干扰。

访问 Matrix 变量的方式

  • ${{ matrix.os }} — 获取当前任务的操作系统
  • ${{ matrix.python-version }} — 获取当前任务的 Python 版本
  • ${{ matrix.database }} — 获取当前任务的数据库类型

这些变量可以在 runs-onstepsenv 等地方使用,帮你动态调整每个任务的行为。

一个常见的坑:很多人以为 Matrix 会自动处理依赖安装的问题。实际上每个任务都是独立的环境,依赖安装会重复执行。这就导致了一个问题——如果你的依赖安装需要 2 分钟,16 个任务就是 32 分钟的等待时间(假设它们串行执行)。

解决方法有两个:

  1. 使用缓存 — 缓存 pipnpm 的依赖目录,跳过重复下载
  2. 减少组合数 — 通过 exclude 排除不必要的测试组合

缓存策略我之前在 [GitHub Actions 缓存策略:加速 CI/CD 流水线 5 倍] 这篇文章里详细讲过,这里就不展开了。接下来我们重点讲讲怎么用 exclude/include 精准控制测试组合。

4. exclude/include:精准控制测试组合

Matrix 默认会把所有维度做全排列组合,但实际项目中,你经常会遇到「某些组合不需要测试」或者「某些组合需要特殊处理」的情况。

4.1 exclude:排除无效组合

我之前维护一个 Python 项目时遇到过这个问题:Windows + Python 3.9 的组合总是失败,因为某个依赖库在 Windows 上的 3.9 版本有兼容性问题。但这个项目主要面向 Linux 服务器部署,Windows 只是顺带支持,不值得花时间去修这个特定的 bug。

这时候 exclude 就派上用场了:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    python-version: ['3.9', '3.10', '3.11', '3.12']
    exclude:
      - os: windows-latest
        python-version: '3.9'
      - os: macos-latest
        python-version: '3.9'

这个配置会排除 Windows 和 macOS 上的 Python 3.9 测试。原本 3 x 4 = 12 个任务,排除 2 个后变成 10 个任务。

exclude 的典型使用场景

  1. 已知兼容性问题 — 某个版本在特定系统上跑不起来
  2. 资源限制 — 自托管 runner 数量有限,需要减少组合数
  3. 边缘场景 — 某些组合用户几乎不会用,不值得花 CI 时间

4.2 include:添加特殊配置

include 的作用相反——它帮你添加额外的测试组合,或者给特定组合补充额外变量。

比如说,你想在 Python 3.12 的测试中额外启用覆盖率报告,其他版本则不需要:

strategy:
  matrix:
    python-version: ['3.10', '3.11', '3.12']
    include:
      - python-version: '3.12'
        coverage: true

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-python@v5
    with:
      python-version: ${{ matrix.python-version }}
  - run: pip install -r requirements.txt
  - name: Run tests
    run: |
      if [ "${{ matrix.coverage }}" = "true" ]; then
        pytest --cov=src --cov-report=xml
      else
        pytest
      fi
    shell: bash

这里 include 做了两件事:

  1. 添加一个新组合 — Python 3.12 的测试
  2. 给这个组合补充额外变量coverage: true

include 的典型使用场景

  1. 实验性版本测试 — 比如 Python 3.13 预览版,只在某个系统上测试
  2. 特殊配置 — 某些组合需要额外的环境变量或参数
  3. 边缘场景覆盖 — 低频使用的组合,单独添加而不是通过全排列生成

4.3 exclude 和 include 可以一起用

实际项目中,你经常需要同时使用 exclude 和 include。比如:排除所有 Python 3.9 的组合,但单独添加一个 Ubuntu + Python 3.9 的最小化测试:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    python-version: ['3.9', '3.10', '3.11', '3.12']
    exclude:
      - python-version: '3.9'
    include:
      - os: ubuntu-latest
        python-version: '3.9'
        minimal: true

这个配置的执行顺序是:先生成所有组合 -> 应用 exclude 排除 -> 应用 include 添加。最终结果:Ubuntu 上跑 4 个版本,Windows 上跑 3 个版本(排除 3.9)。

5. fail-fast 策略:快速失败 vs 完整调试

Matrix 任务默认有一个行为:只要有一个任务失败,其他正在运行的任务会被取消。这个行为叫 fail-fast,默认是开启的。

strategy:
  fail-fast: true  # 默认值,可以省略
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]

5.1 什么时候用 fail-fast: true(默认)

PR 测试场景 — 开发者提了一个 PR,你想快速知道测试有没有问题。一个任务失败了,其他任务大概率也会失败(同样的代码问题),没必要继续浪费时间。

成本敏感场景 — GitHub Actions 的免费额度有限,自托管 runner 的资源也有限。快速失败能帮你省下不少钱。

我个人的习惯是:PR 测试用 fail-fast: true,main 分支的完整测试用 fail-fast: false

5.2 什么时候用 fail-fast: false

调试阶段 — 你的 Matrix 任务经常失败,但你想知道具体是哪些组合失败、失败的原因各是什么。如果 fail-fast: true,你只能看到第一个失败的任务,其他任务被取消了。

兼容性测试 — 你在做多版本多平台的兼容性测试,想知道每个组合的测试结果。即使某个版本有问题,也不影响你获取其他版本的信息。

完整报告场景 — 你需要在 CI 结束后生成一份完整的测试报告,包含所有组合的通过/失败状态。

strategy:
  fail-fast: false  # 让所有任务跑完
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]

5.3 一个真实的案例

去年我帮一个项目排查 CI 问题,他们的测试在 Ubuntu + Node 18 上总是失败,但其他组合都通过。因为默认开启了 fail-fast,每次只能看到 Ubuntu + Node 18 失败,然后就取消了其他任务。后来他们想知道 Windows + Node 18 是不是也有问题,就改成了 fail-fast: false,结果发现 Windows 上没问题,只有 Ubuntu 有问题——最后定位到是文件路径大小写的兼容性问题。

所以我的建议是:开发调试阶段用 fail-fast: false,看清楚所有问题;稳定运行阶段用 fail-fast: true,省钱省时间。

6. max-parallel:并发控制与成本优化

Matrix 任务默认是并行执行的,GitHub 会尽可能多地同时启动任务。对于公开仓库,GitHub 托管 runner 的并发限制是 20 个;对于私有仓库,免费账户的并发限制是 2 个。

但有时候你需要手动控制并发数,这就是 max-parallel 的作用。

6.1 什么时候需要限制并发

自托管 runner 资源有限 — 你的 runner 服务器只有 4 核 8G,同时跑 8 个任务会把机器拖垮。

外部服务限流 — 你的测试需要调用第三方 API,对方有 QPS 限制,并发太高会被封。

数据库连接池限制 — 你的测试需要连接数据库,连接池只有 10 个连接,任务太多会耗尽连接。

strategy:
  max-parallel: 4  # 最多同时跑 4 个任务
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]

这个配置会生成 6 个任务,但最多只有 4 个同时运行。跑完一个,再启动下一个。

6.2 成本计算的一个例子

假设你有一个项目,每次 CI 运行需要测试 3 个系统 x 4 个 Node 版本 = 12 个任务。每个任务平均运行 10 分钟。

不限制并发(假设 runner 足够)

  • 12 个任务同时运行
  • 总耗时约 10 分钟
  • 总计算时间 = 12 x 10 = 120 分钟

限制 max-parallel: 4

  • 12 个任务分 3 批运行
  • 总耗时约 30 分钟
  • 总计算时间 = 12 x 10 = 120 分钟(不变)

看到了吗?max-parallel 不会减少总计算时间,只会延长总耗时。那为什么还要用?

因为 并发峰值成本资源限制

GitHub Actions 的计费单位是「分钟数」,但如果你用自托管 runner,或者你的云服务商按峰值计费,并发控制就很重要。比如说,12 个任务同时跑,你的数据库需要 12 个连接;分批跑,只需要 4 个连接。

我的实践经验

  • 公开仓库,GitHub 托管 runner:不用管 max-parallel,让它自己调度
  • 私有仓库,免费额度:限制 max-parallel: 2,慢慢跑,不超额度
  • 自托管 runner:根据服务器配置限制 max-parallel,4 核建议 2-4 个并发

7. 动态 matrix:fromJSON 进阶技术

前面讲的 Matrix 都是静态配置——你在 YAML 里写死要测试的版本。但有些场景下,你需要根据代码变化动态生成测试组合。

比如说,你有一个 monorepo,里面有多个服务,每个服务都有自己的测试配置。你想做到:只测试这次提交涉及到的服务,而不是所有服务都跑一遍。

7.1 两步 workflow 实现动态 matrix

GitHub Actions 没有直接支持「动态 matrix」的语法,但你可以用一个 job 生成 matrix 配置,然后传给另一个 job 使用。关键是 fromJSON() 函数。

jobs:
  # 第一步:检测变化的服务,生成 matrix 配置
  detect:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # 需要获取上一次提交

      - name: Detect changed services
        id: set-matrix
        run: |
          # 获取这次提交修改的文件
          CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)

          # 判断哪些服务被修改了
          SERVICES="[]"
          if echo "$CHANGED_FILES" | grep -q "services/auth/"; then
            SERVICES=$(echo $SERVICES | jq '. + ["auth"]')
          fi
          if echo "$CHANGED_FILES" | grep -q "services/api/"; then
            SERVICES=$(echo $SERVICES | jq '. + ["api"]')
          fi
          if echo "$CHANGED_FILES" | grep -q "services/web/"; then
            SERVICES=$(echo $SERVICES | jq '. + ["web"]')
          fi

          # 如果没有服务被修改,默认测试所有服务
          if [ "$SERVICES" = "[]" ]; then
            SERVICES='["auth", "api", "web"]'
          fi

          echo "matrix={\"service\":$(echo $SERVICES)}" >> $GITHUB_OUTPUT

  # 第二步:使用动态生成的 matrix
  test:
    needs: detect
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}

    steps:
      - uses: actions/checkout@v4
      - name: Test ${{ matrix.service }}
        run: |
          cd services/${{ matrix.service }}
          npm install
          npm test

这个 workflow 的工作原理:

  1. detect job 检测这次提交修改了哪些目录
  2. 根据修改的目录,动态生成一个 JSON 格式的 matrix 配置
  3. test job 使用 fromJSON() 解析这个配置,生成对应的任务

7.2 动态 matrix 的典型应用场景

Monorepo 场景 — 只测试变化的服务,节省 CI 时间

按需部署 — 检测 Dockerfile 变化,只构建和部署有更新的镜像

矩阵测试优化 — 根据文件类型决定测试组合(比如只有 package.json 变化才跑完整的多版本测试)

我踩过的坑

  1. fromJSON() 只能用在 strategy.matrix 的值上,不能用在其他地方
  2. 生成的 JSON 必须是合法的 matrix 格式,比如 {"service": ["auth", "api"]}
  3. 如果生成的 matrix 为空,workflow 会直接报错——记得加默认值处理

8. 实战模板库:5 个生产级 workflow 示例

接下来给你 5 个可以直接复制使用的 workflow 模板,覆盖从个人项目到企业级应用的各种场景。

8.1 模板 1:Node.js 多版本测试(基础)

适用场景:Node.js 库或应用,需要兼容多个 Node 版本

name: Node.js CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: [18, 20, 22, 23]

    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'

      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

      - name: Upload coverage
        if: matrix.node-version == 22
        uses: codecov/codecov-action@v4

关键点

  • 使用 npm ci 而不是 npm install,确保依赖版本锁定
  • 只在 Node 22 上传覆盖率报告,避免重复上传
  • cache: 'npm' 启用 npm 缓存,加速依赖安装

8.2 模板 2:Python 多平台多版本测试(中级)

适用场景:Python 项目,需要跨平台跨版本测试

name: Python CI

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

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ['3.10', '3.11', '3.12']
        exclude:
          - os: windows-latest
            python-version: '3.10'  # 已知兼容性问题

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: pytest -v

      - name: Lint check
        run: |
          pip install ruff
          ruff check .

关键点

  • 使用 exclude 排除已知问题的组合
  • cache: 'pip' 加速 pip 依赖安装
  • 集成代码检查工具 ruff

8.3 模板 3:exclude/include 精准控制(高级)

适用场景:需要精细控制测试组合,排除特定组合,添加特殊测试

name: Advanced Matrix

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.10', '3.11', '3.12']
        exclude:
          # 排除 Windows + Python 3.10(已知问题)
          - os: windows-latest
            python-version: '3.10'
        include:
          # 添加一个实验性测试:Ubuntu + Python 3.13 预览版
          - os: ubuntu-latest
            python-version: '3.13-dev'
            experimental: true
          # 给 Python 3.12 添加覆盖率报告
          - python-version: '3.12'
            coverage: true

    continue-on-error: ${{ matrix.experimental == true }}

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - run: pip install -r requirements.txt

      - name: Run tests
        run: |
          if [ "${{ matrix.coverage }}" = "true" ]; then
            pytest --cov=src --cov-report=xml
          else
            pytest
          fi
        shell: bash

关键点

  • continue-on-error 让实验性测试失败不影响整体状态
  • include 可以同时添加新组合和补充变量
  • 使用 shell: bash 确保 Windows 和 Linux 命令一致

8.4 模板 4:动态 matrix + caching(进阶)

适用场景:Monorepo,根据文件变化动态生成测试组合

name: Dynamic Matrix CI

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

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Detect changed packages
        id: set-matrix
        run: |
          CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)

          PACKAGES="[]"
          for dir in packages/*/; do
            pkg=$(basename $dir)
            if echo "$CHANGED_FILES" | grep -q "^packages/$pkg/"; then
              PACKAGES=$(echo $PACKAGES | jq ". + [\"$pkg\"]")
            fi
          done

          # 无变化时测试所有包
          if [ "$PACKAGES" = "[]" ]; then
            PACKAGES='["core", "utils", "cli"]'
          fi

          echo "matrix={\"package\":$(echo $PACKAGES)}" >> $GITHUB_OUTPUT

  test:
    needs: detect
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build --if-present

      - name: Test ${{ matrix.package }}
        run: |
          cd packages/${{ matrix.package }}
          npm test

关键点

  • fetch-depth: 2 获取上一次提交用于比较
  • 使用 jq 命令操作 JSON 数组
  • 无变化时提供默认值,避免空 matrix 报错

8.5 模板 5:自托管 runner + max-parallel(企业级)

适用场景:自托管 runner,需要严格控制并发和资源

name: Enterprise CI

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

jobs:
  test:
    runs-on: [self-hosted, linux, x64]
    strategy:
      fail-fast: true
      max-parallel: 4
      matrix:
        java-version: [11, 17, 21]
        database: [postgresql, mysql]

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      mysql:
        image: mysql:8
        env:
          MYSQL_ROOT_PASSWORD: root
        ports:
          - 3306:3306
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java ${{ matrix.java-version }}
        uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java-version }}
          distribution: 'temurin'
          cache: 'maven'

      - name: Run tests with ${{ matrix.database }}
        env:
          DB_TYPE: ${{ matrix.database }}
          DB_HOST: localhost
          DB_PORT: ${{ matrix.database == 'postgresql' && 5432 || 3306 }}
        run: mvn test -Dspring.profiles.active=${{ matrix.database }}

      - name: Archive test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results-${{ matrix.java-version }}-${{ matrix.database }}
          path: target/surefire-reports

关键点

  • runs-on: [self-hosted, linux, x64] 指定自托管 runner 标签
  • max-parallel: 4 限制并发,保护 runner 服务器
  • 使用 services 启动测试数据库容器
  • if: always() 确保测试结果始终上传,即使测试失败

9. 常见陷阱与最佳实践

用 Matrix 这么久,我踩过不少坑。总结几个最常见的。

9.1 陷阱 1:组合数爆炸

我见过最夸张的配置:4 个操作系统 x 5 个运行时版本 x 3 个数据库 x 2 个缓存方案 = 120 个任务。每次 CI 跑完要 45 分钟,账单直接爆了。

解决方法

  • 只在 main 分支跑完整 Matrix,PR 只跑关键组合
  • 使用 exclude 排除边缘场景
  • 评估每个维度的必要性——真的需要测 4 个操作系统吗?
# PR 只测关键组合
on:
  pull_request:
    branches: [main]

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest]  # PR 只测 Ubuntu
        node: [20]            # PR 只测 Node 20

9.2 陷阱 2:fail-fast 阻碍调试

默认 fail-fast: true 在调试阶段很烦——一个任务失败,其他任务全被取消,你看不到完整的失败报告。

解决方法:调试阶段手动改成 fail-fast: false,调试完再改回来。

或者用环境变量控制:

strategy:
  fail-fast: ${{ github.event_name == 'pull_request' }}

9.3 陷阱 3:缺少 caching

Matrix 会重复执行同一个 job 多次,如果每次都重新安装依赖,时间成本很高。我有次测试 12 个组合,每个安装依赖要 2 分钟,光安装就要 24 分钟。

解决方法:使用 GitHub Actions 缓存,或者用专门的缓存 action。

- uses: actions/setup-node@v4
  with:
    node-version: ${{ matrix.node-version }}
    cache: 'npm'  # 关键:启用 npm 缓存

9.4 最佳实践总结

实践 1:PR 小型 Matrix + main 完整 Matrix

jobs:
  test:
    strategy:
      matrix:
        # PR 只测关键组合
        ${{ github.event_name == 'pull_request' && fromJSON('{"os":["ubuntu-latest"],"node":[20]}') || fromJSON('{"os":["ubuntu-latest","windows-latest","macos-latest"],"node":[18,20,22]}') }}

实践 2:exclude 排除已知问题组合

遇到特定组合的兼容性问题,先用 exclude 跳过,记个 TODO,后面再修。

实践 3:结合 caching 减少安装时间

每个 job 的依赖安装是最耗时的环节之一,用好缓存能把时间从分钟级降到秒级。

实践 4:给组合添加有意义的 name

默认的任务名称是 test (ubuntu-latest, 20) 这种格式,你可以用 name 自定义:

jobs:
  test:
    name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]

这样在 GitHub Actions 界面更容易识别每个任务。

10. 结论

GitHub Actions Matrix 是处理多平台多版本测试的利器。核心就几件事:定义维度、控制组合、管理并发、动态生成。

我见过太多项目还在用手写的方式维护重复的 CI 配置,改一个命令要改十几处。Matrix 能帮你把几百行的配置压缩到几十行,维护成本直接下来。

关键点回顾

  1. 基础语法matrix.osmatrix.node 做笛卡尔积展开
  2. 精准控制exclude 排除无效组合,include 添加特殊配置
  3. 策略选择fail-fast 根据场景选择,调试用 false,生产用 true
  4. 并发限制max-parallel 保护自托管 runner,控制成本
  5. 动态生成fromJSON() 实现按需测试,节省 CI 资源

文章里的 5 个模板可以直接复制到你的项目里用,从最简单的 Node.js 多版本测试到企业级的自托管 runner 配置都有覆盖。

如果你刚开始用 Matrix,建议从模板 1 开始,跑通之后再加 excludeinclude,最后再尝试动态生成。一步步来,别一上来就整复杂的。

有问题可以在评论区留言,或者去 GitHub Actions 官方文档看看更多细节。如果你有 Matrix 使用上的心得,也欢迎分享。

配置 GitHub Actions Matrix 多平台多版本测试

从零开始配置 Matrix 策略,实现跨平台跨版本的自动化测试

⏱️ 预计耗时: 30 分钟

  1. 1

    步骤1: 定义 Matrix 维度

    在 workflow 的 job 中添加 strategy.matrix 配置:

    ```yaml
    strategy:
    matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]
    ```

    这会生成 2 x 3 = 6 个并行任务。
  2. 2

    步骤2: 使用 Matrix 变量

    在 runs-on 和 steps 中引用 matrix 变量:

    ```yaml
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/setup-node@v4
    with:
    node-version: ${{ matrix.node }}
    ```

    每个任务会自动获取对应的 os 和 node 值。
  3. 3

    步骤3: 排除特定组合(可选)

    使用 exclude 排除已知问题的组合:

    ```yaml
    strategy:
    matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]
    exclude:
    - os: windows-latest
    node: 18
    ```

    排除 Windows + Node 18 组合,最终生成 5 个任务。
  4. 4

    步骤4: 配置失败策略

    根据场景选择 fail-fast 策略:

    - PR 测试:fail-fast: true(快速失败,省钱)
    - 调试阶段:fail-fast: false(看全所有失败)
    - main 分支:fail-fast: false(完整报告)

    ```yaml
    strategy:
    fail-fast: false
    matrix:
    # ...
    ```
  5. 5

    步骤5: 限制并发数(可选)

    自托管 runner 或资源有限时,限制并发:

    ```yaml
    strategy:
    max-parallel: 4
    matrix:
    # ...
    ```

    最多同时运行 4 个任务,避免 runner 过载。

常见问题

Matrix 组合数有上限吗?
GitHub 对 Matrix 任务数有软限制。公开仓库最多 256 个任务,私有仓库根据套餐不同。但实际使用中,建议组合数控制在 20 以内,避免 CI 时间过长和账单爆炸。4 操作系统 x 5 版本 x 3 数据库 = 60 任务已经很多了。
fail-fast 默认值是什么?
fail-fast 默认值是 true。一个任务失败后,其他正在运行的任务会被取消。调试阶段建议设为 false,看清楚所有失败原因;生产环境用默认值,快速失败节省成本。
exclude 和 include 的执行顺序是什么?
执行顺序:先生成所有组合 -> 应用 exclude 排除 -> 应用 include 添加。所以你可以先排除所有 Python 3.9 的组合,再用 include 单独添加 Ubuntu + Python 3.9 的最小化测试。
动态 matrix 的 fromJSON() 能用在哪些地方?
fromJSON() 只能用在 strategy.matrix 的值上,不能用在其他 YAML 字段。生成的 JSON 必须是合法的 matrix 格式,如 {"os": ["ubuntu", "windows"]}。如果生成的 matrix 为空,workflow 会报错,记得加默认值处理。
max-parallel 会减少总计算时间吗?
不会。max-parallel 只控制同时运行的任务数,不减少总计算时间。12 个任务 x 10 分钟 = 120 分钟计算时间,无论并发数是多少。但限制并发可以:1)降低峰值资源占用;2)避免数据库连接池耗尽;3)控制自托管 runner 负载。
Matrix 任务之间能共享缓存吗?
能。GitHub Actions 的缓存是仓库级别的,所有 job 都能访问。在 setup-node 或 setup-python 中启用 cache 参数即可自动缓存依赖目录。建议在所有任务中启用缓存,第一个任务会创建缓存,后续任务直接使用。
如何给 Matrix 任务添加自定义名称?
使用 job 的 name 属性配合 matrix 变量:

• name: Test (${{ matrix.os }}, Node ${{ matrix.node }})

这样在 GitHub Actions 界面会显示友好的名称如 "Test (ubuntu-latest, Node 20)",方便识别每个任务。

18 分钟阅读 · 发布于: 2026年4月28日 · 修改于: 2026年4月28日

当前属于系列阅读 第 7 / 7 篇

GitHub Actions 完全指南

如果你是从搜索进入这篇文章,建议顺手补上上一篇或继续下一篇,这样更容易把同一主题读完整。

查看系列总览

相关文章

BetterLink

想持续收到这个主题的更新?

你可以直接关注作者更新、订阅 RSS,或者继续沿着系列入口往下读,避免下次又回到搜索结果重新找。

关注公众号

评论

使用 GitHub 账号登录后即可评论