前置知识: GitHub

Actions 矩阵构建

6 min高级

GitHub Actions矩阵策略原理详解:从一次配置多环境测试的痛点出发,深入 strategy.matrix 语法、include/exclude、fail-fast 与动态矩阵。

0. 开始之前:一条”批量生产线”的故事

想象一家饮料厂。过去,工厂里每种口味(橙汁、苹果汁、葡萄汁)都要单独建一条生产线,工人重复做同样的事:灌装、贴标、装箱。三倍口味 = 三倍设备、三倍人力、三倍维护成本。

后来工厂引进了一条柔性生产线:一条线上有一个”配方参数面板”,工人在面板上切换 口味: [橙汁, 苹果汁, 葡萄汁]、包装: [瓶装, 罐装],机器就自动按每种组合各产一批。一套设备,同时覆盖 3×2=6 种产品。参数一变,全线跟着变,再也不用复制三套产线。

GitHub Actions 的矩阵构建(Matrix Strategy) 正是这条”柔性生产线”:你只写一个 job 定义,声明若干”配方参数”(操作系统、语言版本、浏览器……),GitHub 自动按所有组合生成多个并行的 job 实例。配置一份,处处运行。

1. 矩阵构建要解决的问题:先看清痛点

1.1 没有矩阵时的痛苦

假设你要在 Node.js 18、20、22 三个版本上跑测试。没有矩阵,你只能复制粘贴三份 job:

jobs:
  test-node20:                 # 第一份:Node 20
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with: { node-version: '20' }
      - run: npm test

  test-node22:                 # 第二份:Node 22(几乎一样的代码)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with: { node-version: '22' }
      - run: npm test

  test-node24:                 # 第三份:Node 24
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with: { node-version: '24' }
      - run: npm test

问题一目了然:

  • 改一处要改三处:想加 --reporter=json 要同步改三个 job,极易漏改。
  • 难以扩展:想再加 macOS/Windows 两个系统?组合变 3×2=6 份,复制粘贴灾难升级。
  • 可读性差:一个工作流文件几百行,一半是重复代码。

1.2 矩阵的解法

jobs:
  test:
    strategy:
      matrix:                  # 声明两个"维度"
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [20, 22, 24]   # 24 为最新 LTS;18 已于 2025 年停止维护
    runs-on: ${{ matrix.os }}              # 读取当前组合的 os
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}  # 读取当前组合的 node-version
      - run: npm test

一份定义,GitHub 自动生成 3 × 3 = 9 个并行 job,分别对应每种 (os, node-version) 组合。

2. 原理:一次配置,多种环境

2.1 笛卡尔积:矩阵的数学内核

矩阵的本质是笛卡尔积:把每个维度(变量)的所有取值两两组合。os: [A, B]、node-version: [X, Y, Z] 会生成 2×3=6 种组合:

{os: A, node-version: X}   {os: A, node-version: Y}   {os: A, node-version: Z}
{os: B, node-version: X}   {os: B, node-version: Y}   {os: B, node-version: Z}

GitHub 官方文档确认了这一行为:对矩阵中定义的每个变量组合,工作流都会运行一个 job。

2.2 matrix 上下文:每个 job 如何知道自己该用哪个值

每个矩阵 job 运行时,matrix 上下文里装着当前组合的完整取值。通过 ${{ matrix.<变量名> }} 引用:

- name: 打印当前组合
  run: echo "正在 ${{ matrix.os }} 上测试 Node ${{ matrix.node-version }}"

这就像生产线上的工人看一眼参数面板,就知道这一批该灌什么口味。

2.3 递进理解:从”复制”到”模板化”

阶段做法维护成本
复制粘贴每个环境写一个 job高,改一处要改 N 处
模板化一个 job + 矩阵变量低,改一处全线生效
动态矩阵矩阵由前置 job 用 JSON 生成极低,按需生成组合

3. 语法详解:strategy.matrix 全家桶

3.1 基础定义

jobs:
  example:
    strategy:
      matrix:                  # 矩阵定义
        version: [10, 12, 14]  # 维度一:版本
        os: [ubuntu-latest, windows-latest]  # 维度二:系统
    runs-on: ${{ matrix.os }}

3.2 include:给矩阵”加料”

include 有两个作用(官方文档):

  • 给已有组合追加额外变量:当 include 条目中的键值对与某个已有组合匹配时,只在该组合上追加新变量。
  • 新增一个独立组合:当 include 条目不匹配任何已有组合时,直接新增一个 job。
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node-version: [20, 22]
    include:
      # 场景一:匹配已有组合(ubuntu + node 20),追加 experimental 变量
      - os: ubuntu-latest
        node-version: 20
        experimental: true

      # 场景二:不匹配任何组合,新增一个独立 job(macos + node 22)
      - os: macos-latest
        node-version: 22
        experimental: true

      # 场景三:只写部分键,其余键取 include 条目中补充的默认值
      - node-version: 22
        os: ubuntu-latest
        flag: nightly
# 最终 job 数:基础 2×2=4 个 + include 新增 2 个 = 6 个

注意:include 条目匹配判断只针对已存在的组合(笛卡尔积 + 之前 include 新增的组合),这是新手最容易误解的点。

3.3 exclude:剔除不需要的组合

有些组合毫无意义(比如”Windows 上跑 Linux 专用脚本”)或已知不兼容,用 exclude 去掉:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    python: ['3.10', '3.11', '3.12']
    exclude:
      # 不在 Windows 上测 Python 3.10
      - os: windows-latest
        python: '3.10'
      # 不在 Ubuntu 上测 Python 3.10
      - os: ubuntu-latest
        python: '3.10'
# 结果:2×3=6 个组合,剔除 2 个,剩 4 个 job

3.4 执行顺序(重要)

GitHub 处理矩阵的完整顺序:

1. 先计算所有维度的笛卡尔积,得到基础组合集合
2. 应用 include:为匹配的组合追加变量,或新增组合
3. 应用 exclude:从当前集合中剔除匹配的组合

官方文档特别说明:exclude 会剔除 include 之前或之后产生的组合,建议把”先 include 再 exclude”作为习惯,逻辑更清晰。

3.5 fail-fast 与 max-parallel:失败策略与并发闸门

strategy:
  fail-fast: true     # 默认值:任一矩阵 job 失败,立即取消其余所有 job
  # fail-fast: false  # 所有组合都跑完,收集完整失败信息
  max-parallel: 4     # 最多同时运行 4 个矩阵 job
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: [18, 20, 22]
  • fail-fast: true:某个组合一旦失败就”叫停全场”,省运行分钟数,适合发现根本性问题时快速止损。
  • fail-fast: false:9 个 job 全部执行完毕,适合”想收集所有环境下的失败清单”的场景。CI 中常用 false。
  • max-parallel:限制同时运行的 job 数,防止目标系统(如共享数据库)被并发打爆。

4. 实战配置示例

4.1 多操作系统 + 多版本测试(最典型)

name: Test Matrix
on: [push, pull_request]
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false          # 收集所有环境的失败信息
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [20, 22, 24]   # 24 为最新 LTS;18 已于 2025 年停止维护
        exclude:                # Windows + Node 18 已知有问题,跳过
          - os: windows-latest
            node-version: 18
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

4.2 多语言多命令组合(include 充当”配置表”)

用 include 直接定义”每种语言的构建/测试命令”,一条 job 通吃多语言:

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - language: typescript
            build: npm run build
            test: npm test
          - language: python
            build: pip install -e .
            test: pytest
          - language: go
            build: go build ./...
            test: go test ./...
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Build
        run: ${{ matrix.build }}
      - name: Test
        run: ${{ matrix.test }}

4.3 浏览器测试分片(并发放大)

E2E 测试很慢,用矩阵把测试分片并行跑:

jobs:
  e2e:
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
        shard: [1/4, 2/4, 3/4, 4/4]     # 4 个分片
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}

4.4 容器多架构构建

jobs:
  docker:
    strategy:
      matrix:
        platform: [linux/amd64, linux/arm64]
    runs-on: ubuntu-latest
    steps:
      - uses: docker/setup-qemu-action@v3   # 模拟其他 CPU 架构
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          platforms: ${{ matrix.platform }}
          push: true
          tags: myapp:latest-${{ matrix.platform }}

5. 动态矩阵:让矩阵自己长出来

静态矩阵在组合数量固定时很好用;但组合数量不确定(比如 monorepo 里包越来越多)时,可以用动态矩阵:先跑一个”探测 job”,把矩阵 JSON 输出,再让下游 job 用 fromJSON 消费它。

5.1 基于目录列表生成矩阵

jobs:
  setup:                              # 探测 job:读取 packages/ 下的包名
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}   # 输出 JSON 给下游
    steps:
      - id: set-matrix
        run: |
          echo "matrix={\"include\":$(ls packages/ | jq -R -s -c 'split("\n") | map(select(length > 0)) | map({"package": .})')}" >> $GITHUB_OUTPUT

  test:                               # 消费 job:按 JSON 生成矩阵
    needs: setup
    strategy:
      matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing package ${{ matrix.package }}"

5.2 基于文件变更生成矩阵

配合 dorny/paths-filter,只有被改动的模块才进入测试矩阵,省下大量分钟数:

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.filter.outputs.changes }}
    steps:
      - uses: actions/checkout@v6
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            auth: src/auth/**
            user: src/user/**
            order: src/order/**

  test:
    needs: detect
    if: needs.detect.outputs.services != '[]'
    strategy:
      matrix:
        service: ${{ fromJson(needs.detect.outputs.services) }}
    runs-on: ubuntu-latest
    steps:
      - run: npm test --workspace=src/${{ matrix.service }}

5.3 调试技巧:查看矩阵展开结果

在 step 里把矩阵 JSON 打印出来,一目了然:

- name: Debug matrix
  run: echo "${{ toJson(matrix) }}"

6. 常见错误与对策

常见错误报错/现象原因解决办法
include 条目没生效期望新增的 job 不存在include 条目恰好匹配了某个已有组合,只追加了变量而未新增 job检查匹配逻辑;想让 include 条目不匹配现有组合,可用不同的变量值
exclude 顺序理解错误被排除的组合仍在运行exclude 放在 include 之前或组合规则混乱记住顺序:笛卡尔积 → include → exclude
矩阵组合数爆炸一次运行几十上百个 job,分钟数耗尽多维变量全排列组合过大控制矩阵规模(建议不超过 20 个 job),用 exclude 剔除无意义组合,或改用动态矩阵
Windows 上跑 Linux 命令失败Command not found没按系统区分命令用 if: runner.os == 'Windows' 等条件分支,或使用跨平台写法
fail-fast 导致信息丢失第一个失败后其余 job 全被取消fail-fast 默认为 trueCI 场景显式设置 fail-fast: false
在 runs-on 中引用错误变量名job 无法启动${{ matrix.os }} 与矩阵定义中变量名不一致核对矩阵变量名与引用处拼写一致

8. 一句话记忆

矩阵 = 一条柔性生产线:一份 job 定义 + 多个维度变量,GitHub 按笛卡尔积自动生成并行的多环境 job,include 加料、exclude 减料、fail-fast 控止损。