GitHub Actions: Matrix Build 進階
標籤:#DevOps #GitHub Actions #Matrix #並行化 #CI/CD
把單一 workflow 一次跑成 N 個並行 job,涵蓋 include / exclude / 動態矩陣 / fail-fast / 跨平台測試的完整實戰
目錄
- Matrix 的核心價值
- 基本語法
- Include 與 Exclude
- 動態矩陣
- Fail-fast 與容錯
- 並行數量控制
- 跨平台測試實戰
- Matrix 與 Reusable Workflow 組合
- Matrix 輸出聚合
- 最佳實踐
- 常見問題
- 總結
Matrix 的核心價值
Matrix 不只是「同一個程式跑多個 Go / Node 版本」,核心價值是:
- 並行化降時間:30 分鐘 sequential → 5 分鐘 parallel
- 覆蓋多組合:OS × language version × DB version,一次測完
- 共用流程定義:同一份 YAML 描述「N 個 job 的差異」
一個 job 同時跑多個版本是「順序執行」,matrix 是「同時開 N 個 job 並行執行」
基本語法
最簡單的 matrix
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
- run: npm test
→ 產生 3 個並行 job:test (18)、test (20)、test (22)
多維度組合(Cartesian Product)
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
steps:
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }} }
- run: npm test
→ 產生 3 × 3 = 9 個並行 job
多變數使用同一個 matrix entry
strategy:
matrix:
config:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: macos-latest, target: x86_64-apple-darwin }
- { os: windows-latest, target: x86_64-pc-windows-msvc }
steps:
- run: cargo build --target ${{ matrix.config.target }}
name: Build on ${{ matrix.config.os }}
把多個關聯屬性綁在一起,避免 cartesian product 產生不要的組合。
Include 與 Exclude
include — 增加額外組合(或補充欄位)
用法 1:加入 cartesian product 外的組合
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20]
include:
- os: ubuntu-latest
node: 22 # 多測一個 Ubuntu + Node 22
- os: windows-latest
node: 20 # 多加一個 Windows + Node 20
最終組合:
| os | node |
|---|---|
| ubuntu-latest | 18 |
| ubuntu-latest | 20 |
| ubuntu-latest | 22 ← include 加的 |
| macos-latest | 18 |
| macos-latest | 20 |
| windows-latest | 20 ← include 加的 |
用法 2:給特定組合加上額外欄位
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
include:
- os: ubuntu-latest
cache-path: ~/.cache
- os: macos-latest
cache-path: ~/Library/Caches
- os: windows-latest
cache-path: D:\.cache
steps:
- uses: actions/cache@v4
with:
path: ${{ matrix.cache-path }}
key: ${{ runner.os }}-build
exclude — 移除特定組合
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18 # Windows 不測 Node 18
- os: macos-latest
node: 22 # macOS 不測 Node 22
從 9 個降到 7 個。
include / exclude 處理順序
GitHub 處理順序:
1. 先計算 cartesian product
2. 套用 exclude(移除組合)
3. 套用 include(增加組合,或補充欄位)
所以 include 不會被 exclude 移除。
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20]
exclude:
- os: macos-latest
node: 20
include:
- os: macos-latest
node: 20 # 雖然在 exclude,但 include 還是會加回來
最終 macOS + Node 20 仍會被 include 加回。
實戰:複雜矩陣設計
針對「主流測試 + 邊緣測試」做出不同密度:
strategy:
matrix:
# 主流組合:全部測
os: [ubuntu-latest]
node: [18, 20, 22]
# 邊緣組合:只測 LTS
include:
- os: macos-latest
node: 20
- os: windows-latest
node: 20
- os: ubuntu-latest
node: 22
extra: "experimental" # 額外欄位標記
動態矩陣
當 matrix 內容無法在 workflow 寫死(例如要根據 PR 變更的檔案決定),用動態矩陣:
動態矩陣模式
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- id: set
run: |
# 偵測有哪些 package 改動
PACKAGES=$(./scripts/changed-packages.sh | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "matrix=$PACKAGES" >> "$GITHUB_OUTPUT"
test:
needs: detect
if: needs.detect.outputs.matrix != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJSON(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- run: cd packages/${{ matrix.package }} && npm test
動態矩陣的關鍵點
- 必須輸出合法 JSON:
["a", "b"]或更複雜的 array of objects - 用
fromJSON()在 caller 解析 - 空陣列要處理:
if: needs.detect.outputs.matrix != '[]'避免 matrix 為空時報錯 - outputs 有大小限制:單一 output 最大 1MB
範例:從 PR 變更檔案產生 matrix
jobs:
changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.filter.outputs.packages }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
list-files: json
filters: |
api:
- 'packages/api/**'
web:
- 'packages/web/**'
mobile:
- 'packages/mobile/**'
- run: |
MATRIX=$(echo '${{ steps.filter.outputs.changes }}' | jq -c '.')
echo "packages=$MATRIX" >> "$GITHUB_OUTPUT"
test:
needs: changes
if: needs.changes.outputs.packages != '[]'
strategy:
matrix:
package: ${{ fromJSON(needs.changes.outputs.packages) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cd packages/${{ matrix.package }} && npm test
Fail-fast 與容錯
fail-fast(預設 true)
strategy:
fail-fast: false # 預設 true
matrix:
node: [18, 20, 22]
fail-fast: true(預設):matrix 內任何一個 job 失敗,其他還沒跑完的會被取消fail-fast: false:讓所有 matrix job 跑完,即使某些失敗
選擇建議:
- 開發中:
true(快速看到失敗就停) - 全面測試 / pre-release 驗證:
false(看完整失敗分佈)
continue-on-error
strategy:
matrix:
config:
- { node: 18, stable: true }
- { node: 20, stable: true }
- { node: 22, stable: true }
- { node: 23, stable: false } # 預發版,容許失敗
jobs:
test:
runs-on: ubuntu-latest
continue-on-error: ${{ !matrix.config.stable }}
steps:
- run: npm test
continue-on-error: true 的 job 即使失敗,整個 workflow 仍會顯示為 success(會打勾但 job log 仍標示失敗)。
組合用法
strategy:
fail-fast: false
matrix:
target:
- { node: 18, allow-failure: false }
- { node: 20, allow-failure: false }
- { node: 22, allow-failure: false }
- { node: 23, allow-failure: true } # nightly
jobs:
test:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.target.allow-failure }}
→ 跑完所有版本,但 Node 23 失敗不影響整體狀態
並行數量控制
max-parallel — 限制同時執行數
strategy:
max-parallel: 3
matrix:
target: [a, b, c, d, e, f, g, h]
→ 雖然有 8 個組合,但同時只跑 3 個,排隊執行
用途:
- runner 配額有限
- 對外部資源有壓力(API rate limit、DB 連線)
- self-hosted runner 容量不夠
與 concurrency 對比
| 機制 | 控制範圍 | 用途 |
|---|---|---|
strategy.max-parallel |
單一 matrix 內 | 控制 matrix 內並行數 |
concurrency |
整個 workflow | 避免同時跑多個 workflow run |
# 多個觸發只跑最新
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
strategy:
max-parallel: 5 # 配合用
matrix: ...
跨平台測試實戰
範例 1:Go 跨平台 build
jobs:
build:
name: Build (${{ matrix.target.os }} / ${{ matrix.target.arch }})
runs-on: ${{ matrix.target.runner }}
strategy:
fail-fast: false
matrix:
target:
- { runner: ubuntu-latest, os: linux, arch: amd64 }
- { runner: ubuntu-latest, os: linux, arch: arm64 }
- { runner: macos-latest, os: darwin, arch: amd64 }
- { runner: macos-latest, os: darwin, arch: arm64 }
- { runner: windows-latest, os: windows, arch: amd64 }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: "1.22" }
- name: Build
env:
GOOS: ${{ matrix.target.os }}
GOARCH: ${{ matrix.target.arch }}
run: |
OUT="myapp-${GOOS}-${GOARCH}"
[ "$GOOS" = "windows" ] && OUT="$OUT.exe"
go build -o "$OUT" ./cmd/myapp
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target.os }}-${{ matrix.target.arch }}
path: myapp-*
範例 2:Python + 多個 DB
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12"]
db:
- { image: "postgres:15", port: 5432, type: "postgres" }
- { image: "postgres:16", port: 5432, type: "postgres" }
- { image: "mysql:8", port: 3306, type: "mysql" }
services:
database:
image: ${{ matrix.db.image }}
env:
POSTGRES_PASSWORD: postgres
MYSQL_ROOT_PASSWORD: mysql
ports:
- ${{ matrix.db.port }}:${{ matrix.db.port }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: ${{ matrix.python }} }
- run: pip install -r requirements.txt
- run: pytest
env:
DB_TYPE: ${{ matrix.db.type }}
DB_PORT: ${{ matrix.db.port }}
→ 3 × 3 = 9 組合
範例 3:Node + 多 OS + browser 測試
jobs:
e2e:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
browser: [chromium, firefox, webkit]
exclude:
- os: ubuntu-latest
browser: webkit # ubuntu 上 webkit 有問題
include:
- os: ubuntu-latest
shell: bash
- os: macos-latest
shell: bash
- os: windows-latest
shell: pwsh
defaults:
run:
shell: ${{ matrix.shell }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install ${{ matrix.browser }}
- run: npx playwright test --project=${{ matrix.browser }}
Matrix 與 Reusable Workflow 組合
Reusable workflow 也能接收 matrix 風格的 input,讓 caller 帶矩陣:
# reusable-test.yml
on:
workflow_call:
inputs:
node-version:
type: string
required: true
os:
type: string
required: true
jobs:
test:
runs-on: ${{ inputs.os }}
steps:
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node-version }} }
- run: npm test
# caller.yml
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20, 22]
uses: ./.github/workflows/reusable-test.yml
with:
node-version: ${{ matrix.node }}
os: ${{ matrix.os }}
注意:
jobs.<id>用uses:時,只能在外層放strategy.matrix,不能在 reusable workflow 內部再做 matrix
Matrix 輸出聚合
Matrix 內每個 job 都是獨立的,要彙整結果需要技巧。
方法 1:用 artifact 收集
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npm test -- --shard=${{ matrix.shard }}/4 --reporter=json > result-${{ matrix.shard }}.json
- uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.shard }}
path: result-${{ matrix.shard }}.json
aggregate:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: test-results-*
merge-multiple: true
- run: jq -s 'add' result-*.json > all.json
方法 2:整合測試報告
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: ./run-tests.sh --shard ${{ matrix.shard }}
- uses: actions/upload-artifact@v4
with:
name: junit-${{ matrix.shard }}
path: junit.xml
report:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: junit-*
path: reports
- uses: dorny/test-reporter@v1
with:
name: Combined Tests
path: reports/**/*.xml
reporter: java-junit
方法 3:聚合 outputs(有限制)
Matrix outputs 不能直接合併,要用 strategy 內 outputs 或 artifact。最簡單的還是 artifact。
最佳實踐
1. 給 matrix job 取好讀的名字
預設名字會把所有 matrix 變數連起來:test (ubuntu-latest, 20, postgres:15),長且不好讀。
jobs:
test:
name: Test on ${{ matrix.target.label }}
strategy:
matrix:
target:
- { label: "Ubuntu / Node 20", os: ubuntu-latest, node: 20 }
- { label: "macOS / Node 20", os: macos-latest, node: 20 }
2. 大矩陣要謹慎
100 個 matrix job × 5 分鐘 = 500 分鐘 runner 時間,費用很驚人。考慮:
- 主流組合每個 PR 跑
- 完整矩陣每天 nightly 跑一次
# pr-ci.yml — 主流測試
strategy:
matrix:
node: [20] # 只測 LTS
os: [ubuntu-latest]
# nightly.yml — 完整矩陣
on:
schedule:
- cron: "0 2 * * *"
strategy:
matrix:
node: [18, 20, 22, 23]
os: [ubuntu-latest, macos-latest, windows-latest]
3. 善用 include 避免無意義組合
不要這樣:
# ❌ 16 個組合,但很多不合理
matrix:
os: [ubuntu, macos, windows, ubuntu-arm]
arch: [x86, x64, arm, arm64]
改成:
# ✅ 只列合理組合
matrix:
include:
- { os: ubuntu-latest, arch: x86_64 }
- { os: ubuntu-latest, arch: aarch64 }
- { os: macos-latest, arch: x86_64 }
- { os: macos-latest, arch: aarch64 }
- { os: windows-latest, arch: x86_64 }
4. 動態矩陣加上「空陣列」檢查
test:
needs: detect
if: needs.detect.outputs.matrix != '[]' && needs.detect.outputs.matrix != ''
strategy:
matrix:
target: ${{ fromJSON(needs.detect.outputs.matrix) }}
不然 fromJSON('[]') 會讓 matrix 為空,GitHub 顯示為 error。
5. 用 matrix 做 sharding 加速測試
把測試切分到 N 個 shard 並行跑:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- run: ./run-tests.sh --shard=${{ matrix.shard }}/8
→ 原本 40 分鐘 → 8 shard × 5 分鐘並行 → 5 分鐘完成
6. Self-hosted runner + matrix 要小心
strategy:
matrix:
node: [18, 20, 22]
runs-on: [self-hosted, linux]
如果只有 2 個 self-hosted runner,3 個 matrix job 會有 1 個排隊。要評估 runner 容量。
常見問題
Q1:Matrix job 名字怎麼自訂?
用 name: 並引用 matrix 變數:
jobs:
test:
name: Test ${{ matrix.label }}
strategy:
matrix:
include:
- { label: "Linux Node 20", os: ubuntu-latest, node: 20 }
Q2:Matrix 內變數可以在 step if: 用嗎?
可以,而且 step 層級的 condition 也能取 matrix:
steps:
- name: macOS only step
if: matrix.os == 'macos-latest'
run: brew install something
Q3:Matrix outputs 怎麼合併?
Matrix 內每個 job 的 outputs 是獨立的,不能直接合併成陣列。實務做法:
- 用 artifact 上傳每個 job 的結果,後續 job 下載聚合
- 或用
needs.<job>.outputs.<key>但只能拿單一 job 的值
Q4:動態矩陣產生失敗,顯示 matrix must define at least one vector
通常是 fromJSON 收到空字串或 []。加 if: 保護:
if: needs.detect.outputs.matrix != '[]'
或在 detect job 直接給 fallback:
echo "matrix=${PACKAGES:-[]}" >> "$GITHUB_OUTPUT"
Q5:fail-fast: false 但 workflow 還是顯示失敗?
fail-fast 控制的是「matrix 內彼此是否互相取消」,不影響整個 workflow 的最終狀態。要避免失敗影響整體,需要在 job 層級設 continue-on-error。
Q6:Matrix 內可以呼叫 reusable workflow 嗎?
可以,語法:
jobs:
test:
strategy:
matrix:
target: [a, b, c]
uses: ./.github/workflows/reusable.yml
with:
target: ${{ matrix.target }}
但 reusable workflow 內不能再宣告 matrix(matrix 必須在 caller 層級)。
Q7:Matrix 上限是多少?
GitHub 規定:
- 單一 workflow 內,最多 256 個 job(包含 matrix 展開後的)
- 單一 matrix 最多 256 個組合(扣掉 exclude 後)
超過會直接拒絕執行。
Q8:能不能讓 matrix 等待某個共用準備 job 後再跑?
可以,用 needs:
jobs:
build-image:
runs-on: ubuntu-latest
steps: ...
test:
needs: build-image
strategy:
matrix:
shard: [1, 2, 3, 4]
steps: ...
build-image 跑完一次,4 個 matrix shard 並行用它的結果。
總結
核心要點
- 基本語法:
strategy.matrix.<key>: [values]→ cartesian product include:加組合或補充欄位(在 product 後加)exclude:從 product 移除組合(在 include 前處理)- 動態矩陣:
fromJSON(needs.x.outputs.matrix),要保護空陣列 fail-fast: false:跑完所有 matrix job,看完整失敗continue-on-error:允許某些 matrix job 失敗不影響整體max-parallel:限制 matrix 內並行數- Output 聚合:用 artifact,不能直接合併 outputs
速查 — 處理順序
1. cartesian product (key 陣列展開)
2. exclude 移除
3. include 加入(不會被 exclude 影響)
4. max-parallel 限制並行
5. fail-fast 決定失敗時是否取消其他
設計檢查清單
- Job 名字易讀(用
name:或include+label) - 主流組合每個 PR 跑,完整矩陣 nightly 跑
- 動態矩陣有空陣列檢查
- Sharding 切分長測試
- OS-specific 步驟用
if:包起來 - Self-hosted runner 容量足夠
速查骨架
strategy:
fail-fast: false
max-parallel: 5
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20, 22]
exclude:
- { os: macos-latest, node: 22 }
include:
- { os: windows-latest, node: 20 }
- { os: ubuntu-latest, node: 20, extra: "ssr" }
jobs:
test:
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.extra == 'experimental' }}
steps: ...
建立日期:2026-05-25