Как упростить GitLab CI: семь приёмов для настройки пайплайна
Разберём семь возможностей GitLab CI, доступных без платной подписки, от простого к сложному: начнём с того, как сделать граф пайплайна компактнее, и закончим распределением тестов между параллельными задачами.
Пайплайн успешно завершился, несмотря на найденные линтером ошибки. Тесты прошли дважды: для процента покрытия и для XML-отчёта завели разные задачи, и каждая заново прогоняет весь набор тестов. А два соседних пайплайна одновременно начали обновлять одно окружение. Такие ситуации требуют разных исправлений: настроить статусы проверок, убрать повторную работу и ограничить одновременный доступ к ресурсу.
Примеры в статье независимы: их можно адаптировать по отдельности. Для Python-задач ниже используются каталоги my_app и tests, а команды установки зависимостей показаны прямо в YAML.
Как свернуть похожие задачи в графе пайплайна
Когда в пайплайне становится много похожих задач — например, из-за ручного дублирования или распараллеливания, — граф может разрастись в длинный список строк. Если несколько задач выполняют похожие проверки, GitLab умеет объединять их в визуальную группу по именам. Для этого подойдёт общий префикс и нумерация X/Y:
В обычном графе три задачи свернутся в группу checks, которую можно раскрыть для просмотра отдельных результатов. В мини-графах GitLab такую группировку не применяет.
Этот приём меняет только представление. Задачи сохраняют свои команды, статусы и требования к раннерам. Они не начинают выполняться быстрее и не получают общую блокировку из-за совпадения префикса. Для экземпляров, созданных через числовой parallel, GitLab сам добавляет нумерацию к именам.
Как поставить изменения общего ресурса в очередь
Несколько пайплайнов могут выполняться одновременно — например, из разных веток или мёрж-реквестов (Merge Request). Если они меняют одно и то же окружение, это может привести к конфликту: пока первый пайплайн применяет инфраструктурную конфигурацию, второй пытается сделать то же самое.
Resource group позволяет выполнять задачи с одним ключом по очереди в пределах проекта. Например, для проекта с Terraform-кодом в каталоге infra можно настроить ручной запуск:
Этот фрагмент рассчитан на раннер с установленным Terraform и уже настроенным доступом к инфраструктуре и её удалённому бэкенду. Оператор запускает задачу вручную; параметр auto-approve позволяет команде apply завершиться без интерактивного подтверждения внутри CI.
Если задача с ключом terraform-production уже выполняется, другая задача с таким же ключом в этом проекте будет ждать освобождения ресурса. Однако очередь не защищает от terraform apply, запущенного с ноутбука, из другого проекта или из задачи без нужного resource_group. Блокировка состояния на стороне Terraform по-прежнему нужна.
Порядок очереди тоже настраивается отдельно. Режим по умолчанию unordered не гарантирует, что первым пойдёт старейший пайплайн. Для обработки по возрастанию идентификаторов пайплайнов выберите oldest_first через API ресурсных групп. Это порядок пайплайнов, который не следует путать с датами коммитов.
Как разрешить ожидаемый код ошибки и сохранить остальные проверки
На этапе внедрения линтера команда может решить, что найденные нарушения пока не должны блокировать пайплайн. Если добавить к команде конструкцию || true, оболочка скроет любой ненулевой код завершения, включая ошибку запуска инструмента.
Более точное правило задаёт allow_failure:exit_codes. Например, Ruff возвращает код 1 при найденных нарушениях, а код 2 при нештатном завершении, в том числе из-за неверной конфигурации:
При коде 1 GitLab отметит задачу предупреждением и позволит пайплайну продолжиться. Код 2 останется обычной ошибкой и повлияет на итоговый статус. Успех всего пайплайна при этом зависит и от остальных задач.
Разрешайте только те коды, значение которых известно для выбранного инструмента. У другого линтера код 1 может означать совсем другую проблему. Когда команда сделает проверку обязательной, настройку allow_failure можно удалить, сохранив ту же команду запуска.
Как добавить выбор параметра при ручном запуске
В существующем пайплайне переменная может управлять режимом проверки: запустить тесты или дополнительно вывести покрытие. Чтобы человек не вводил значение по памяти, задайте список options для переменной:
На странице ручного запуска GitLab покажет список с описанием. Значение value должно входить в options. Оно задаёт значение по умолчанию, но переменную могут переопределить другие источники с более высоким приоритетом. Поэтому скрипт отдельно отклоняет неизвестный режим.
Options полезен для интерфейса запуска пайплайна, который уже использует переменные. Для новых параметров рассмотрите CI/CD inputs: они задают типы и проверяют ограничения при создании пайплайна. Выбор между этими механизмами зависит от того, как параметр должен проверяться и использоваться, а не от размера YAML-файла.
Как переиспользовать часть конфигурации из другого файла
После настройки выполнения часто обнаруживается другой источник работы: одинаковые команды установки зависимостей скопированы в несколько задач. Изменение приходится повторять в каждом месте, и конфигурации постепенно расходятся.
Тег !reference позволяет взять выбранный блок из другой задачи или скрытого шаблона, в том числе из файла, подключённого через include. Например, вынесите подготовку Python-окружения в .ci/python.yml:
В основном .gitlab-ci.yml подключите файл и возьмите из шаблона только before_script:
Начальная точка в имени .python_setup делает его скрытым шаблоном: отдельную задачу GitLab для него не запускает. Остальные параметры unit_tests остаются рядом с самой задачей.
YAML-якоря действуют только внутри файла, где объявлены. При этом extends тоже умеет работать с подключёнными файлами: он подходит для наследования общей конфигурации задачи. !reference удобен, когда нужен конкретный раздел. Если для понимания одной команды приходится переходить через несколько вложенных ссылок, такая экономия строк уже затрудняет сопровождение.
Как получать два отчёта о покрытии тестами за один прогон
GitLab поддерживает два независимых механизма отображения покрытия кода тестами. Ключ coverage извлекает общий процент из лога задачи. Настройка artifacts:reports:coverage_report передаёт XML-отчёт, по которому GitLab отмечает покрытые и непокрытые строки в изменениях мёрж-реквеста. Эти механизмы можно подключить к одной задаче.
Для проверки создайте небольшой модуль my_app/calculator.py:
В tests/test_calculator.py добавьте проверки сложения и деления:
Теперь настройте задачу в .gitlab-ci.yml. Плагин pytest-cov умеет формировать несколько отчётов за один запуск:
Команда python -m pytest --cov=my_app --cov-report=term-missing --cov-report=xml:coverage.xml tests/ выводит покрытие в терминал и сохраняет тот же результат в coverage.xml. Регулярное выражение для pytest-cov забирает процент из строки TOTAL. В рабочем проекте закрепите версии зависимостей в принятом у вас файле требований: изменение формата вывода может потребовать правки регулярного выражения.
Чтобы увидеть построчную разметку, добавьте эти файлы в отдельной ветке и откройте мёрж-реквест. После завершения пайплайна GitLab обработает отчёт и покажет аннотации в изменённых файлах. В примере непокрытой останется строка return внутри unused_function. Само объявление функции выполняется при импорте модуля, поэтому считать строку def заведомо непокрытой было бы ошибкой.
Процент помогает заметить изменение объёма проверяемого кода, а построчная разметка указывает, где искать пробелы. Ни один из этих показателей не проверяет, насколько содержательны утверждения assert в тестах.
Как распределить тесты между параллельными задачами
Когда отчёты, статусы и конфигурация задач настроены, можно разбирать время выполнения. Ключ parallel создаёт несколько экземпляров задачи. Для каждого GitLab задаёт номер CI_NODE_INDEX и общее количество CI_NODE_TOTAL. Распределение самих тестов должен выполнить тестовый инструмент: обычная команда pytest в каждом экземпляре повторит весь набор.
В Python эту работу можно поручить плагину pytest-split. Сначала локально выполните полный прогон и сохраните длительности тестов:
Плагин создаст файл .test_durations. Добавьте его в репозиторий, чтобы все экземпляры CI-задачи использовали одинаковые данные. Затем задайте три группы:
GitLab создаст три задачи, а pytest-split выберет для каждой её часть набора. Когда состав и длительности тестов заметно изменятся, обновите .test_durations. Наборы с зависимостью от порядка запуска сначала нужно подготовить к разделению.
Для одновременного выполнения нужны свободные раннеры или несколько доступных слотов на одном раннере. Если мощности не хватает, часть задач будет ждать. Время также зависит от самой долгой группы, подготовки окружения и установки зависимостей, поэтому трёхкратное увеличение числа задач не обещает трёхкратного ускорения.
Если нужно проверить один набор тестов на разных версиях Python, используйте parallel:matrix:
Здесь версия меняется: её значение подставляется в имя образа, и GitLab запускает каждую задачу в своём контейнере с нужным Python. Если добавить переменную OS, она сама по себе не переключит операционную систему. Для Windows или macOS нужны подходящие раннеры и их выбор, например через теги, заданные переменными матрицы. Команды задачи тоже должны работать в выбранной среде.
Что проверить после изменения конфигурации
Перед переносом настроек в основной пайплайн:
- Прогоните конфигурацию через CI Lint с подключаемыми файлами. Проверьте правила создания задач для push, мёрж-реквеста и ручного запуска.
- Убедитесь, что каждый тест попадает в нужную группу, а самая долгая группа не сводит выигрыш от параллельности к минимуму. Сравните полное время пайплайна с исходным.
- Проверьте логи и XML-отчёт покрытия на заведомо непокрытой строке. Найденный процент и построчная разметка должны описывать один прогон.
- Вызовите разрешённую ошибку линтера и отдельно ошибку его конфигурации. Убедитесь, что пайплайн различает эти случаи.
- На тестовом окружении запустите два пайплайна с общей ресурсной группой. Проверьте ожидание ресурса и выбранный порядок обработки.
- Передайте неподдерживаемое значение TEST_MODE при запуске. Задача должна явно отклонить его, не переходя к тестам или другим действиям.
