GitHub Actions Matrix 构建
大约 11 分钟约 3385 字
GitHub Actions Matrix 构建
简介
Matrix 构建的核心价值,是在一套工作流中系统性验证多个运行环境、语言版本、构建参数或部署目标,而不是单纯"把 job 跑得更多"。对于 SDK、前端库、CLI 工具、多版本运行时项目来说,Matrix 能显著提高回归覆盖率,但前提是控制好组合数量、缓存策略和日志可诊断性。
Matrix 构建的本质是"参数化测试"在 CI 层面的体现。通过定义多个维度的参数组合,GitHub Actions 会自动创建多个并行的 Job 实例,每个实例运行相同的步骤但使用不同的参数。这使得团队可以在一次代码提交中,同时验证代码在不同操作系统、不同运行时版本、不同架构下的行为一致性。
特点
Matrix 基础概念
矩阵维度类型
# 维度类型一:固定列表(最常用)
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
# 维度类型二:对象列表(每个组合可以有不同属性)
matrix:
include:
- os: ubuntu-latest
node: 18
npm: npm
- os: ubuntu-latest
node: 20
npm: pnpm
- os: windows-latest
node: 20
npm: yarn
# 维度类型三:动态矩阵(由前置 Job 生成)
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}矩阵组合计算
# 笛卡尔积计算
# os: [ubuntu, windows] x node: [18, 20]
# = 4 个组合
# (ubuntu, 18), (ubuntu, 20), (windows, 18), (windows, 20)
# 包含额外组合
# os: [ubuntu, windows] x node: [18, 20] + include: [{os: macos, node: 20}]
# = 5 个组合
# 排除特定组合
# os: [ubuntu, windows] x node: [18, 20] - exclude: [{os: windows, node: 18}]
# = 3 个组合实现
基础 Matrix:Node 版本 + 操作系统
name: CI Matrix
on:
push:
branches: [main]
pull_request:
branches: [main]
# 并发控制:同一分支的新构建取消旧的
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
# 为每个组合起一个可读的名称
name: Test (${{ matrix.os }} / Node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
# fail-fast: false — 某个组合失败时继续执行其他组合
# 默认为 true(一个失败就取消所有)
fail-fast: false
max-parallel: 10 # 最大并行数
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.node == 20
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/Go 语言多版本多平台矩阵
name: Go CI Matrix
on:
push:
branches: [main]
pull_request:
jobs:
test:
name: Test (${{ matrix.os }} / Go ${{ matrix.go-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
go-version: ['1.21', '1.22', '1.23']
steps:
- uses: actions/checkout@v4
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true # 自动缓存 Go module
- name: Download dependencies
run: go mod download
- name: Run vet
run: go vet ./...
- name: Run tests
run: go test -race -coverprofile=coverage.out ./...
- name: Run lint
uses: golangci/golangci-lint-action@v4
with:
version: latest
- name: Build
run: go build -v ./...Python 多版本矩阵
name: Python CI Matrix
on:
push:
branches: [main]
pull_request:
jobs:
test:
name: Test (${{ matrix.os }} / Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Set up 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
pip install pytest pytest-cov
- name: Run tests
run: |
pytest --cov=src --cov-report=xml --cov-report=term-missing
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}-py${{ matrix.python-version }}
path: coverage.xmlinclude / exclude 与差异化步骤
jobs:
build:
name: Build (${{ matrix.os }} / Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python: ['3.10', '3.11']
# 额外添加一个实验性组合
include:
- os: ubuntu-latest
python: '3.12'
experimental: true
- os: ubuntu-latest
python: '3.10'
experimental: false
extra-flag: '--enable-feature-x'
# 排除特定组合
exclude:
- os: windows-latest
python: '3.12'
# 实验性组合允许失败
continue-on-error: ${{ matrix.experimental || false }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest -q
# 条件步骤:只在特定组合上运行
- name: Run Linux-only benchmark
if: matrix.os == 'ubuntu-latest'
run: python scripts/benchmark.py
- name: Upload Windows artifact
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-build-py${{ matrix.python }}
path: dist/
# 使用额外参数
- name: Build with extra flags
if: matrix.extra-flag
run: python setup.py build ${{ matrix.extra-flag }}动态矩阵
# 动态矩阵:由前置 Job 生成组合列表
jobs:
# 前置 Job:发现需要构建的服务列表
discover:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set-matrix
run: |
# 从目录结构或配置文件中提取服务列表
SERVICES=$(ls -d services/*/ | xargs -I{} basename {} | jq -R . | jq -sc .)
echo "matrix={\"service\":$SERVICES}" >> $GITHUB_OUTPUT
# 构建矩阵:使用动态生成的组合
build:
needs: discover
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJson(needs.discover.outputs.matrix) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Build ${{ matrix.service }}
run: |
cd services/${{ matrix.service }}
docker build -t registry.example.com/${{ matrix.service }}:${{ github.sha }} .# 从 Git 变更文件动态生成矩阵
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.changes.outputs.services }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
api:
- 'services/api/**'
worker:
- 'services/worker/**'
admin:
- 'services/admin/**'
list-files: shell
build-changed:
needs: detect-changes
runs-on: ubuntu-latest
if: needs.detect-changes.outputs.services != '[]'
strategy:
matrix:
service: ${{ fromJson(needs.detect-changes.outputs.services) }}
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Build changed service
run: |
cd services/${{ matrix.service }}
docker build -t ${{ matrix.service }}:${{ github.sha }} .缓存策略
# 缓存必须带上维度,避免不同版本污染
# npm 缓存
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-node${{ matrix.node }}-
npm-${{ runner.os }}-# pnpm 缓存
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: pnpm-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-node${{ matrix.node }}-
pnpm-${{ runner.os }}-# pip 缓存
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
restore-keys: |
pip-${{ runner.os }}-py${{ matrix.python-version }}-
pip-${{ runner.os }}-# Go module 缓存(setup-go 自带 cache: true)
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true# Docker layer 缓存
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: docker-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('Dockerfile', 'go.sum') }}
restore-keys: |
docker-${{ runner.os }}-${{ matrix.target }}-可复用工作流
# .github/workflows/reusable-build.yml
name: Reusable Build
on:
workflow_call:
inputs:
service-name:
required: true
type: string
node-version:
required: false
type: string
default: '20'
secrets:
registry-password:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm run build
- run: npm test# 调用可复用工作流 + 矩阵
name: CI
on:
push:
branches: [main]
jobs:
build-all:
strategy:
matrix:
service: [api, worker, admin]
node-version: [18, 20]
fail-fast: false
uses: ./.github/workflows/reusable-build.yml
with:
service-name: ${{ matrix.service }}
node-version: ${{ matrix.node-version }}
secrets: inherit发布矩阵
# 按环境/镜像变体构建发布
jobs:
docker-build:
name: Build ${{ matrix.target }} (${{ matrix.env }})
runs-on: ubuntu-latest
strategy:
matrix:
target: [api, worker, gateway]
env: [staging, production]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
target: ${{ matrix.target }}
push: true
tags: |
registry.example.com/${{ matrix.target }}:${{ github.sha }}
registry.example.com/${{ matrix.target }}:${{ matrix.env }}-latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
ENV=${{ matrix.env }}
VERSION=${{ github.sha }}# 多架构构建矩阵
jobs:
multi-arch-build:
runs-on: ubuntu-latest
strategy:
matrix:
platform: [linux/amd64, linux/arm64]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build for ${{ matrix.platform }}
run: |
docker buildx build \
--platform ${{ matrix.platform }} \
-t registry.example.com/app:${{ matrix.platform }}-${{ github.sha }} \
--push .矩阵结果聚合
# 等待所有矩阵组合完成后执行汇总
jobs:
test:
name: Test (${{ matrix.os }} / Node ${{ matrix.node }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test
# 汇总 Job:所有测试通过后执行
report:
needs: test
runs-on: ubuntu-latest
if: always() # 即使有测试失败也执行
steps:
- name: Check test results
run: |
echo "Test matrix completed"
echo "All results: ${{ needs.test.result }}"
- name: Generate coverage report
uses: actions/download-artifact@v4
with:
pattern: coverage-*
path: coverage/
merge-multiple: true分层触发策略
# PR:快速路径(只跑核心组合)
name: CI (PR)
on:
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
node: [20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- run: npm test# 主分支:全量矩阵
name: CI (Full Matrix)
on:
push:
branches: [main]
# 定时全量验证
schedule:
- cron: '0 2 * * 1' # 每周一凌晨 2 点
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-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 -- --coverage
- run: npm run lintComposite Action 复用
# .github/actions/setup-python-env/action.yml
name: 'Setup Python Environment'
description: 'Setup Python with caching and dependencies'
inputs:
python-version:
required: true
requirements-file:
required: false
default: 'requirements.txt'
runs:
using: 'composite'
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'
- shell: bash
run: pip install -r ${{ inputs.requirements-file }}# 在矩阵中使用 Composite Action
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
python: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-python-env
with:
python-version: ${{ matrix.python }}
- run: pytest优点
缺点
总结
GitHub Actions Matrix 的重点是用最小成本覆盖真正有风险的环境差异。实际落地时建议先识别"哪些维度真的会导致行为不同",再通过 include / exclude、缓存隔离、可复用工作流和分层触发策略控制矩阵规模。核心原则:PR 用快速路径,主分支用全量矩阵,定时任务用实验性组合。
关键知识点
- Matrix 不是越大越好,而是要覆盖关键风险维度
fail-fast: false更适合排查多维失败场景- 缓存 key 必须包含 OS / 版本等差异维度
- include / exclude 是控制矩阵规模和兼容性差异的关键
- 动态矩阵通过
fromJson()从上游 Job 获取参数 continue-on-error可让实验性组合失败不阻断整体流程- 并发控制
concurrency可避免同一分支的重复构建浪费资源
项目落地视角
- 前端库:Node 18/20/22 + Ubuntu/Windows 双维验证
- Python SDK:多个 Python 版本 + 多平台测试轮子构建
- CLI 工具:针对 macOS / Linux / Windows 分别打包与验证
- 微服务 monorepo:按服务名动态生成构建矩阵
- Go 项目:多个 Go 版本 + 多平台交叉编译
- Docker 镜像:多架构 + 多环境构建
常见误区
- 把所有可能维度都加进矩阵,导致 CI 慢得不可接受
- 所有组合共用同一个缓存 key,结果互相污染
- 只跑矩阵,不给失败组合起清晰名字,日志难看懂
- 生产发布也全矩阵并发执行,却没有环境隔离和审批
- 动态矩阵的 JSON 格式错误导致工作流解析失败
- 忽略 macOS/Windows Runner 的费用成本
- 没有区分 PR 快速路径和主分支全量路径
进阶路线
- 学习 reusable workflow 和 composite action 抽象公共逻辑
- 按代码变更范围动态生成矩阵,减少无效构建
- 将矩阵拆成 PR 快路径和 nightly 全量路径
- 为大规模矩阵接入 test shard、artifact 聚合和失败重试策略
- 使用 GitHub Actions 的
needs.result实现矩阵结果汇总 - 研究自托管 Runner 降低大规模矩阵的运行成本
适用场景
- 多平台 CLI / SDK / 桌面工具项目
- 需要验证多个 Node / Python / .NET 版本兼容性的库项目
- Monorepo 中多个服务并行构建测试
- 构建多镜像、多目标环境的 CI/CD 流程
- 开源项目需要验证多平台兼容性
- 多架构 Docker 镜像构建
落地建议
- 先确定真正有风险差异的维度,再启用矩阵
- 快路径只保留核心组合,全量矩阵放到定时或主分支流水线
- 对每个组合使用可读 job 名称和独立 artifact 命名
- 对实验性维度使用
continue-on-error,避免拖垮主流程 - 监控 CI 耗时和费用,定期优化矩阵维度
- 利用 GitHub Actions 的缓存特性减少重复下载
排错清单
- 检查 matrix include/exclude 是否符合预期
- 检查失败组合是否有共同特征(OS、版本、依赖)
- 检查缓存 key 是否遗漏了关键维度
- 检查并发限制、artifact 名称和 secrets 使用是否存在冲突
- 检查动态矩阵的 JSON 输出格式是否正确
- 检查
fromJson()是否能正确解析矩阵参数
复盘问题
- 当前矩阵中的每个维度,是否都对应真实风险?
- 哪些组合应作为 PR 快速回归,哪些适合夜间全量回归?
- 失败时你能否一眼看出是哪个维度的问题?
- 现有矩阵是否在"覆盖率"和"执行成本"之间取得了平衡?
- 矩阵的平均执行时间和费用是多少?是否有优化空间?
