当你的项目需要在 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-latest | macos-latest | windows-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 数 | 矩阵膨胀风险 |
|---|---|---|
| Free | 20 | 4×5=20 刚好触顶 |
| Pro/Team | 40 | 5×4×2=40 刚好触顶 |
| Enterprise | 500+ | 一般项目不会触顶 |
一个 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-artifact 和 actions/download-artifact 间接共享:
- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}-${{ matrix.node }}
path: dist/
Q4: 为什么我的动态矩阵显示为空?
常见原因:
- JSON 格式错误(缺少引号或逗号)
fromJson的输入确实是空数组[]- 上一步 job 的
outputs定义格式错误
调试方法:在上一步添加 run: echo '${{ steps.xxx.outputs.yyy }}' 确认输出内容。
总结
GitHub Actions 矩阵构建是 CI 自动化的核心能力,但「滥用」比「不用」更危险。一个 100 个组合的矩阵可能让 PR 等待 30 分钟才能看到结果,完全违背持续集成的快速反馈原则。
矩阵设计的黄金法则:
| 原则 | 具体操作 |
|---|---|
| 最小覆盖 | 每个维度只选最关键的组合,而非全排列 |
| exclude > include | 先用 exclude 剪枝,再用 include 补充 |
| 动态裁剪 | monorepo 中只测试变更的 package |
| 分级 CI | PR 时用精简矩阵,main 分支用完整矩阵 |
| 缓存隔离 | 缓存键必须包含所有影响依赖的矩阵变量 |
通过 fail-fast、continue-on-error 和 max-parallel 的组合控制,可以在覆盖率与反馈速度之间找到团队的最优平衡点。
延伸阅读:
- GitHub Actions 缓存优化完全指南 — 矩阵构建中的缓存隔离与复用策略
- GitHub Actions 自托管 Runner — 自托管标签与矩阵路由
- Go 语言测试与质量工程 — Go 项目中的多版本矩阵测试实践
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。