Matrix strategy
Как запускать варианты по OS, runtime version, package manager или feature flag; `include`, `exclude`, `fail-fast`, `max-parallel`, naming jobs и контроль стоимости.
Matrix strategy
Matrix strategy в GitHub Actions — это способ описать один job как набор вариантов: разные OS, версии runtime, package managers, feature flags или режимы тестов. Вместо трёх почти одинаковых jobs вы пишете один job с strategy.matrix, а GitHub создаёт отдельный job run для каждой комбинации значений.
В CI pipeline базовый контракт был один: checkout → setup → install → lint/typecheck/test/build. Matrix добавляет к нему вопрос: «в каких средах этот контракт должен быть правдой?» Если библиотека должна работать на Node 20 и 22, на Ubuntu и Windows, это уже не один ответ, а четыре.
flowchart TD A[Один job definition] --> B[Оси matrix] B --> C[os] B --> D[runtime] B --> E[package manager] C --> F[Полный набор комбинаций] D --> F E --> F F --> G[exclude убирает бессмысленные варианты] G --> H[include добавляет поля или точечные jobs] H --> I[Отдельные job runs] I --> J[fail-fast / continue-on-error] I --> K[max-parallel]
Как матрица разворачивается
Минимальная матрица выглядит так:
jobs:
test:
name: test / ${{ matrix.os }} / node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm testGitHub создаст четыре job run: Ubuntu+Node 20, Ubuntu+Node 22, Windows+Node 20, Windows+Node 22. Значения доступны через matrix.os, matrix.node и другие ключи, которые вы сами задали. runs-on часто берут прямо из матрицы, поэтому выбор runner связан с GitHub-hosted runners и, если используются свои машины, с Self-hosted runners.
Есть важный практический лимит: одна матрица может породить максимум 256 jobs за workflow run. До этого лимита легко добраться случайно: 4 OS × 5 версий runtime × 3 package managers × 4 feature flags = 240 jobs. Формально красиво, но почти всегда слишком дорого и шумно.
include и exclude
exclude убирает комбинации из полного декартова произведения. Это нужно, когда вариант бессмысленен или нестабилен: например, старый runtime не поддерживается на Windows, macOS нужен только для одной версии, а experimental-флаг не должен запускаться везде.
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20, 22]
package-manager: [npm, pnpm]
exclude:
- os: macos-latest
package-manager: pnpminclude работает иначе. Он может добавить поля к существующим комбинациям или добавить отдельную новую комбинацию. Это удобно для точечных исключений из общего правила:
jobs:
test:
name: test / ${{ matrix.os }} / node ${{ matrix.node }} / ${{ matrix.package-manager }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [20, 22]
package-manager: [npm]
include:
- os: ubuntu-latest
node: 22
package-manager: pnpm
experimental: true
continue-on-error: ${{ matrix.experimental == true }}
steps:
- uses: actions/checkout@v4
- name: Set up pnpm
if: ${{ matrix.package-manager == 'pnpm' }}
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: ${{ matrix.package-manager }}
- name: Install dependencies
shell: bash
run: |
if [ "${{ matrix.package-manager }}" = "pnpm" ]; then
pnpm install --frozen-lockfile
else
npm ci
fi
- run: ${{ matrix.package-manager }} testЗдесь основная матрица проверяет npm на двух OS и двух версиях Node. Через include добавлен один отдельный экспериментальный job: Ubuntu + Node 22 + pnpm. Такой job не должен ломать весь workflow, поэтому continue-on-error привязан к matrix.experimental.
fail-fast, experimental jobs и шум
По умолчанию fail-fast для matrix strategy включён. Если один job в матрице падает, GitHub отменяет остальные queued и in-progress jobs этой матрицы. Это хорошо для быстрых PR-проверок: если Ubuntu+Node 22 уже показал грубую ошибку компиляции, ждать остальные варианты часто бессмысленно.
Но для диагностики совместимости fail-fast: false часто полезнее. Например, если вы поддерживаете Windows, macOS и Linux, вам может быть важно увидеть все падения сразу, а не чинить их по одному rerun за rerun.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20, 22]Не путайте fail-fast и continue-on-error. fail-fast управляет всей матрицей: отменять ли соседние jobs после падения. continue-on-error относится к конкретному job run. Хорошая схема для experimental runtime такая: стабильные варианты обязаны проходить, experimental-вариант может упасть без красного workflow, но при этом виден в checks.
max-parallel и контроль стоимости
Matrix не всегда ускоряет pipeline. Она ускоряет wall-clock time только если есть доступные runners и если параллельный запуск не упирается во внешние ресурсы: rate limits, базу данных, staging API, shared test account. На платных private repos ещё появляется прямой денежный эффект: GitHub Actions usage считается по времени runner jobs, а Windows и macOS runners стоят дороже Linux.
max-parallel ограничивает число одновременно выполняющихся jobs внутри матрицы:
strategy:
max-parallel: 2
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20, 22]Это не уменьшает общее число jobs, но снижает пиковую нагрузку. Полезно, если тесты бьют в один внешний sandbox, если self-hosted runners ограничены железом или если macOS jobs не должны занимать всю очередь. Для отмены устаревших запусков на новых pushes используйте concurrency, как в CI pipeline; это другая ось оптимизации, ближе к Оптимизация времени CI.
Имена jobs и required checks
У matrix jobs должны быть понятные name. Если оставить один общий name: test, в интерфейсе checks будет сложнее понять, что именно упало. Лучше включать ключевые значения:
name: test / ${{ matrix.os }} / node ${{ matrix.node }}Для branch protection это тоже важно. Required check должен быть стабильным и предсказуемым. Если вы меняете имя job или структуру матрицы, protected branch может начать ждать старый check или перестать требовать новый. Поэтому матрицу лучше проектировать как публичный контракт репозитория, а не как временную YAML-игрушку.
Что класть в оси матрицы
Хорошая ось матрицы отвечает на реальный compatibility-риск. Для app-кода обычно достаточно одной основной OS и одной актуальной версии runtime на быстрых PR-проверках, плюс расширенная матрица на push в main, nightly schedule или release branch. Для библиотек матрица шире: поддерживаемые версии runtime, OS, иногда package manager или backend.
Плохая ось матрицы просто размножает уверенность без новой информации. Например, гонять lint на трёх OS обычно не нужно: lint почти всегда зависит от runtime и lock-файла, а не от Windows/macOS. E2E на всех runtime-версиях тоже часто избыточны: smoke/e2e можно оставить на одном «каноническом» runtime, а unit tests прогнать шире.
Cache в матрице требует аккуратных ключей. Если OS, runtime или package manager влияют на зависимости, включайте их в cache key или используйте setup-action cache, который учитывает lock-файлы. Иначе один вариант матрицы может пытаться переиспользовать неподходящий cache. Подробная граница между cache и артефактами разобрана в Dependency cache, а сохранение отчётов для каждого варианта — в Artifacts и reports.
Рабочая эвристика
Начинайте с минимальной матрицы, которая покрывает обещанную поддержку. Для PR: самая быстрая и репрезентативная проверка. Для main и релизов: расширенная совместимость. Используйте exclude, чтобы убрать бессмысленные комбинации, include — чтобы добавить точечные варианты, fail-fast — осознанно под режим диагностики, max-parallel — когда параллельность дороже пользы.
Matrix strategy хороша не потому, что делает YAML «мощнее». Она делает поддержку явной: какие среды проект считает важными, какие варианты экспериментальны и сколько команда готова платить за уверенность.
See also
- CI pipeline — базовый контракт build/test workflow, который matrix размножает по средам.
- Jobs, dependencies и conditions —
needs,ifи условия вокруг jobs. - Contexts и expressions — как работают
matrix.*, expressions и вычисление значений. - GitHub-hosted runners — доступные OS, runner images и drift окружения.
- Self-hosted runners — когда матрица исполняется на своих runner pools.
- Dependency cache — cache keys для разных OS, runtime и package managers.
- Artifacts и reports — отчёты и build outputs из разных matrix jobs.
- Оптимизация времени CI — pruning, cancellation, split jobs и анализ длительности.
- Secure use и threat model — почему широкая матрица не должна автоматически получать широкие права.
