Review apps: окружение на каждый MR
Разворачиваем временную копию приложения на каждый MR, чтобы ревьюер видел изменения вживую.
Review app — динамическое эфемерное окружение, разворачиваемое для конкретного Merge Request и автоматически удаляемое при его закрытии.
Зачем смотреть, а не воображать
Читать diff полезно, но «как это выглядит в работе» по коду не всегда понятно. Review apps решают это: для каждого MR поднимается живая копия приложения с изменениями этой ветки. Ревьюер открывает ссылку прямо из MR и щёлкает по интерфейсу. Это одна из визитных карточек GitLab.
Стоит проговорить, чего именно diff не показывает. Diff отвечает на вопрос «что изменилось в тексте», но молчит про то, как это ощущается: не съехала ли вёрстка на узком экране, не стало ли действие на два клика длиннее, читается ли новый текст ошибки. Особенно это касается фронтенда и UX-правок, где «правильный код» и «удобный продукт» — не одно и то же. Review app переносит проверку из плоскости «я прочитал и поверил» в плоскость «я открыл и убедился». Это снимает с ревьюера необходимость держать в голове виртуальную модель приложения и мысленно применять к ней патч.
Есть и неочевидная выгода: review app полезен не только программисту-ревьюеру. Ссылку из MR можно дать дизайнеру, продакт-менеджеру или тестировщику, и они проверят фичу до мёржа, не разворачивая ничего у себя и не дожидаясь попадания кода на общий staging. Так обратная связь приходит раньше и дешевле: исправить замечание в открытом MR проще, чем после релиза. По сути review apps сдвигают тестирование и приёмку влево по пайплайну — к моменту, когда менять код ещё легко.
Динамическое окружение
Магия в том, что имя и URL окружения формируются из переменных, уникальных для ветки/MR:
deploy-review:
stage: deploy
script:
- ./deploy.sh "review-$CI_COMMIT_REF_SLUG"
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: stop-review
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'Каждый MR получает своё окружение review/имя-ветки и свой поддомен. CI_COMMIT_REF_SLUG — «безопасная» для URL версия имени ветки.
Разберём, почему имя пишут с косой чертой — review/$CI_COMMIT_REF_SLUG. GitLab трактует часть до / как «папку» окружений и группирует все review-окружения вместе на странице Environments. Вместо плоской свалки из сотни записей вы получаете аккуратную ветку review/*, которую можно свернуть и которая не мешает видеть staging и production. Это не требование синтаксиса, а соглашение, заметно улучшающее навигацию, когда открытых MR много.
Отдельно про CI_COMMIT_REF_SLUG. Имена веток в Git могут содержать символы, недопустимые в DNS и URL: слэши (feature/login), верхний регистр, подчёркивания, длинные хвосты. Переменная-slug приводит имя к нижнему регистру, заменяет небезопасные символы на дефис и обрезает длину так, чтобы получился валидный поддомен. Использовать сырое CI_COMMIT_REF_NAME в URL — почти гарантированно нарваться на сломанный адрес на первой же ветке с нестандартным именем.
Автоматическая уборка
Эфемерные окружения нельзя оставлять навсегда — иначе их накопятся сотни. Поэтому объявляют джобу остановки, ссылаясь на неё в on_stop:
stop-review:
stage: deploy
script:
- ./teardown.sh "review-$CI_COMMIT_REF_SLUG"
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
when: manual
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'action: stop помечает джобу как останавливающую окружение. GitLab вызовет её при закрытии/слиянии MR (или по кнопке), и review app исчезнет.
Тут важна экономика, а не только чистота. Каждый поднятый review app потребляет ресурсы: память, CPU, иногда отдельный namespace в Kubernetes, запись в реестре образов, строку в балансировщике. Десять открытых MR — это десять параллельно работающих копий приложения. Если уборку не автоматизировать, окружения переживают свои MR, и через месяц инфраструктура забита «зомби-стендами», за которые приходят счета, а никто уже не помнит, к какой ветке они относились. Связка on_stop + action: stop делает жизненный цикл окружения симметричным жизненному циклу MR: открылся — поднялось, закрылся — погасло.
Для подстраховки на случай «забытых» MR у окружений есть и автоудаление по неактивности (auto-stop через срок жизни): можно задать, что review app сам гасится, скажем, через несколько дней без новых деплоев. Это страховочная сетка под основной механизм on_stop, а не замена ему.
Сравнение с GitHub Actions
В GitHub похожего эффекта добиваются через GitHub Environments и сторонние сервисы (Vercel/Netlify preview deployments). В GitLab review apps — встроенная концепция динамических окружений, тесно интегрированная с MR и историей деплоев.
Разница в том, откуда берётся фича. В экосистеме GitHub превью-деплои чаще всего приносит платформа хостинга: Vercel или Netlify сами слушают пуши, поднимают preview-URL и постят его в Pull Request. Это удобно, но привязывает вас к конкретному провайдеру и хорошо работает в основном для фронтенда и статики. В GitLab review app — это ваш деплой-скрипт плюс механизм динамических окружений: вы сами решаете, что и куда катить (Kubernetes, своя VM, что угодно), а GitLab берёт на себя связывание с MR, ссылку в виджете и автоуборку. Гибкости больше, но и собрать конвейер придётся самому — это не «галочка», а несколько джоб в .gitlab-ci.yml.
Как работает под капотом
Поскольку name окружения содержит переменную, GitLab создаёт динамическое окружение на лету для каждого значения. В виджете MR появляется ссылка на URL этого окружения. При событии закрытия MR GitLab находит окружение с тем же именем и запускает связанную через on_stop джобу с action: stop, после чего помечает окружение остановленным.
Ключевой момент — как stop-джоба вообще может запуститься после закрытия MR, когда нового пайплайна уже нет. GitLab при первом деплое запоминает stop-джобу как «отложенную»: её определение сохраняется в привязке к окружению. Когда наступает событие остановки, движок не генерирует новый пайплайн с нуля, а исполняет именно ту запомненную джобу в том же контексте переменных, что и при создании окружения. Поэтому так важно, чтобы name у deploy- и stop-джобы совпадал символ в символ: по нему GitLab и находит, что именно гасить.
Поток событий целиком:
MR открыт / push -> deploy-review -> окружение review/branch (live)
|
v
ссылка в виджете MR -> ревьюер кликает
MR закрыт / смёржен -> GitLab находит окружение по name
-> запускает stop-review (action: stop)
-> окружение помечено stopped, ресурсы освобожденыЧастые ошибки
- Забыть
on_stop— review apps не убираются, инфраструктура засоряется. - Использовать в URL сырое имя ветки вместо
CI_COMMIT_REF_SLUG— символы вроде/ломают поддомен. - Не ограничить джобу
rulesна MR — review-окружения начнут плодиться и на обычных push. - Дать stop-джобе имя окружения, отличное от deploy-джобы — GitLab не свяжет их и не сможет погасить окружение.
- Полагаться только на ручную кнопку остановки без auto-stop — забытые MR оставят зомби-стенды.
Итоги
- Review app — живая копия приложения на каждый MR для визуальной проверки.
- Динамическое окружение с именем/URL из
CI_COMMIT_REF_SLUG. on_stop+action: stopавтоматически гасят окружение при закрытии MR.- Совпадение
nameу deploy- и stop-джобы — обязательное условие автоуборки. - В отличие от GitHub-экосистемы, превью в GitLab — ваш деплой-скрипт, а не услуга провайдера хостинга.