GitHub Actions 矩阵构建策略:多平台、多版本、多配置的高效 CI

系统讲解 GitHub Actions 矩阵构建策略的设计模式与高级用法,涵盖多平台(Linux/macOS/Windows)、多语言版本、依赖矩阵组合、动态矩阵生成、失败策略控制与并行度优化,帮助团队将 CI 覆盖率提升 3 倍的同时将时间成本控制在可接受范围。

当你的项目需要在 Linux、macOS 和 Windows 三种操作系统上运行,同时支持 Node.js 18、20、22 三个 LTS 版本,还要区分标准构建和启用实验性特性的构建——传统方式是写 18 个独立的 job,维护噩梦随之诞生。GitHub Actions 的 strategy.matrix 正是为这种组合爆炸场景设计的优雅解药。本文从基础语法到动态矩阵、从失败策略到并行度调优,全面覆盖矩阵构建的工程实践。


一、矩阵构建的核心语法

1.1 最简示例:双轴矩阵

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

这个配置会生成 3 × 3 = 9 个并行 job

ubuntu-latestmacos-latestwindows-latest
Node 18
Node 20
Node 22

1.2 矩阵维度扩展

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20]
    experimental: [false, true]

# 2 × 2 × 2 = 8 个 job

随着维度增加,组合数呈指数级增长。2×2×2=8 尚可接受,但 3×3×4=36 就需要仔细评估成本和收益了。


二、include 与 exclude:精细化矩阵控制

2.1 添加特殊组合

某些测试只在特定配置下有意义,可以用 include 追加:

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest]
    node: [18, 20]
    include:
      # 额外测试:Windows + Node 20(项目有 Windows 特定问题历史)
      - os: windows-latest
        node: 20
      # 额外测试:ARM64 架构(GitHub 最近推出)
      - os: ubuntu-24.04-arm
        node: 22

2.2 排除不合法组合

不是所有组合都有意义。例如某些依赖在 Windows 上不支持旧版本 Node:

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: [16, 18, 20]
    exclude:
      # node-sass 在 Windows + Node 16 上已知有问题
      - os: windows-latest
        node: 16
      # macOS 上不需要测试 Node 16(已 EOL)
      - os: macos-latest
        node: 16

2.3 组合修正表

语法作用常见场景
include在笛卡尔积基础上追加特定组合特殊平台测试、金丝雀版本
exclude从笛卡尔积中移除特定组合已知不兼容、EOL 版本
include + 新 key注入额外变量同一 os 但不同的 runner 标签

三、动态矩阵生成:基于代码仓库内容

静态矩阵适合配置固定的项目,但在 monorepo 或微服务架构中, packages 可能动态增减。此时需要在 workflow 运行时生成矩阵。

3.1 从文件系统生成矩阵

jobs:
  discover:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.set-matrix.outputs.packages }}
    steps:
      - uses: actions/checkout@v4
      - name: Discover packages
        id: set-matrix
        run: |
          # 查找所有包含 package.json 的子目录
          PACKAGES=$(find packages -name 'package.json' -maxdepth 2 | \
            xargs -I {} dirname {} | \
            jq -R -s -c 'split("\n")[:-1]')
          echo "packages=$PACKAGES" >> $GITHUB_OUTPUT

  test:
    needs: discover
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: ${{ fromJson(needs.discover.outputs.packages) }}
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
        working-directory: ${{ matrix.package }}
      - run: npm test
        working-directory: ${{ matrix.package }}

3.2 从变更文件生成矩阵(仅测试改动部分)

大型 monorepo 中,为每个 package 跑完整测试集太昂贵。可以只测试本次 PR 改动的 package

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.changed.outputs.packages }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Get changed packages
        id: changed
        run: |
          CHANGED=$(git diff --name-only origin/main...HEAD | \
            grep '^packages/' | cut -d'/' -f2 | sort -u | \
            jq -R -s -c 'split("\n")[:-1]')
          echo "packages=$CHANGED" >> $GITHUB_OUTPUT

  test-changed:
    needs: changes
    if: needs.changes.outputs.packages != '[]'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        package: ${{ fromJson(needs.changes.outputs.packages) }}
    steps:
      - uses: actions/checkout@v4
      - name: Test ${{ matrix.package }}
        run: |
          cd packages/${{ matrix.package }}
          npm ci && npm test

四、失败策略控制

4.1 fail-fast 与 continue-on-error

默认情况下,矩阵中任一 job 失败,其余正在运行的 job 会被立即取消fail-fast: true)。这对于快速反馈很有用,但有时你需要看到所有平台的结果:

strategy:
  fail-fast: false  # 只要有一个失败就全部取消 → 改为 false,全部跑完
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: [18, 20, 22]

对于实验性配置(如 Node 23 预览版),你不希望它阻塞整个 CI:

strategy:
  matrix:
    os: [ubuntu-latest]
    node: [18, 20, 22]
    include:
      - os: ubuntu-latest
        node: 23
        experimental: true

jobs:
  test:
    runs-on: ${{ matrix.os }}
    continue-on-error: ${{ matrix.experimental == true }}
    steps:
      - run: npm test

continue-on-error: true 的 job 失败时不会导致 workflow 失败,但会在 UI 中显示为橙色警告,提醒你关注。

4.2 失败策略决策树

是否需要尽快获得反馈?
├── 是 → fail-fast: true(默认)
│   └── 失败时剩余 job 自动取消
└── 否 → fail-fast: false
    ├── 是否需要看到所有平台完整结果?
    │   └── 是 → continue-on-error: false(默认)
    │       └── 任一失败则 workflow 失败
    └── 是否有实验性配置?
        └── 是 → continue-on-error: true(仅实验项)
            └── 实验项失败不阻塞,其他项正常判定

五、并行度控制与资源配额

5.1 GitHub-hosted Runner 并发限制

计划并发 job 数矩阵膨胀风险
Free204×5=20 刚好触顶
Pro/Team405×4×2=40 刚好触顶
Enterprise500+一般项目不会触顶

一个 5×4×2=40 的矩阵会在 Team 计划上完全占满并发配额,导致其他 workflow 排队。缓解策略:

5.2 max-parallel 限制

strategy:
  max-parallel: 5  # 最多同时跑 5 个 job
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    node: [18, 20, 22]
    # 3×3=9,但 max-parallel=5,总时间 ≈ 2 批次

5.3 关键路径优先调度

把最快的组合放在前面,确保关键路径尽早完成:

strategy:
  matrix:
    config:
      - os: ubuntu-latest  # 最快,最重要
        node: 20
        priority: critical
      - os: ubuntu-latest
        node: 18
      - os: macos-latest    # 中等速度
        node: 20
      - os: windows-latest  # 最慢
        node: 20

GitHub Actions 不保证调度顺序,但实证表明列表前面的组合通常优先分配 runner。


六、矩阵构建中的缓存隔离

6.1 缓存键必须包含矩阵变量

当不同矩阵项使用不同的 Node 版本或操作系统时,缓存必须隔离:

steps:
  - uses: actions/cache@v4
    with:
      path: node_modules
      key: ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('package-lock.json') }}

反例:如果所有矩阵项使用同一缓存键,Ubuntu + Node 18 的 job 可能恢复 macOS + Node 20 缓存中的原生模块,导致构建失败。

6.2 跨矩阵缓存共享的例外情况

某些缓存(如 Yarn 的全局缓存)与 Node 版本无关,可以安全共享:

- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ~/.cache/yarn
    key: ${{ runner.os }}-global-pkgs-${{ hashFiles('package-lock.json') }}

七、自托管 Runner 的矩阵适配

当使用自托管 Runner 时,runs-on 需要使用标签匹配而非 GitHub 提供的预设标签:

strategy:
  matrix:
    runner: [self-hosted-linux, self-hosted-windows]
    arch: [x64, arm64]
    include:
      - runner: self-hosted-macos
        arch: arm64  # Apple Silicon

jobs:
  test:
    runs-on: [self-hosted, ${{ matrix.runner }}, ${{ matrix.arch }}]
    steps:
      - run: uname -m  # 验证架构

八、常见问题解答(FAQ)

Q1: 矩阵 job 数量有上限吗?

技术上无硬性上限,但 GitHub-hosted Runner 的并发配额会限制实际并行数。超过 256 个组合的矩阵可能触发二次调度排队,导致总时间不可预测。

Q2: 如何为矩阵中的特定项设置不同的环境变量?

strategy:
  matrix:
    config:
      - os: ubuntu-latest
        env: { DATABASE_URL: postgres://localhost/test }
      - os: windows-latest
        env: { DATABASE_URL: postgres://win-host/test }
env: ${{ matrix.config.env }}

Q3: 矩阵 job 之间可以共享产物吗?

直接共享不行(每个 job 在独立 runner 上),但可以通过 actions/upload-artifactactions/download-artifact 间接共享:

- uses: actions/upload-artifact@v4
  with:
    name: build-${{ matrix.os }}-${{ matrix.node }}
    path: dist/

Q4: 为什么我的动态矩阵显示为空?

常见原因:

  1. JSON 格式错误(缺少引号或逗号)
  2. fromJson 的输入确实是空数组 []
  3. 上一步 job 的 outputs 定义格式错误

调试方法:在上一步添加 run: echo '${{ steps.xxx.outputs.yyy }}' 确认输出内容。


总结

GitHub Actions 矩阵构建是 CI 自动化的核心能力,但「滥用」比「不用」更危险。一个 100 个组合的矩阵可能让 PR 等待 30 分钟才能看到结果,完全违背持续集成的快速反馈原则。

矩阵设计的黄金法则

原则具体操作
最小覆盖每个维度只选最关键的组合,而非全排列
exclude > include先用 exclude 剪枝,再用 include 补充
动态裁剪monorepo 中只测试变更的 package
分级 CIPR 时用精简矩阵,main 分支用完整矩阵
缓存隔离缓存键必须包含所有影响依赖的矩阵变量

通过 fail-fastcontinue-on-errormax-parallel 的组合控制,可以在覆盖率与反馈速度之间找到团队的最优平衡点。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「github-actions」更多文章

  1. GitHub Actions 通知与 ChatOps:Slack/钉钉/飞书集成与评论触发工作流
  2. GitHub Actions 自托管 Runner:架构设计、安全隔离与大规模部署
  3. GitHub Actions 缓存优化完全指南:从 actions/cache 到分层依赖管理