workflow и ручные джобы (when: manual)

Учимся управлять созданием всего пайплайна и оформлять ручные шаги-гейты.

workflow:rules — правила на уровне всего пайплайна, решающие, создавать ли пайплайн вообще для данного события.

Проблема дублирующихся пайплайнов

Если включены и push-, и MR-пайплайны, на коммит в ветке с открытым MR может создаться два пайплайна: один на push, другой на MR. Это путаница и лишний расход раннеров. Решает её workflow — правила на уровне файла, а не отдельной джобы.

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_TAG'
    - when: never

Эта классическая конструкция говорит: создавать пайплайн для MR, для ветки main и для тегов, а в остальных случаях — не создавать. Так избегают двойных пайплайнов: на ветке с MR будет только MR-пайплайн.

Чтобы понять, откуда берётся дубль, полезно представить два независимых триггера. Когда вы пушите коммит в ветку, у которой уже открыт merge request, GitLab по умолчанию рассматривает это событие дважды: как обычный push (создаётся branch-пайплайн) и как обновление MR (создаётся merge-request-пайплайн). Оба валидны, оба видны в интерфейсе, оба занимают раннеры — и оба гоняют, по сути, одинаковые тесты. На загруженном проекте это удваивает счёт за минуты CI и засоряет историю пайплайнов. Схема ниже показывает, как workflow:rules схлопывает развилку в одну ветвь:

  push в ветку с открытым MR
            |
      +-----+-----+
      |           |
   branch-      MR-
  пайплайн   пайплайн   <-- без workflow: оба создаются (дубль)
      |           |
      X           +--> остаётся только MR-пайплайн
   workflow:rules отсекает branch-пайплайн

Логика классической конструкции читается сверху вниз, как и у обычных rules: сперва проверяется, не MR ли это; если да — пайплайн создаётся как MR-пайплайн, и до правила про CI_COMMIT_BRANCH дело не доходит. Именно поэтому порядок правил в workflow критичен: правило про merge request должно стоять выше правила про ветку, иначе ветка main с открытым MR снова даст дубль.

Помимо устранения дублей, workflow часто используют как «рубильник» всего пайплайна. Через workflow:rules с блоком variables можно задавать глобальные переменные в зависимости от того, как запущен пайплайн: пометить релизные пайплайны особым флагом, переключить окружение деплоя, выставить уровень логирования. А ещё workflow умеет полностью гасить пайплайны на draft-MR или на коммитах с пометкой пропуска CI в сообщении — последнее GitLab понимает «из коробки», но через workflow поведение можно сделать явным и предсказуемым для всей команды.

Ручные джобы: when: manual

Джоба с when: manual попадает в пайплайн, но не запускается сама — в интерфейсе появляется кнопка «play». Это стандартный способ оформить гейт деплоя: пайплайн зелёный, артефакт готов, но релиз на production делает человек одним кликом.

deploy-production:
  stage: deploy
  script:
    - ./deploy.sh production
  when: manual
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Гейт ручного деплоя — это больше, чем удобная кнопка. Он встраивает человеческое решение в автоматизированный конвейер ровно там, где автоматизации доверять рискованно: выкат на боевой прод. Пайплайн доходит до стадии deploy, все тесты зелёные, артефакт собран и лежит наготове, но финальный шаг ждёт сознательного клика ответственного человека. Это снимает напряжение «а вдруг автодеплой выкатит недоделку» и одновременно сохраняет всю выгоду автоматизации: к моменту клика делать уже нечего, кроме как нажать.

На практике ручной деплой почти всегда связывают с environment: указав environment: production, вы получаете в GitLab учёт окружений, историю деплоев, кнопку отката к предыдущей версии и привязку к защищённым переменным окружения. А resource_group рядом с manual-джобой не даст двум деплоям на один и тот же прод выполняться одновременно — лишний клик не приведёт к гонке двух выкатов.

allow_failure и blocking manual

По умолчанию ручная джоба не блокирует пайплайн: если её не нажать, пайплайн всё равно считается успешным. Чтобы сделать ручной шаг блокирующим (пайплайн «застывает» в ожидании клика), используют allow_failure: false у manual-джобы. И наоборот, необязательную проверку помечают allow_failure: true, чтобы её падение не валило весь пайплайн.

Поведение по умолчанию здесь контринтуитивно, и на нём спотыкаются многие. Логика такая: раз джобу нужно нажимать руками, её «непажатие» не должно автоматически считаться провалом — иначе любой пайплайн без клика по деплою висел бы красным. Поэтому manual-джоба по умолчанию ведёт себя как allow_failure: true и не блокирует завершение пайплайна. Но для гейта это часто не то, что нужно: если деплой обязателен, вы хотите, чтобы пайплайн честно «застывал» в ожидании клика, а не убегал в зелёный статус без релиза. Тогда и ставят allow_failure: false — джоба становится блокирующей (blocking manual), и пайплайн не завершится, пока её не запустят или явно не пропустят.

Сведём логику в таблицу, потому что именно её путают чаще всего:

whenallow_failureЭффект
manualtrue (по умолчанию)Опциональный шаг: пайплайн зелёный даже без клика
manualfalseБлокирующий гейт: пайплайн ждёт клика, не завершается
on_successtrueЗапустится сама; её падение не валит пайплайн
on_successfalse (по умолчанию)Запустится сама; падение валит пайплайн

when: delayed

Есть и отложенный запуск: when: delayed с start_in: 30 minutes запустит джобу с задержкой — полезно для canary-наблюдения или авто-отката через паузу.

Отложенный запуск закрывает класс задач, где между шагами нужна пауза, а не действие человека. Классический сценарий — canary-деплой: выкатили на 5% трафика, дали when: delayed с start_in: 30 minutes на джобу, которая раскатывает на 100%, и за эти полчаса успеваете заметить всплеск ошибок и отменить раскатку кнопкой. Технически delayed-джоба до истечения таймера висит в состоянии ожидания, и её можно запустить досрочно или отменить — то есть это «мягкий» автоматический шаг с окном для ручного вмешательства.

Параллель с GitHub Actions

Прямого аналога workflow:rules в Actions нет: там фильтрация события задаётся блоком on: в каждом workflow-файле, а проблему дублей решает сама модель триггеров (pull_request и push — разные события с разными запусками). Ручной гейт в Actions — это workflow_dispatch (запуск всего workflow кнопкой) или, для пошагового подтверждения внутри выполнения, механизм environments с required reviewers, который ставит деплой на паузу до одобрения. Отложенному запуску GitLab в Actions соответствует разве что шаг с sleep или отдельный таймер — встроенного start_in там нет. Иными словами, GitLab отдаёт больше управления запуском на уровень декларативного YAML, тогда как Actions чаще опирается на настройки окружений в UI.

Как работает под капотом

Сначала GitLab применяет workflow:rules и решает, создавать ли пайплайн. Если да — вычисляет джобы с их rules. Manual-джоба создаётся в статусе ожидания действия. Нажатие кнопки переводит её в очередь, и раннер выполняет. allow_failure: false у manual-джобы делает её обязательной для перехода пайплайна в успех.

Частые ошибки

  • Не настроить workflow и получать по два пайплайна на коммит в ветке с MR.
  • Думать, что manual-джоба блокирует пайплайн по умолчанию — нет, нужен allow_failure: false.
  • Ставить when: manual без rules на ветку и удивляться, что кнопка деплоя появляется и в feature-ветках.

Итоги

  • workflow:rules решают, создавать ли пайплайн; классическая конструкция убирает двойные пайплайны.
  • when: manual делает джобу-гейт с кнопкой запуска — стандарт для прод-деплоя.
  • allow_failure управляет тем, блокирует ли джоба пайплайн.
Проверьте себя
1. Зачем настраивают workflow:rules?
AЧтобы шифровать переменные
BЧтобы решать, создавать ли пайплайн вообще, и избегать двойных пайплайнов на ветке с MR
CЧтобы ускорить git clone
DЧтобы задать теги раннера
2. Блокирует ли manual-джоба пайплайн по умолчанию?
AДа, всегда
BНет, по умолчанию не блокирует; для блокировки нужен allow_failure: false
CТолько в стадии deploy
DТолько при schedule