切换语言
切换主题

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

上周项目上线,下午还没过就收到用户反馈:Node 16 环境下页面直接白屏。我当时脑子里嗡一下。

排查了整整两小时。日志翻了一遍,代码对比了三轮,最后发现是某个 API 在 Node 16 下处理 JSON.stringify() 的行为跟 Node 20 不一样——旧版本遇到循环引用直接抛错,新版本会静默处理。CI 流水线只测了 Node 20,这个兼容性问题压根没被拦截。

事后复盘的时候我就在想:如果当初配置了多版本并行测试,这些问题上线前就能被发现。也就是那次之后,我开始认真研究 GitHub Actions 的 Matrix 矩阵构建——这个词听起来挺唬人,说白了就是把一个任务自动拆成多份,同时跑在不同版本、不同平台上。

这篇文章我会把 Matrix 从基础到进阶完整讲一遍。包括基本语法、exclude/include 过滤组合、fail-fast 策略怎么选、max-parallel 资源控制。最后给你一个可以直接复制使用的完整 Node.js 多版本测试模板。大概 10 分钟看完,回去就能把 CI 改成多版本并行跑。

Matrix 基础 — 5 分钟上手

Matrix 一句话就能理解:你写一个 job,GitHub Actions 自动帮你展开成多个并行任务。

举个例子。你定义三个 Node.js 版本 [18, 20, 22],Matrix 就会创建三个独立的测试任务,分别在 Node 18、Node 20、Node 22 环境下跑。这三个任务同时启动,同时执行,互不干扰。

最简配置是这样的:

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 会遍历这个数组,每次取一个值赋给 matrix.node-version,然后创建一个对应的 job 实例。

${{ matrix.node-version }} 这个语法就是在引用当前的值。第一次执行时它是 18,第二次是 20,第三次是 22。

我第一次用的时候有个疑问:这三个任务是串行还是并行?答案是默认并行。你推一次代码,GitHub 会同时启动三个 Runner,三个任务一起跑。实测下来,之前串行测三个版本要 15 分钟,改成 Matrix 后只要 5 分钟——因为三个任务同时在跑,时间就压缩到最慢那个任务的耗时。

不过有一点要注意:并行跑意味着消耗更多 Runner 分钟数。三个任务就是三倍的时间消耗。如果你用的是免费额度(每月 2000 分钟),大矩阵可能会很快把额度吃光。这个后面讲 max-parallel 的时候会细说。

exclude/include 过滤 — 细控制测试矩阵

当你开始组合多个维度的时候,Matrix 的组合数会指数级增长。

比如三个 Node 版本 [16, 18, 20],三个操作系统 [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 这两个组合。原本 9 个任务变成 7 个,直接省了 22% 的 Runner 分钟。

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 这个任务里存在。你可以在后续步骤里判断这个变量,比如实验版本失败了不阻止整个 workflow:

- name: Run tests
  run: npm test
  continue-on-error: ${{ matrix.experimental == true }}

我踩过一个坑:excludeinclude 的优先级。GitHub Actions 先执行 include 添加组合,再执行 exclude 删除。所以如果你 include 了一个组合,又在 exclude 里排除了它,这个组合是不会出现的。顺序搞反的话,结果可能跟你预想的不一样。

总结下这两个的用法:

  • exclude:删掉没必要的组合,省钱省时间
  • include:补充特殊组合,还能加额外变量做差异化处理

fail-fast 与 max-parallel — 并行策略调优

Matrix 默认有一个行为你可能没注意到:fail-fast: true

啥意思?就是矩阵里任何一个任务失败了,GitHub Actions 会立即取消其他还在跑的任务。比如你 10 个任务并行,第 3 个任务跑了一分钟失败了,剩下 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不限快速反馈,一个失败就停,省时间
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 多版本并行测试流水线

前面的内容都是碎片知识点。这一章给你一个完整的、可以直接复制用的配置模板。

这个模板包含:

  • 三种 Node 版本(16、18、20)
  • 两套测试(unit 和 integration)
  • 两套操作系统(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 的 hash 缓存 npm 依赖,第二次跑的时候直接复用,不用重新下载。实测下来,这个缓存能减少 50% 以上的依赖安装时间。

fail-fast: false:这里故意设置成 false,因为多版本测试的目的就是发现所有问题。一个版本挂了,其他版本还要继续跑完。

npm run test:${{ matrix.test-suite }}:假设你的 package.json 里定义了 test:unittest:integration 两个命令,Matrix 会分别调用。

这个配置总共会产生多少个任务?

3 个版本 x 2 个测试 x 2 个操作系统 = 12 个任务,减去排除的 Node 16 + Windows(2 个测试套件),剩下 10 个任务。

实测数据:我用这个配置跑了几个项目,配合缓存后,CI 时间从之前串行的 25 分钟压缩到 8 分钟左右。时间主要省在并行执行和依赖缓存两个地方。

如果你项目更大,组合更多,可以考虑:

  • 增加 max-parallel 上限(比如 8 或 10)
  • 把 e2e 测试单独拆一个 job,避免拖慢整体
  • continue-on-error 处理实验版本的失败

把这个模板复制到你的 .github/workflows/test.yml,根据实际情况调整版本号和测试套件名,应该就能跑起来了。

结论

说了这么多,总结下核心要点:

Matrix 本质上就是把一个 job 自动展开成多个并行任务。配置简单,效果直接——一次推送,三个版本同时跑,CI 时间直接压缩到最慢那个任务的耗时。

exclude/include 是精细控制的手段。组合太多的时候用 exclude 删掉没必要的任务,能直接省掉 20%+ 的 Runner 分钟。include 则是补充特殊组合,还能加额外变量做差异化处理。

fail-fast 默认 true,一个失败就停其他任务。PR 检查用默认就好,Nightly 测试改成 false 收完整报告。max-parallel 控制并发上限,大矩阵才需要关注这个设置。

缓存是标配。setup-node 自带的 cache 功能,配置一行代码就能省掉一半依赖安装时间。

下一步建议:把上面的完整模板复制到你的项目 .github/workflows/ 目录下,先跑 Node.js 三版本测试。确认没问题之后,再逐步扩展到多平台和多测试套件。如果遇到缓存配置的问题,可以看看系列文章《GitHub Actions 缓存策略:加速 CI/CD 流水线 5 倍》,里面有更细的技巧。

多版本测试这事儿,越早配置越好。别等到上线出问题才后悔——那个白屏 bug,我现在想起来还头疼。

配置 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 页面查看并行任务执行情况
    • 检查各版本测试结果是否正常

常见问题

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 的 hash 判断是否命中缓存。实测可减少 50% 以上的依赖安装时间。如果使用 pnpm 或 yarn,改成 `cache: 'pnpm'` 或 `cache: 'yarn'`。
Matrix 支持哪些变量类型?
支持三种类型:

• 数组:`[18, 20, 22]`
• 对象数组:`[{name: 'a', value: 1}, {name: 'b', value: 2}]`
• 字符串:需要用 include 添加

推荐使用数组和对象数组,可读性更好。

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

评论

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

相关文章