Матричные сборки
New

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

Используя матричные сборки, вы можете:

  1. Эффективно тестировать код на множестве платформ, операционных систем или версий языков программирования, минимизируя при этом дублирование конфигураций.
  2. Сократить объем кода, делая его более чистым и поддерживаемым за счет параметризации задач.
  3. Динамически управлять созданием и выполнением задач, добавляя или исключая специфические комбинации конфигураций, или даже формируя матрицу на основе данных из внешних событий.
  4. Детально контролировать поведение задач при возникновении ошибок, определяя, должны ли все параллельные задачи быть немедленно отменены или же конкретные задачи могут завершаться с ошибкой без прерывания всего процесса.
  5. Управлять параллелизмом выполнения задач, что помогает оптимизировать использование ресурсов раннеров и предотвращать их перегрузку.

Матричные стратегии выполнения задач

jobs.<job_id>.strategy и jobs.<job_id>.strategy.matrix — это ключевые элементы в файлах конфигурации рабочих процессов, используемые для реализации матричной стратегии выполнения задач.

jobs.<job_id>.strategy является общей декларацией использования стратегии, а jobs.<job_id>.strategy.matrix — конкретным определением параметров для создания множества вариаций задач.

jobs.<job_id>.strategy, стратегия выполнения задач

Секция jobs.<job_id>.strategy в конфигурации конкретной задачи (<job_id>) указывает стратегию выполнения. Эта секция содержит настройки того, как будут генерироваться и управляться множественные запуски.

jobs.<job_id>.strategy.matrix, матричная стратегия

jobs.<job_id>.strategy.matrix используется для определения матрицы различных конфигураций задач. В матрице можно задать одну или несколько переменных, каждой из которых присваивается массив значений.

Подсекция jobs.<job_id>.strategy.matrix внутри strategy определяет саму матрицу конфигураций. Здесь перечисляются переменные и массивы значений для каждой из них:

  • одну или несколько переменных (например, python_version, runner);
  • для каждой переменной указывается массив значений;
  • CI/CD берет все возможные комбинации этих значений и для каждой уникальной комбинации создает отдельный запуск (job run) задачи.

Пример:

on:
  push:
 
jobs:
  build_and_test: # Название задачи
    strategy:
      matrix: # Определение матрицы
        runner: ["ubuntu-cloud-runner"] # Раннеры
        python_version: ["3.8", "3.9", "3.10"] # Версии Python
    runs-on: ${{ matrix.runner }} # Использование раннера из матрицы
    steps:
      - name: Установить Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python_version }} # Используем версию Python из матрицы

Ограничения и логика работы матрицы

  1. Ограничение — матрица может генерировать максимум 256 задач на рабочий процесс.
  2. Для каждой возможной комбинации переменных будет запущена отдельная задача. Например, если у вас есть переменная version со значениями [10, 12, 14] и переменная os со значениями [ubuntu-latest, windows-latest], рабочий процесс выполнит 6 задач (3 версии * 2 ОС), то GitVerse по умолчанию максимизирует количество параллельно выполняемых задач в зависимости от доступности раннеров.
  3. Порядок создания задач определяется порядком переменных в матрице — первая указанная переменная создает первую задачу в рабочем процессе.
  4. Доступ к переменным: определяемые переменные становятся свойствами в контексте matrix. Вы можете ссылаться на них в других частях файла, используя синтаксис matrix.<имя_переменной>. Например, matrix.version и matrix.os позволят получить текущие значения переменных version и os, которые используются конкретным заданием.

Примеры использования матриц

Одномерная матрица

Одномерная матрица используется для запуска однотипных задач с разными значениями одной характеристики:

on:
  push:
 
jobs:
  example_job:
    runs-on: ubuntu-cloud-runner
    strategy:
      matrix:
        python_version: ['3.8', '3.9', '3.12']
    steps:
      - name: Install Python
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python_version }}

Этот рабочий процесс устанавливает переменную python_version, содержащую список версий Python (['3.8', '3.9', '3.12']), и запускает задачу для каждой указанной версии. Для каждой итерации используется соответствующая версия Python через синтаксис ${{ matrix.python_version }}, передаваемая действию установки Python (actions/setup-python).

Многомерная матрица

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

on:
  push:
jobs:
  test_environment:
    strategy:
      matrix:
        architecture: ['x64', 'x86']
        python_version: ['3.8', '3.9', '3.12']
    runs-on: ubuntu-cloud-runner
    steps:
      - name: Install Python
        run: |
          echo "Install ${{ matrix.architecture }} python ${{ matrix.python_version }}"

Здесь демонстрируется использование многомерной матрицы для тестирования среды с разными комбинациями платформ и версий Python:

  • architecture включает две значения — 'x86' и 'x64', определяющие целевые архитектуры для запуска тестов;
  • python_version задает три версии Python — '3.8', '3.9', '3.12'.

Таким образом, запустится шесть задач, выполняя каждую возможную комбинацию указанных архитектур и версий Python.

Матрица с массивом объектов

Переменная в матрице может быть определена как массив объектов. Это позволяет включать дополнительные, специфичные для каждой комбинации параметры:

matrix:
  environment:
    - name: production
    - name: staging
  database:
    - type: postgresql
      version: '13'
    - type: mysql
      version: '8.0'
      config: innodb_buffer_pool_size=1GB

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

  • environment — определяет два окружения (production и staging);
  • database — содержит конфигурации для PostgreSQL и MySQL, включая дополнительные настройки для конкретной версии базы данных.

Для каждого сочетания значений из этих матриц создаются уникальные задачи. Например, конфигурация задачи для MySQL 8.0 дополнительно включает настройку размера буферного пула InnoDB.

Всего создается четыре уникальных контекста для выполнения задач:

- matrix.environment.name: production
  matrix.database.type: postgresql
  matrix.database.version: '13'
- matrix.environment.name: production
  matrix.database.type: mysql
  matrix.database.version: '8.0'
  matrix.database.config: innodb_buffer_pool_size=1GB
- matrix.environment.name: staging
  matrix.database.type: postgresql
  matrix.database.version: '13'
- matrix.environment.name: staging
  matrix.database.type: mysql
  matrix.database.version: '8.0'
  matrix.database.config: innodb_buffer_pool_size=1GB

Расширение и исключение конфигураций

jobs.<job_id>.strategy.matrix.include, расширение матрицы

Значение include представляет собой список объектов. Используйте jobs.<job_id>.strategy.matrix.include для расширения существующих матричных конфигураций или добавления совершенно новых:

  1. Добавление к существующим — пары ключ:значение из объекта include добавляются в каждую комбинацию матрицы, если они не перезаписывают оригинальные значения матрицы.
  2. Создание новых комбинаций — если объект из include нельзя добавить ни к одной существующей комбинации без перезаписи, он создает новую комбинацию матрицы.

Важные правила:

  1. Оригинальные значения матрицы не перезаписываются. Это означает, что если в базовой матрице уже есть определенное значение, include не изменит его.
  2. Добавленные значения матрицы могут быть перезаписаны другими значениями из того же или последующих объектов include.
  3. Невозможные для добавления комбинации создают новые варианты выполнения.

Все комбинации include обрабатываются после exclude. Это означает, что вы можете сначала исключить ненужные конфигурации через exclude, а затем добавить обратно некоторые из них (или новые) через include, если это необходимо.

Пример: работа с include

on:
  push
 
strategy:
  matrix:
    city: [moscow, saint_petersburg]
    transport: [bus, train]
    include:
      - weather: sunny
      - weather: rainy
        transport: bus
      - city: moscow
        population: large
      - city: yekaterinburg
        transport: train

В результате будет создано 6 задач со следующими комбинациями:

  • city: moscow, transport: bus, weather: rainy, population: large;
  • city: moscow, transport: train, population: large;
  • city: saint_petersburg, transport: bus, weather: rainy;
  • city: saint_petersburg, transport: train;
  • weather: sunny;
  • city: yekaterinburg, transport: train.

Логика работы директивы include

Данный пример показывает работу механизма include для расширения возможностей стандартной матричной стратегии. Исходная матрица состоит из городов (city) и видов транспорта (transport), к которым мы добавляем различные условия с помощью блока include.

Пошаговая логика обработки:

  1. Базовые комбинации. Изначально получаем 4 базовых набора:
   {city: moscow, transport: bus}
   {city: moscow, transport: train}
   {city: saint_petersburg, transport: bus}
   {city: saint_petersburg, transport: train}
  1. Создание нового набора:{weather: sunny} без совпадения с имеющимися, добавится отдельно, итоговые наборы теперь выглядят следующим образом
{city: moscow, transport: bus, weather: sunny}
{city: moscow, transport: train, weather: sunny}
{city: saint_petersburg, transport: bus}
{city: saint_petersburg, transport: train}
{weather: sunny}
  1. Уточнение условий. Следующая запись {weather: rainy, transport: bus} применяет условие только к задачам, использующим транспорт bus.

  2. Дополнение новой информацией. Затем идет правило {city: moscow, population: large}, которое устанавливает свойство population только для города Москва, остальные базовые правила остаются неизмененными.

  3. Создание нового набора. Если объект не совпадает ни с одной базовой комбинацией, он автоматически становится отдельной задачей. Так, сочетание {city: yekaterinburg, transport: train} генерирует новую строку.

Итоговая последовательность задач выглядит следующим образом:

- {city: moscow, transport: bus, weather: rainy, population: large}
- {city: moscow, transport: train, population: large}
- {city: saint_petersburg, transport: bus, weather: rainy}
- {city: saint_petersburg, transport: train}
- {weather: sunny}
- {city: yekaterinburg, transport: train}

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

Пример: немного логики и снова include

Представьте, что нам нужно протестировать приложение на двух языках программирования (Python, JavaScript):

on:
  push:
 
jobs:
  test_applications:
    strategy:
      matrix:
        os: [linux, windows, macos]
        language: [python, javascript]
        include:
          - os: linux
            language: python
            additional_test: database_tests
          - os: macos
            language: javascript
            additional_test: performance_tests
    runs-on: ubuntu-cloud-runner
    steps:
      - name: Setup Environment
        run: |
          echo "Use ${{ matrix.language }}"
      - name: Standard Test Suite
        run: |
          echo "Running standard tests..."
      - if: ${{ matrix.additional_test }}
        name: Additional Test Suite
        run: |
          echo "Running additional ${{ matrix.additional_test }}..."

Базовая матрица состоит из шести комбинаций (три ОС × два языка программирования).

Директива include добавляет две специальные комбинации:

Ubuntu + Python → дополнительно запускаются тесты базы данных (database_tests).

Windows + JavaScript → дополнительно запускаются тесты производительности (performance_tests).

Эти дополнительные тесты выполняются только для указанных случаев благодаря условному выполнению шага if: ${{ matrix.additional_test }}.

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

jobs.<job_id>.strategy.matrix.exclude, исключение конфигураций

Используйте jobs.<job_id>.strategy.matrix.exclude для исключения конкретных конфигураций из матрицы. Для исключения достаточно частичного совпадения конфигурации — не обязательно указывать все параметры.

Все комбинации exclude обрабатываются перед include. Это означает, что вы можете сначала исключить ненужные конфигурации через exclude, а затем добавить обратно некоторые из них (или новые) через include, если это необходимо.

Пример исключения конфигураций

on:
  push:
 
jobs:
  display_combinations:
    strategy:
      matrix:
        region: [us-east, eu-central]
        instance_type: [small, medium, large]
        service: [web-app, api-gateway]
        exclude:
          - region: us-east
            instance_type: small
            service: web-app
          - region: eu-central
            instance_type: large
    runs-on: ubuntu-cloud-runner
    steps:
      - name: Display Combinations
        run: |
          echo "Region: ${{ matrix.region }}, Instance Type: ${{ matrix.instance_type }}, Service: ${{ matrix.service }}"

Получим следующую последовательность комбинаций, исключая указанные в разделе exclude:

  • Region: us-east, Instance Type: small, Service: api-gateway;
  • Region: us-east, Instance Type: medium, Service: web-app;
  • Region: us-east, Instance Type: medium, Service: api-gateway;
  • Region: us-east, Instance Type: large, Service: web-app;
  • Region: us-east, Instance Type: large, Service: api-gateway;
  • Region: eu-central, Instance Type: small, Service: web-app;
  • Region: eu-central, Instance Type: small, Service: api-gateway;
  • Region: eu-central, Instance Type: medium, Service: web-app;
  • Region: eu-central, Instance Type: medium, Service: api-gateway.

Таким образом, наглядно продемонстрирована работа директивы exclude: исключенные комбинации не попадают в выполнение рабочего процесса, и остаются только валидные наборы параметров.

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

Управление параллельным выполнением

По умолчанию GitVerse максимизирует количество параллельно выполняемых задач в зависимости от доступности runner'ов. Это означает, что если у вас достаточно ресурсов, все задачи из матрицы могут быть запущены одновременно.

Для ограничения максимального количества одновременно выполняемых задач при использовании матричной стратегии используйте параметр:

jobs.<job_id>.strategy.max-parallel, ограничение параллельного выполнения

Этот параметр позволяет вам задать максимальное число задач из матрицы, которые могут выполняться параллельно в любой момент времени. Это полезно, например, для управления нагрузкой на self-hosted раннеры или для соблюдения ограничений на ресурсы.

Пример ограничения параллельного выполнения:

on:
  push:
 
jobs:
  parallel_matrix:
    runs-on: ubuntu-cloud-runner
    strategy:
      max-parallel: 2 # сейчас это максимальное количество одновременных задач на GitVerse
      matrix:
        version: [10, 12, 14]
        os: [ubuntu-latest, windows-latest]
    steps:
      - name: Show OS version
        run: |
          echo "OS: ${{ matrix.os }}"
          echo "Version: ${{ matrix.version }}"

В этом примере, несмотря на то что матрица сгенерирует 6 комбинаций (3 версии × 2 ОС), одновременно будут выполняться не более 2 задач. Остальные задачи будут ждать в очереди, пока не освободятся ресурсы.