Матрицы в GitHub Actions позволяют запускать одну работу сразу в нескольких конфигурациях, экономя время и усилия. В этой статье разберём, как правильно строить такие матрицы, какие приёмы ускоряют CI и как избежать типичных ловушек.

Понимание базовой идеи

Идея простая: у вас есть несколько осей — операционные системы, версии языков, наборы параметров — и вы хотите проверить все сочетания. Вместо ручного создания множества работ достаточно описать их как матрицу, и Actions порождает параллельные джобы автоматически.

Это особенно удобно при кросс-платформенном тестировании или при проверке пакета на разных версиях зависимостей. Но простота маскирует нюансы: без продуманной стратегии матрица легко разрастётся и приведёт к долгим и дорогим прогонкам.

Базовый синтаксис: пример и объяснение

Типичная конфигурация выглядит компактно: вы объявляете стратегию и массивы значений для каждой оси. GitHub Actions создаёт декартово произведение значений и запускает отдельную работу для каждой комбинации.

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [14, 16]
        include:
          - os: windows-latest
            node: 18
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

В этом примере создаются комбинации осей, а include добавляет специфическое сочетание, которое не получается простым декартовым произведением. Параметр fail-fast контролирует, завершать ли остальные задачи, если одна из них упала.

include и exclude: как управлять набором комбинаций

Если вам нужны не все сочетания осей, используйте exclude, чтобы убрать нежелательные пары, и include, чтобы добавить уникальные случаи. Это помогает держать матрицу компактной и релевантной.

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    python: [3.8, 3.9, 3.10]
    exclude:
      - os: windows-latest
        python: 3.8
    include:
      - os: windows-latest
        python: 3.11

exclude полезен, когда какие-то комбинации бессмысленны или приводят к длительным прогонкам. include — способ добавить редкие, но важные конфигурации без расширения других осей.

fail-fast и max-parallel: тонкая настройка параллелизма

Параметр max-parallel ограничивает количество одновременно выполняемых задач, что полезно при ограничениях по ресурсам или при желании уменьшить нагрузку на внешние сервисы. Значение по умолчанию позволяет запускать все задания одновременно, но это не всегда выгодно.

fail-fast прерывает оставшиеся задачи при первом падении (если true). Это удобно на ранних этапах разработки, когда важны быстрые сигналы об ошибках. На этапе релиза чаще устанавливают fail-fast: false, чтобы собрать полную картину по всем конфигурациям.

Продвинутые приёмы: динамические матрицы и кэширование

Иногда количество комбинаций генерируется программно — например, список тестовых пакетов зависит от содержимого репозитория. В таких случаях можно сгенерировать JSON с комбинациями в одном задании и передать его в качестве вывода в следующий job, где использовать fromJson.

Идея: одна работа собирает пары параметров и сохраняет их как output, в следующей работе strategy.matrix задаётся через выражение вида ${{ fromJson(needs.generate.outputs.matrix) }}. Это даёт гибкость и позволяет исключать лишние комбинации ещё на этапе планирования.

Кэширование в рамках матрицы

Матрицы часто запускают одинаковые шаги — установка зависимостей, сборка. Кэширование помогает сократить время каждого прогона. Формируйте ключ кэша с учётом осей матрицы, чтобы избежать конфликтов и повысить процент попаданий.

- name: Cache node modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}

В этом примере ключ включает runner.os и matrix.node — кэш будет специфичен для каждой комбинации, но при этом переиспользуем внутри неё между запусками.

Оркестрация: сборка артефакта и параллельные тесты

Частая архитектура CI — сперва собрать артефакт, затем прогнать тесты параллельно в разных окружениях. Для этого первая работа создаёт и загружает артефакт, а следующая, с матрицей, скачивает его и выполняет проверки.

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-name: ${{ steps.pack.outputs.name }}
    steps:
      - id: pack
        run: |
          # сборка и упаковка
          echo "::set-output name=name::my-artifact-1.0.0"
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ steps.pack.outputs.name }}
          path: ./dist

  test:
    needs: build
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [14,16]
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: ${{ needs.build.outputs.artifact-name }}

Это позволяет собирать артефакт один раз и использовать его в параллельных тестах, экономя время и ресурсы. В сложных пайплайнах такое разделение упрощает диагностику: сборка и тестирование логически отделены.

Таблица: когда использовать какой подход

Короткая таблица поможет выбрать стратегию в зависимости от цели и ограничений.

Цель Подход Комментарий
Быстрый рантайм на PR Ограниченная матрица + max-parallel Тестировать лишь критичные ОС и версии
Полная проверка перед релизом Полная матрица, fail-fast: false Собрать результаты по всем комбинациям
Динамические варианты Генерация матрицы в job + fromJson Позволяет учитывать содержимое репозитория

Практические советы и распространенные ошибки

Не стоит сразу расширять все возможные оси до многотысячной матрицы. Ограничьте число осей и используйте include для ключевых сочетаний. Это уменьшит количество лишних прогонов и упростит анализ результатов.

  • Именуйте комбинации через include, чтобы в логах было проще ориентироваться.
  • Используйте max-parallel при ограниченных ресурсах или когда внешние сервисы ограничивают количество одновременных подключений.
  • Кэшируйте зависимости с учётом matrix-осей, чтобы избежать конфликтов кэша.
  • Если тесты флаки, сначала фиксируйте их локально на целевых комбинациях, а потом включайте в матрицу.

Мой опыт: что реально помогает

В одном проекте мы начали с простой матрицы, но быстро столкнулись с тем, что большинство комбинаций проверяли одни и те же вещи. Добавил include для ключевых сочетаний и исключил тривиальные комбинации через exclude — сократили время CI на 60% без потери покрытия.

Также помогло разделение на build и test. Сборка выполняется один раз, а тесты запускаются параллельно по матрице. Кэширование node_modules и таргетированных wheel-пакетов дополнительно ускорило итерации разработчиков.

Наконец, матрица — это инструмент. Правильная стратегия помогает сделать CI быстрым, предсказуемым и экономичным, а не просто парадом параллельных прогонов. Постепенно отбирая оси и оптимизируя кэш, вы получите стабильную систему проверки, которая будет масштабироваться вместе с проектом.