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), и пайплайн не завершится, пока её не запустят или явно не пропустят.
Сведём логику в таблицу, потому что именно её путают чаще всего:
when | allow_failure | Эффект |
|---|---|---|
manual | true (по умолчанию) | Опциональный шаг: пайплайн зелёный даже без клика |
manual | false | Блокирующий гейт: пайплайн ждёт клика, не завершается |
on_success | true | Запустится сама; её падение не валит пайплайн |
on_success | false (по умолчанию) | Запустится сама; падение валит пайплайн |
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управляет тем, блокирует ли джоба пайплайн.