Всем привет!

В первой части мы в общих чертах посмотрели на различия GitHub Actions и GitLab, а также начали разбирать структуру файла .gitlab-ci.yml.

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

Не понимаете, «что тут происходит»? Рекомендую начать с первой части:: по ссылке.

Если вам интересны подобные материалы, подписывайтесь на Telegram-канал «Код на салфетке». Там я делюсь гайдами для новичков, полезными инструментами и практическими примерами из реальных проектов. А прямо сейчас у нас там ещё и проходит новогодний розыгрыш.


Триггеры и условия запуска Pipeline

Теперь, когда вы понимаете структуру конфигурационного файла, разберёмся с когда и при каких условиях запускается pipeline. Это жизненно важно: от корректных условий зависит, сколько ресурсов тратится и сколько лишних сборок вы видите в логах.

Что такое триггер

Триггер — это событие или условие, из-за которого GitLab начинает выполнение pipeline. Это может быть push, создание/обновление Merge Request, расписание, ручной запуск, внешнее HTTP-событие и т. п.

Раздел rules — условия запуска job’ов (современный подход)

rules — это основной и рекомендуемый способ управлять тем, будет ли job добавлен в pipeline. Именно он пришёл на смену only и except и сегодня используется практически во всех новых конфигурациях.

Ранее для этого использовались only и except — простые директивы, которые ограничивали запуск job’а по веткам, тегам или типу pipeline. Они работали, но были негибкими и плохо масштабировались, поэтому в современных конфигурациях считаются устаревшими и заменены rules.

Важно понимать ключевую идею:

rules решают появится ли job в pipeline вообще, а не просто когда он запустится.

Если ни одно правило не сработало — job даже не будет добавлен в pipeline.

Базовый синтаксис rules

job_name:
  stage: test
  script:
    - echo "test"
  rules:
    - if: condition
      when: always
    - when: never

Каждый элемент в rules — это правило, которое GitLab проверяет сверху вниз.

Основные поля:

  • if — условие (логическое выражение, результат true или false).

  • when — что делать, если условие выполнилось.

Чаще всего используются значения when:

  • always — job добавляется и запускается автоматически.

  • never — job не добавляется.

  • manual — job добавляется, но ждёт ручного запуска.

  • delayed — отложенный запуск (реже используется, разберём позже).

Первое сработавшее правило останавливает проверку остальных.

Практические сценарии использования rules

Проверка ветки

Самый частый сценарий — запуск job только для определённой ветки:

test_main_only:
  stage: test
  script:
    - npm test
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always
    - when: never

Если ветка не main, второе правило гарантирует, что job не появится в pipeline.

Совет: вместо "main" используйте $CI_DEFAULT_BRANCH, чтобы конфиг был переносимым между проектами:

build_main:
  stage: build
  script: 
    - echo "Сборка main"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: always
    - when: never

Проверка тега

Теги чаще всего используют для релизов:

release_build:
  stage: build
  script:
    - npm run build
  rules:
    - if: '$CI_COMMIT_TAG'
      when: always

$CI_COMMIT_TAG содержит имя тега или пустую строку. Если pipeline запущен не из тега, условие просто не выполнится.

Проверка источника pipeline

Иногда важно понимать как именно был запущен pipeline:

scheduled_job:
  stage: test
  script:
    - echo "Running scheduled job"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: always
    - when: never

Возможные значения $CI_PIPELINE_SOURCE:

  • push — обычный git push.

  • merge_request_event — событие Merge Request.

  • schedule — запуск по расписанию.

  • web — ручной запуск из интерфейса.

  • api — запуск через API.

  • trigger — запуск из другого pipeline.

Срабатывание на Merge Request

Чтобы запускать job при создании или обновлении Merge Request:

test_on_mr:
  stage: test
  script:
    - npm test
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: always
    - when: never

Пропуск job по сообщению коммита

skip_on_draft:
  stage: test
  script:
    - npm test
  rules:
    - if: '$CI_COMMIT_MESSAGE =~ /\[skip ci\]/'
      when: never
    - when: always

Если в сообщении коммита есть [skip ci], job не добавится. (GitLab умеет полностью пропускать pipeline по этому флагу, но здесь показан пример именно в rules.)

Проверка наличия файлов (exists)

build_only_on_src_change:
  stage: build
  script:
    - npm run build
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      exists:
        - src/**/*
      when: always
    - when: never

exists проверяет, есть ли файлы в репозитории. Это полезно, если структура проекта может отличаться, например в монорепозитории.

Фильтрация по изменённым файлам (changes)

Один из самых эффективных способов сократить время pipeline в больших проектах — запуск job только если изменились нужные файлы:

test_backend:
  stage: test
  script:
    - cd backend && pytest
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      changes:
        - backend/**/*
        - shared/**/*
      when: always
    - when: never

changes проверяет дифф коммита, а не наличие файлов.

Логика AND / OR в rules

В if можно использовать логические операторы AND (&&) и OR (||):

deploy_to_prod:
  stage: deploy
  script:
    - ./deploy.sh
  rules:
    # Тег ИЛИ ветка main (автодеплой)
    - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH == "main"'
      when: always

    # Merge Request в main — но вручную (AND)
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
      when: manual

    # Всё остальное — запрещено
    - when: never

Здесь хорошо видно силу rules:

  • Автоматический деплой при пуше тега или при пуше в main.

  • Ручной деплой из Merge Request, который нацелен в main.

  • Всё остальное — запрещено.

Основные триггеры pipeline

Push — самый частый триггер

Pipeline запускается автоматически при git push в репозиторий (по умолчанию — для веток и тегов). Простой job, который выполняется на любой push:

build_on_push:
  stage: build
  script:
    - echo "Запуск на любом push"

Если вам нужно ограничить запускаемые ветки — используйте rules (это современный и гибкий способ; only/except сейчас считаются устаревшими и рекомендуется переходить на rules).

Современный пример фильтра по ветке (через rules):

build_main:
  stage: build
  script: 
    - echo "Сборка main"
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: on_success
    - when: never

Этот rules-блок гарантирует, что job добавится в pipeline только если текущая ветка совпадает с дефолтной ($CI_DEFAULT_BRANCH), а во всех остальных случаях job не будет добавлен.

Merge Request (MR)

Чтобы запускать job при создании или обновлении Merge Request, чаще всего используют rules с переменной источника пайплайна. Пример:

test_on_mr:
  stage: test
  script:
    - npm test
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: on_success
    - when: never

Такой job будет добавлен в pipeline при событии Merge Request. Раньше для этого использовали only: - merge_requests, но rules даёт больше контроля и гибкости.

Schedule — запуск по расписанию (cron)

Когда задача должна запускаться регулярно (ночная сборка, регу��ярная проверка зависимостей, бэкап и т. п.), используйте Pipeline schedules. Сchedules настраиваются в интерфейсе GitLab (CI/CD → Schedules) и принимают cron-выражения. В расписании вы указываете описание, cron, ветку и опциональные переменные. GitLab Документация

Примеры cron-выражений (основы):

  • 0 2 * * * — ежедневно в 02:00.

  • 0 * * * * — каждый час.

  • 0 0 * * 0 — каждое воскресенье в 00:00.

  • 0 0 1 * * — первого числа каждого месяца.

Если требуется программный доступ к расписаниям, есть REST API для управления pipeline schedules (создание, редактирование, запуск).

Manual trigger — ручной запуск

Если хотите, чтобы job не запускался автоматически, а появлялся как кнопка «Run» в интерфейсе, используйте when: manual:

deploy_to_prod:
  stage: deploy
  when: manual
  script:
    - ./scripts/deploy-to-production.sh

Это удобно для развертываний в production: вы контролируете момент запуска. Job с when: manual не стартует автоматически — его нужно запустить вручную из UI либо через API.

Webhook — запуск со стороны внешних событий

В GitLab можно настроить Webhooks (в проекте: Settings → Integrations → Webhooks), чтобы отправлять уведомления о событиях внешним системам. Сам GitLab также может выполнять вызовы по URL при изменениях; в обратную сторону (чтобы внешний сервис запускал пайплайн) обычно используют API или pipeline triggers (токены), описанные ниже.

API / Trigger tokens — запуск программно

Пайплайн можно запустить программно через API — либо стандартным endpoint-ом pipeline, либо через pipeline trigger (токен) для более безопасной интеграции:

curl --request POST \
  --header "PRIVATE-TOKEN: <your_access_token>" \
  "https://gitlab.example.com/api/v4/projects/<project_id>/pipeline" \
  --form "ref=main"

Или через pipeline trigger token: POST /projects/:id/trigger/pipeline. Это удобно для интеграций: CI запускается из внешних систем, job'ы могут получать переменные через параметры запроса.

Короткие практические советы

  • Рекомендуется использовать rules вместо only/except. rules даёт больше контроля (условия, проверки переменных, changes и т. п.). only/except поддерживаются, но к ним уже не добавляют новые возможности — лучше перейти на rules.

  • Не захардкодьте main — используйте $CI_DEFAULT_BRANCH. Это делает конфиг переносимым между проектами.

  • Для расписаний используйте UI или API, и не забывайте про временную зону (cron-timezone в API).

  • Если внешняя система должна запускать пайплайн — используйте trigger token или API, а не «подделывайте» webhooks вручную. Это безопаснее и проще в управлении прав.

Раздел workflow — условия для всего pipeline

Помните, во первой части мы вскользь упоминали workflow? Самое важное, что нужно про него запомнить: workflow управляет запуском pipeline целиком, а не отдельных job’ов.

Если условия workflow не выполняются — pipeline вообще не создаётся. Ни один job даже не появится в графе.

Пример:

workflow:
  rules:
    # Не запускаем pipeline для draft Merge Request
    - if: '$CI_MERGE_REQUEST_DRAFT == "true"'
      when: never

    # Запускаем pipeline для Merge Request
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

    # Запускаем pipeline для main-ветки
    - if: '$CI_COMMIT_BRANCH == "main"'

    # Запускаем pipeline для тегов
    - if: '$CI_COMMIT_TAG'

    # Всё остальное пропускаем (например, feature-ветки)
    - when: never

Что здесь происходит:

  • Draft Merge Request — pipeline не создаётся вообще.

  • Обычный Merge Request — pipeline создаётся.

  • Push в main — pipeline создаётся.

  • Push тега — pipeline создаётся.

  • Любые другие сценарии (например, push в feature/*) — pipeline пропускается.

Обратите внимание: правила проверяются сверху вниз, и первое сработавшее правило останавливает дальнейшую проверку — ровно так же, как и в rules у job’ов.

Когда нужен workflow, а когда — rules у job’ов

Это частый вопрос у новичков, поэтому зафиксируем разницу явно.

Разница между workflow: rules и rules у job’ов:

  • workflow rules — если условие не выполнено, pipeline не создаётся целиком.

  • job rules — pipeline создаётся, но конкретный job может быть пропущен.

Проще говоря:

  • workflow — отвечает за вопрос: «Нужен ли нам pipeline вообще?».

  • rules у job’ов — «Нужен ли этот job внутри уже существующего pipeline?».

На практике это часто комбинируют:
workflow отсеивает лишние пайплайны (экономия времени и ресурсов), а rules тонко управляют тем, какие job’ы выполняются внутри.

Переменные для использования в условиях

В условиях if вы почти всегда будете опираться на встроенные переменные GitLab CI. Ниже — самые часто используемые, которых обычно хватает для 90% сценариев:

Переменная

Описание

Пример значения

$CI_COMMIT_BRANCH

Имя текущей ветки

main, develop, feature/new-ui

$CI_COMMIT_TAG

Имя тега (если pipeline из тега)

v1.0.0, release-2025-12-01

$CI_COMMIT_SHA

Хеш текущего коммита

abc123def456...

$CI_COMMIT_MESSAGE

Сообщение коммита

Fix: update dependencies

$CI_PIPELINE_SOURCE

Источник запуска pipeline

push, merge_request_event, schedule

$CI_MERGE_REQUEST_IID

ID Merge Request (в рамках проекта)

42

$CI_MERGE_REQUEST_TARGET_BRANCH_NAME

Целевая ветка Merge Request

main

$CI_MERGE_REQUEST_DRAFT

Является ли MR черновиком (draft)

true, false

$CI_PROJECT_NAME

Имя проекта

my-awesome-app

$CI_PROJECT_PATH

Полный путь проекта

group/subgroup/my-project

Небольшие, но важные нюансы:

  • $CI_COMMIT_BRANCH может быть пустым в некоторых типах pipeline (например, в pipeline, созданном только для Merge Request). В таких случаях стоит использовать $CI_COMMIT_REF_NAME, если вам нужно универсальное имя ref.

  • $CI_COMMIT_TAG — это строка. Если pipeline не теговый, переменная либо пустая, либо не определена — этого достаточно для проверки в if.

  • Переменные MR ($CI_MERGE_REQUEST_*) доступны только в pipeline, связанных с Merge Request.

Регулярные выражения в условиях (regex)

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

В GitLab CI для этого используется оператор =~:

if: '$VARIABLE =~ /pattern/'

Важно:

  • Справа всегда используется regex в слэшах /.../.

  • Слева — строковая переменная GitLab CI.

  • Выражение возвращает true или false.

Пример: релизные теги

Частый кейс — запуск job только для релизных тегов вида v1.2.3:

job_for_release:
  stage: deploy
  script:
    - echo "Deploying release version"
  rules:
    # Теги вида v1.2.3
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
      when: always
    - when: never

Разберём регулярное выражение:

  • — строка начинается сv.

  • \d+ — одна или несколько цифр.

  • \. — точка (экранируется).

  • $ — конец строки.

Таким образом:

  • v1.2.3 → подходит.

  • v2.0 → не подходит.

  • release-1.2.3 → не подходит.

Пример: hotfix-ветки

Если в команде принято заводить hotfix-ветки с префиксом hotfix/, это легко описывается через regex:

job_for_hotfix:
  stage: deploy
  script:
    - echo "Deploying hotfix"
  rules:
    - if: '$CI_COMMIT_BRANCH =~ /^hotfix\/.+/'
      when: always
    - when: never

Здесь:

  • / — ветка начинается с hotfix/.

  • .+ — дальше идёт любое имя (не пустое).

Подойдут, например:

  • hotfix/login-bug.

  • hotfix/urgent-fix-123.

Пример: фильтрация по сообщению коммита

Регулярные выражения особенно полезны для анализа сообщений коммитов:

job_skip_wip:
  stage: test
  script:
    - npm test
  rules:
    # Пропускаем job, если коммит помечен как WIP / Draft / skip ci
    - if: '$CI_COMMIT_MESSAGE =~ /(WIP|Draft|\[skip ci\])/'
      when: never
    - when: always

Здесь:

  • (WIP|Draft|\[skip ci\]) — логическое «или».

  • Квадратные скобки экранированы, потому что в regex они имеют специальное значение.

Такой подход помогает:

  • Не запускать тесты для черновых коммитов.

  • Экономить время CI.

  • Делать поведение pipeline более предсказуемым.

Когда и зачем использовать when: never

Очень распространённый паттерн:

job:
  rules:
    - if: some_condition
      when: always
    - when: never

На первый взгляд when: never кажется избыточным. Но на практике это важная и полезная привычка.

Что тут описано:

  • Если some_conditiontrue → job добавляется в pipeline.

  • Во всех остальных случаях → job не добавляется вообще.

Без when: never:

job:
  rules:
    - if: some_condition
      when: always
    # Для остальных случаев: что делать? Неясно!

Поведение становится менее очевидным, особенно для тех, кто будет читать конфиг позже. Формально GitLab просто не добавит job, но это не всегда сразу понятно из кода.

Поэтому when: never в конце:

  • Делает логику явной.

  • Упрощает чтение конфигурации.

  • Снижает риск ошибок при доработках.

Хорошее правило:
если вы используете rules, почти всегда заканчивайте их - when: never, если нет причины для другого поведения.

$CI_DEBUG_TRACE — отладка условий и выполнения job’ов

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

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

job:
  script:
    - echo "This is my job"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always
  variables:
    CI_DEBUG_TRACE: "true"

Что происходит при включённом CI_DEBUG_TRACE:

  • GitLab выводит подробный shell-лог выполнения job.

  • Показываются все команды, их аргументы и переменные окружения.

  • Становится проще понять, какие условия и значения реально использовались.

Это особенно полезно, когда:

  • Условие if ведёт себя «странно».

  • Переменная оказывается пустой.

  • job запускается (или не запускается) не так, как ожидалось.

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


Структура Job'а и его параметры

Job — это отдельная задача внутри pipeline. Именно job’ы выполняют реальную работу: запускают тесты, собирают проект, деплоят код и т. д.

В этом разделе разберём базовую структуру job’а и ключевые параметры, без которых невозможно написать осмысленный .gitlab-ci.yml.

Обязательные параметры

Строго говоря, единственный действительно обязательный параметр job’а — это script. Всё остальное либо имеет значение по умолчанию, либо наследуется.

script — команды для выполнения

script — это список shell-команд, которые GitLab Runner выполнит внутри job’а:

job_name:
  script:
    - echo "Hello"
    - npm install
    - npm test

Важные моменты:

  • Команды выполняются строго по порядку.

  • Каждая команда запускается в одной и той же среде (контейнере).

  • Если любая команда возвращает ненулевой код выхода, job считается failed, и выполнение останавливается.

Пример с ошибкой:

failing_job:
  script:
    - echo "This will print"
    - false                    # Эта команда вернёт код ошибки
    - echo "This won't print"  # До сюда не дойдёт

Последняя строка не выполнится — job завершится на команде false.

Игнорирование ошибок в командах

Иногда ошибка допустима (например, необязательные тесты). Тогда можно явно сказать shell’у «игнорируй ошибку»:

tolerant_job:
  script:
    - npm test || true  # Даже если тесты упадут, job продолжит работу

Но здесь есть важный нюанс:
job всё равно будет считаться успешным, даже если тесты упали. Это подходит только для второстепенных проверок.

Альтернатива — allow_failure: true (разберём ниже): в этом случае job будет помечен как failed, но pipeline продолжит работу. Это обычно более наглядный и безопасный вариант.

stage — к какому этапу относится job

stage определяет, на каком этапе pipeline будет выполняться job:

build_job:
  stage: build
  script:
    - npm run build

test_job:
  stage: test
  script:
    - npm test

deploy_job:
  stage: deploy
  script:
    - ./deploy.sh

Что важно помнить:

  • pipeline выполняется по стадиям, а не по job’ам.

  • Все job’ы одной стадии могут выполняться параллельно.

  • Следующая стадия начнётся только после успешного завершения предыдущей (если не указано иное).

Стадия должна быть объявлена заранее в верхнеуровневой секции stages:

stages:
  - build
  - test
  - deploy

Если job ссылается на несуществующую стадию — pipeline не запустится.

image — Docker образ для выполнения job

GitLab CI чаще всего работает в Docker-контейнерах. Параметр image определяет, в каком окружении будут выполняться команды из script.

Пример с общим образом по умолчанию и переопределением в конкретных job’ах:

default:
  image: ubuntu:22.04

python_job:
  stage: build
  image: python:3.11
  script:
    - python --version
    - pip install -r requirements.txt

node_job:
  stage: build
  image: node:18
  script:
    - node --version
    - npm install

Как это работает:

  • Если image указан в job’е — используется он.

  • Если нет — берётся image из секции default.

  • Если image не указан нигде — используется образ по умолчанию GitLab Runner (что часто приводит к неожиданностям).

Поэтому хорошая практика — всегда явно указывать образ, хотя бы в default.

Как выбрать правильный образ

Чаще всего используют официальные образы с Docker Hub:

  • Python: python:3.11, python:3.12, python:3.13.

  • Node.js: node:16, node:18, node:20.

  • Java: maven:3.8, gradle:7.5.

  • Базовые системы: ubuntu:22.04, alpine:3.19.

Официальный каталог образов: https://hub.docker.com

Вы также можете:

  • Использовать приватные образы из собственного Docker Registry.

  • Подтягивать образы с авторизацией.

  • Собирать кастомные образы под свой проект.

Но это уже отдельная, более продвинутая тема — к ней лучше переходить после понимания базовой структуры job’ов.

Основные дополнительные параметры

Помимо script и stage, у job’ов есть множество параметров, которые делают pipeline управляемым, читаемым и удобным для команды. Ниже — самые часто используемые из них.

name — отображаемое имя job’а

По умолчанию GitLab показывает job под тем именем, которое вы указали в конфиге (ключ в YAML). Иногда это имя техническое и не очень читаемое.

С помощью name можно задать более понятное отображаемое название:

build_frontend:
  name: "Build Frontend Application"
  stage: build
  script:
    - npm run build

В результате:

  • В .gitlab-ci.yml остаётся короткое техническое имя (build_frontend).

  • В UI GitLab вы видите понятное описание задачи.

Это особенно полезно в больших pipeline’ах с десятками job’ов.

timeout — максимальное время выполнения job’а

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

long_running_tests:
  stage: test
  timeout: 2h
  script:
    - pytest --slow-tests

Если job не завершится за указанное время, GitLab принудительно остановит его и пометит как failed.

Поддерживаемые форматы:

  • 30s.

  • 15m.

  • 1h.

  • 2h 30m.

Также помните: у проекта и у GitLab Runner может быть глобальный таймаут, который ограничивает максимальное значение timeout.

retry — автоматические повторы при ошибке

CI не всегда падает из-за кода. Бывают:

  • Временные сетевые сбои.

  • Нестабильные внешние сервисы.

  • «Флейковые» тесты.

Для таких случаев есть retry:

flaky_tests:
  stage: test
  retry: 2  # Повторяет до 2 раз, если упадёт
  script:
    - npm test

GitLab автоматически перезапустит job до 2 раз, если он завершится с ошибкой.

Более точная настройка:

job:
  retry:
    max: 2
    when:  # Когда повторять
      - runner_system_failure
      - stuck_or_timeout_failure

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

allow_failure — разрешить job упасть, но не ломать pipeline

Иногда job важен, но не критичен. В таких случаях используйте allow_failure.

optional_tests:
  stage: test
  allow_failure: true  # Job может упасть, но pipeline продолжит работу
  script:
    - npm run test:experimental

Что это даёт:

  • job будет помечен как failed.

  • pipeline продолжит выполнение и может завершиться успешно.

Типичные кейсы:

  • Экспериментальные или нестабильные тесты.

  • Дополнительные проверки качества кода.

  • Новые инструменты, которые вы только внедряете.

В UI такие job’ы обычно подсвечиваются отдельно, чтобы было видно, что ошибка была, но она не блокирующая.

variables — переменные окружения для job'а

Параметр variables позволяет задать переменные окружения только для конкретного job’а:

build_job:
  stage: build
  variables:
    NODE_ENV: "production"
    LOG_LEVEL: "debug"
    CUSTOM_VAR: "some value"
  script:
    - echo $NODE_ENV      # Выведет: production
    - echo $LOG_LEVEL     # Выведет: debug

Особенности:

  • Переменные доступны как обычные переменные окружения.

  • Используются через $VAR или ${VAR}.

  • Приоритет выше, чем у переменных, заданных на уровне проекта или группы.

Это удобно для:

  • Конфигурации окружения сборки.

  • Переключения режимов (dev / prod).

  • Передачи флагов в скрипты.

environment — деплой в окружение

Параметр environment связывает job с логическим окружением (staging, production и т. п.):

deploy_to_staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - ./deploy-to-staging.sh

И аналогично для production:

deploy_to_production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - ./deploy-to-production.sh

Что это даёт:

  • GitLab начинает отслеживать деплой.

  • Появляется история развёртываний.

  • Становится доступен раздел Deployments → Environments.

  • Можно использовать environment-specific переменные и политики доступа.

Это особенно важно для production-деплоев и работы в команде.

coverage — отображение покрытия кода

Если ваши тесты выводят процент покрытия в консоль, GitLab может автоматически его извлечь и показать в интерфейсе.

test_with_coverage:
  stage: test
  script:
    - pytest --cov=src --cov-report=term --cov-report=xml
  coverage: '/TOTAL.*\s+(\d+%)$/'  # Regex для извлечения % покрытия

Как это работает:

  • GitLab анализирует stdout job’а.

  • Ищет совпадение по regex.

  • Берёт значение из первой группы (...).

Результат:

  • Процент покрытия виден в UI.

  • Удобно отслеживать динамику покрытия по коммитам и MR.

Артефакты и их сохранение

Во время выполнения job’ов GitLab может сохранять файлы и папки, созданные в процессе работы. Эти сохранённые файлы называются артефактами (artifacts).

Артефакты используются для:

  • Передачи результатов между job’ами.

  • Скачивания результатов сборки или тестов.

  • Анализа отчётов (покрытие, тесты, логи).

  • Хранения билдов, которые затем деплоятся.

artifacts — сохранение файлов

Пример job’а, который сохраняет результат сборки:

build_job:
  stage: build
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/              # Сохраняем всю папку
      - build/output.jar   # Или конкретный файл
    expire_in: 7 days      # Удалить через неделю
    name: "build-$CI_COMMIT_SHA"

Что здесь происходит:

  • После выполнения script GitLab упаковывает указанные файлы.

  • Артефакт сохраняется на стороне GitLab.

  • Его можно скачать или использовать в следующих job’ах.

Основные параметры artifacts

  • paths - список файлов и папок для сохранения. Поддерживаются glob-паттерны (dist/**/*).

  • expire_in - через какое время артефакт будет автоматически удалён. По умолчанию — 30 дней (может быть переопределено настройками проекта).

  • name - имя архива при скачивании. Полезно для удобной навигации и версионирования.

  • when - при каких условиях сохранять артефакты:

    • on_success (по умолчанию).

    • on_failure.

    • always.

Сохранение артефактов даже при ошибках

Частый кейс — сохранить результаты тестов или отчёты даже если job упал:

test_job:
  stage: test
  script:
    - pytest
  artifacts:
    paths:
      - coverage.xml
    when: always
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

Это особенно полезно, если:

  • Тесты упали, но вам нужен отчёт.

  • Вы хотите проанализировать логи или покрытие.

  • Артефакт используется для отчётов в Merge Request.

Где доступны артефакты

Артефакты можно получить несколькими способами:

  • Через UI GitLab — CI/CD → Pipelines → Job → Download artifacts.

  • В следующих job’ах pipeline (если они зависят от текущего).

  • Через GitLab API.

Важно: артефакты не предназначены для долгосрочного хранения. Для этого лучше использовать Docker Registry, Package Registry или внешние хранилища.

needs — зависимости между job’ами

По умолчанию GitLab выполняет pipeline строго по стадиям:
все job’ы стадии build → потом test → потом deploy.

Параметр needs позволяет задать явные зависимости между job’ами и тем самым ускорить pipeline.

Пример:

stages:
  - build
  - test
  - package

build_app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

test_app:
  stage: test
  needs:
    - build_app  # Ждём артефакты от build_app
  script:
    - npm test
  artifacts:
    paths:
      - test-results/

package_app:
  stage: package
  needs:
    - build_app     # Можем зависеть от нескольких job'ов
    - test_app
  script:
    - ./package.sh

Что здесь важно понять:

  • test_app не ждёт завершения всей стадии build, ему нужен только build_app.

  • package_app стартует, как только готовы build_app и test_app.

  • Артефакты автоматически передаются между зависимыми job’ами.

DAG вместо линейного pipeline

Использование needs превращает pipeline в DAG (Directed Acyclic Graph) — граф зависимостей.

Преимущества:

  • pipeline выполняется быстрее.

  • job’ы не ждут лишние стадии.

  • Зависимости становятся явными и читаемыми.

Когда needs особенно полезен:

  • Большие проекты с множеством job’ов.

  • Монорепозитории.

  • Сложные сборки, где не все шаги зависят друг от друга.

Практический совет

  • Используйте artifacts для передачи результатов, а не для кэша.

  • Используйте needs, если хотите ускорить pipeline, но не переусердствуйте — слишком сложный DAG сложнее поддерживать.

  • Если job использует артефакты другого job’а — явно указывайте needs, даже если они находятся в соседних стадиях.

Кэширование

Если артефакты нужны для передачи результатов работы, то кэш используется исключительно для ускорения pipeline.

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

cache — кэширование файлов между запусками

Простейший пример:

default:
  cache:
    paths:
      - node_modules/       # Кэшируем зависимости
      - .cache/
    key: "$CI_COMMIT_REF_SLUG"  # Разный кэш для разных веток

build_job:
  stage: build
  script:
    - npm install  # Будет быстрее благодаря кэшу
    - npm run build

Что здесь происходит:

  • GitLab пытается забрать кэш перед выполнением job’а.

  • Если кэш найден — файлы уже есть, установка зависимостей проходит быстрее.

  • После завершения job’а кэш обновляется.

Основные параметры cache

  • paths — Какие файлы и директории кэшировать. Обычно это зависимости (node_modules, .venv, .m2, .cache).

  • key — Ключ кэша. Если ключ совпадает — используется существующий кэш. Если ключ изменился — создаётся новый.

  • policy — Управляет тем, как кэш используется:

    • pull — только забирать кэш.

    • push — только сохранять.

    • pull-push (по умолчанию) — забирать и обновлять.

Кэш для разных веток

Частая практика — разделять кэш по веткам, чтобы они не конфликтовали:

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - node_modules/

Это означает:

  • main, develop и feature-ветки будут иметь разные кэши.

  • Меньше шансов поймать странные баги из-за старых зависимостей.

Кэш с учётом lock-файлов

Более надёжный вариант — привязать кэш к package-lock.json или uv.lock:

cache:
  paths:
    - .cache/
  key:
    files:
      - package-lock.json
    prefix: "$CI_COMMIT_REF_SLUG"

Теперь:

  • Если lock-файл изменился — ключ кэша тоже изменится.

  • Зависимости будут пересобраны корректно.

  • Старый кэш не «сломает» сборку.

Разные кэши для разных job’ов

Иногда имеет смысл разделять кэш по типам задач:

build:
  cache:
    key: "build-$CI_COMMIT_REF_SLUG"
  script:
    - npm install

test:
  cache:
    key: "test-$CI_COMMIT_REF_SLUG"
  script:
    - npm test

Такой подход полезен, если:

  • job’ы используют разные наборы файлов.

  • Тесты портят кэш сборки (или наоборот).

  • pipeline стал сложным и тяжёлым.

Важное отличие: cache vs artifacts

Очень частый источник путаницы у новичков:

Кэш

Артефакты

Временные файлы для ускорения

Результаты работы job'а

Может использоваться между pipeline

Используется внутри pipeline

Хранится у runner’а

Загружается в GitLab

Хранится у runner’а

Всегда соответствует job’у

Не влияет на результат

Влияет на следующий шаг

Коротко:

  • cache — про скорость.

  • artifacts — про результат.

Практические рекомендации

  • Не кэшируйте результаты сборки — для этого есть артефакты.

  • Не кладите в кэш всё подряд — он должен быть минимальным.

  • Если pipeline ведёт себя странно — первым делом попробуйте сбросить кэш.

  • Для production-pipeline лучше выбирать детерминированные ключи (через lock-файлы).

Services — вспомогательные контейнеры

Иногда для выполнения job’а одного контейнера недостаточно. Например, для тестов может понадобиться база данных, Redis, RabbitMQ или другой внешний сервис.

В GitLab CI для этого есть servicesдополнительные Docker-контейнеры, которые запускаются рядом с основным job’ом.

Как это работает

  • Основной job и сервисы запускаются в одной Docker-сети.

  • Каждый сервис — это отдельный контейнер.

  • Доступ к сервисам осуществляется по имени контейнера.

  • Сервисы живут только в рамках текущего job’а.

Простой пример с PostgreSQL

test_with_db:
  stage: test
  image: python:3.11
  services:
    - postgres:17
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: test_user
    POSTGRES_PASSWORD: test_password
    DATABASE_URL: "postgresql://test_user:test_password@postgres:5432/test_db"
  script:
    - pip install -r requirements.txt
    - pytest

Здесь важно несколько моментов:

  • postgres:17 — официальный образ PostgreSQL.

  • hostname postgres появляется автоматически и совпадает с именем сервиса.

  • Переменные POSTGRES_* используются самим образом PostgreSQL.

  • DATABASE_URL — это уже ваша переменная, которую читает приложение.

Использование нескольких сервисов

Если тестам нужны сразу несколько зависимостей, их можно указать списком:

integration_tests:
  stage: test
  image: python:3.11
  services:
    - postgres:17
    - redis:7
  variables:
    POSTGRES_DB: test_db
    POSTGRES_USER: testuser
    POSTGRES_PASSWORD: testpass
    DATABASE_URL: "postgresql://testuser:testpass@postgres:5432/test_db"
    REDIS_URL: "redis://redis:6379"
  script:
    - pip install -r requirements.txt
    - pytest

Обращение к сервисам происходит:

  • К PostgreSQL — по хосту postgres.

  • К Redis — по хосту redis.

Алиасы сервисов

Иногда удобнее использовать более осмысленные имена. Для этого можно задать alias:

test:
  stage: test
  image: node:18
  services:
    - name: postgres:17
      alias: db
      variables:
        POSTGRES_DB: testdb
        POSTGRES_USER: user
        POSTGRES_PASSWORD: pass
  variables:
    DATABASE_URL: "postgresql://user:pass@db:5432/testdb"
  script:
    - npm test

Теперь база доступна по хосту db, а не postgres.

Готовность сервиса — важный нюанс

Сервисы не гарантированно готовы к моменту старта script.
Частая ошибка новичков — сразу подключаться к БД и ловить connection refused.

GitLab не ждёт, пока сервис полностью поднимется.

Обычно используют один из подходов:

  • retry-логику в коде.

  • sleep (не лучший вариант).

  • Утилиты ожидания (wait-for-it, pg_isready, nc и т.п.).

Пример с PostgreSQL:

before_script:
  - until pg_isready -h postgres -p 5432; do sleep 1; done

Когда стоит использовать services

services подходят для:

  • unit и integration-тестов.

  • Локальных БД для CI.

  • Проверки миграций.

  • Временных очередей и кэшей.

Не стоит использовать их для:

  • production-нагрузок.

  • Долгоживущих окружений.

  • Сложных multi-container сценариев (для этого лучше Docker Compose или Kubernetes).

Порядок выполнения команд в job’е

Каждый job в GitLab CI проходит фиксированный набор шагов. Понимание этого порядка сильно упрощает отладку pipeline’ов.

Общий порядок выглядит так:

  1. Pulling Docker image — скачивание Docker-образа.

  2. Starting services — запуск сервисов (PostgreSQL, Redis и т.д.).

  3. Cloning repository — клонирование репозитория.

  4. Restoring cache — восстановление кэша.

  5. before_script — подготовительный скрипт (опционально).

  6. script — основной скрипт job’а.

  7. after_script — завершающий скрипт (опционально).

  8. Uploading artifacts — загрузка артефактов.

  9. Uploading cache — сохранение кэша.

Важно понимать:

  • Если job упал на этапе script, шаги after_script всё равно выполняются.

  • Если job упал раньше (например, при pull образа), script даже не начнётся.

  • after_script не может изменить статус job’а — он всегда informational.

before_script и after_script

build_job:
  stage: build
  before_script:
    - echo "Preparing environment..."
    - npm install
  script:
    - npm run build
  after_script:
    - echo "Cleaning up..."
    - rm -rf node_modules

Типичные сценарии использования:

  • before_script:

    • Установка зависимостей.

    • Подготовка окружения.

    • Логин в registry.

    • Проверка доступности сервисов.

  • after_script:

    • Очистка временных файлов.

    • Логирование.

    • Отладочная информация.

    • Уведомления.

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

Параллелизм на одном stage

Job’ы, находящиеся в одном stage и не зависящие друг от друга, выполняются параллельно — насколько это позволяет количество runner’ов.

stages:
  - test

test_unit:
  stage: test
  script:
    - pytest tests/unit/

test_integration:
  stage: test
  script:
    - pytest tests/integration/

test_e2e:
  stage: test
  script:
    - npm run test:e2e

Все три job’а запустятся одновременно, если:

  • Есть свободные runner’ы.

  • Нет ограничений по concurrency у runner’ов.

  • Между job’ами нет needs.

Если runner всего один — параллелизма не будет, даже если stage один и тот же.

Матрица параллельных job’ов (parallel: matrix)

Матрицы позволяют запускать один job с разными комбинациями переменных.

test_matrix:
  stage: test
  image: python:3.9
  parallel:
    matrix:
      - PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11"]
        DJANGO_VERSION: ["3.2", "4.0"]
  script:
    - echo "Testing Python $PYTHON_VERSION with Django $DJANGO_VERSION"
    - pip install django==$DJANGO_VERSION
    - pytest

Что произойдёт:

  • GitLab создаст 8 отдельных job’ов (4 × 2).

  • Каждый job получит свою комбинацию переменных.

  • В UI они будут отображаться как отдельные задания.

Это особенно удобно для:

  • Тестирования на нескольких версиях языка.

  • Проверки совместимости библиотек.

  • Кросс-платформенных сборок.

Важные ограничения матриц

  • Количество job’ов в матрице ограничено (зависит от версии GitLab).

  • Большое количество комбинаций может резко увеличить время pipeline’а.

  • Каждый job — это отдельный контейнер со своими ресурсами.

Поэтому матрицы стоит использовать осознанно и не превращать pipeline в «взрыв job’ов».


Заключение

В этой части мы закрыли (почти) все основные пробелы, связанные с написанием pipeline в GitLab CI.

При этом всё описанное не обязательно применять «один в один» — к построению CI/CD лучше подходить прагматично: лишняя сложность может сыграть плохую шутку при поддержке, не дав заметных плюсов.

В следующей (надеюсь, завершающей) части подробнее разберём runner’ы и базовые методы отладки пайплайнов.

Если вам интересны подобные материалы, подписывайтесь на Telegram-канал «Код на салфетке». Там я делюсь гайдами для новичков, полезными инструментами и практическими примерами из реальных проектов. А прямо сейчас у нас там ещё и проходит новогодний розыгрыш.