Когда я впервые столкнулся с GitLab CI/CD, то потратил добрый час, пытаясь понять, почему мой пайплайн запускается в самый неподходящий момент. Оказалось, я просто не знал о существовании нескольких полезных техник, которые меняют игру. Вот они.
1. Условный запуск джобов через push-опции
Самая частая ситуация: вы пушите код, а CI/CD сразу же начинает гонять тесты и деплой. Но иногда нужно просто отправить изменения в репозиторий без лишних затрат времени и ресурсов.
Знакомо? Раньше я просто ждал, пока всё отработает. Потом узнал про флаги --push-option.
git push --push-option="ci.skip"
Вот так просто—пайплайн вообще не запустится. Очень удобно, когда нужно закоммитить, скажем, обновление документации или исправить что-то мелкое в комментариях.
Но есть и более интересный вариант. Если вы хотите передать переменную в пайплайн:
git push --push-option="ci.variable=DEPLOY_ENV=staging"
Эта переменная станет доступна во всех джобах, и вы можете её проверять в конфигурации. Например, я использую это для выбора окружения при деплое, не меняя сам .gitlab-ci.yml.
2. Переменные для гибкого управления поведением
Дальше интереснее. Представьте, что у вас есть этап сборки, который обычно запускается автоматически, но иногда нужно, чтобы он был только по клику. Классический сценарий: вы разрабатываете что-то сложное, регулярно пушите код, но полную сборку хотите запустить только перед релизом.
Раньше я копировал весь .gitlab-ci.yml, менял в нём only и except. Теперь просто использую переменные:
build-npm:
stage: build-npm
image: node:22.1.0
script:
- npm install
- npm run build:all
- tar -czf dist.tar.gz dist/
rules:
- if: $CI_COMMIT_BRANCH != "dev" && $CI_COMMIT_BRANCH != "master"
when: never
- if: $MANUAL_TRIGGER == "true"
when: manual
- when: on_success
artifacts:
paths:
- dist.tar.gz
interruptible: true
Теперь я могу сделать обычный push:
git push
И джоб запустится автоматически. Или с флагом:
git push --push-option="ci.variable=MANUAL_TRIGGER=true"
И джоб появится в UI с надписью «manual»—щёлкаешь, когда готов.
3. Фильтрация по веткам через rules
Тут есть один момент, который многих путает. Если вы раньше использовали only и except, то первый раз с rules может быть странновато.
Дело в том, что only и rules несовместимы. GitLab выбросит ошибку: «jobs:build-npm config key may not be used with rules: only». Я наступил на эти грабли, поэтому делюсь опытом.
Вместо этого:
only:
- dev
- master
except:
- tags
Пишем так:
rules:
- if: $CI_COMMIT_BRANCH != "dev" && $CI_COMMIT_BRANCH != "master"
when: never
- if: $CI_COMMIT_TAG
when: never
- when: on_success
Логика: первое правило, которое совпадает—оно и применяется. Если это не ветка dev или master, джоб не запустится. Если это тег, тоже не запустится. В остальных случаях запустится нормально.
4. Комбинирование условий для сложных сценариев
Вот здесь начинается реальная магия. У нас на работе была ситуация: нужно было запустить полный деплой только при слиянии в master, но позволить разработчикам самостоятельно деплоить на staging при необходимости, а остальные коммиты просто должны были пройти лinting и базовые тесты.
Получилось так:
stages:
- lint
- test
- build
- deploy
lint:
stage: lint
script:
- npm run lint
rules:
- when: always
test:
stage: test
script:
- npm run test
rules:
- when: always
build:
stage: build
script:
- npm run build
rules:
- if: $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "staging"
when: on_success
- when: manual
deploy-staging:
stage: deploy
script:
- ./deploy.sh staging
rules:
- if: $CI_COMMIT_BRANCH == "staging"
when: on_success
- if: $FORCE_DEPLOY == "true"
when: manual
- when: never
environment:
name: staging
deploy-production:
stage: deploy
script:
- ./deploy.sh production
rules:
- if: $CI_COMMIT_BRANCH == "master"
when: manual
environment:
name: production
Теперь логика такая:
- Lint и тесты всегда запускаются
- Build автоматически на master/staging, остальные ветки—по клику
- Deploy на staging автоматический, но можно форсировать через переменную
- Deploy на production только на master, и только по клику (safety first!)
Если нужна форсированная сборка на фиче:
git push --push-option="ci.variable=FORCE_DEPLOY=true"
5. Оптимизация с artifacts и кешированием
Последний лайфхак, который сэкономит вам гигабайты трафика и минуты времени—правильное использование артефактов и кеша.
Много раз я видел конфиги, где node_modules переходит из джоба в джоб, раздувая весь пайплайн. Вот как это делать правильно:
variables:
npm_config_cache: $CI_PROJECT_DIR/.npm
cache:
paths:
- .npm/
policy: pull-push
stages:
- install
- lint
- test
- build
install-deps:
stage: install
image: node:22.1.0
script:
- npm ci --cache $npm_config_cache
cache:
paths:
- node_modules/
- .npm/
policy: pull-push
lint:
stage: lint
image: node:22.1.0
script:
- npm run lint
cache:
paths:
- node_modules/
- .npm/
policy: pull
test:
stage: test
image: node:22.1.0
script:
- npm run test
cache:
paths:
- node_modules/
- .npm/
policy: pull
build:
stage: build
image: node:22.1.0
script:
- npm run build
- tar -czf dist.tar.gz dist/
artifacts:
paths:
- dist.tar.gz
expire_in: 1 week
cache:
paths:
- node_modules/
- .npm/
policy: pull
Вот что здесь происходит: install-deps скачивает зависимости один раз и складывает их в кеш. Остальные джобы просто используют этот кеш (политика pull), не переписывая его. Значительно быстрее.
Артефакты dist.tar.gz я сохраняю только на неделю—экономим место. Если нужна более долгая сохранность, меняю expire_in.
Реальный пример: полный конфиг для Node.js проекта
Вот конфиг, который я использую для большинства своих проектов. Там комбинируются все описанные выше техники:
variables:
npm_config_cache: $CI_PROJECT_DIR/.npm
NODE_ENV: production
cache:
paths:
- .npm/
stages:
- install
- lint
- test
- build
- deploy
install-deps:
stage: install
image: node:22.1.0
script:
- npm ci --cache $npm_config_cache
cache:
paths:
- node_modules/
- .npm/
policy: pull-push
lint:
stage: lint
image: node:22.1.0
script:
- npm run lint
cache:
paths:
- node_modules/
- .npm/
policy: pull
rules:
- when: always
test:
stage: test
image: node:22.1.0
script:
- npm run test
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
cache:
paths:
- node_modules/
- .npm/
policy: pull
rules:
- when: always
build:
stage: build
image: node:22.1.0
script:
- npm run build
- tar -czf dist.tar.gz dist/
artifacts:
paths:
- dist.tar.gz
expire_in: 2 weeks
cache:
paths:
- node_modules/
- .npm/
policy: pull
rules:
- if: $CI_COMMIT_BRANCH == "dev" || $CI_COMMIT_BRANCH == "master"
when: on_success
- when: manual
interruptible: true
deploy-staging:
stage: deploy
image: alpine:latest
before_script:
- apk add curl
script:
- curl -X POST $STAGING_WEBHOOK_URL -d '{"ref":"'$CI_COMMIT_BRANCH'"}'
rules:
- if: $CI_COMMIT_BRANCH == "dev"
when: on_success
- if: $FORCE_STAGING_DEPLOY == "true"
when: manual
- when: never
environment:
name: staging
url: $STAGING_URL
deploy-production:
stage: deploy
image: alpine:latest
before_script:
- apk add curl
script:
- curl -X POST $PRODUCTION_WEBHOOK_URL -d '{"ref":"'$CI_COMMIT_BRANCH'"}'
rules:
- if: $CI_COMMIT_BRANCH == "master"
when: manual
- when: never
environment:
name: production
url: $PRODUCTION_URL
Пара советов, которые я узнал методом проб и ошибок
Первое—всегда используйте interruptible: true для джобов, которые долго работают. Если вы пушите новые изменения, предыдущий пайплайн просто отменится, экономя ресурсы.
Второе—не забывайте про expire_in для артефактов. Я однажды исчерпал квоту хранилища, потому что сохранял все архивы навсегда.
Третье—тестируйте конфиг перед тем, как пушить в продакшн. На GitLab есть отличная функция—Pipelines → CI/CD Lint, которая подсветит все ошибки.
Вот, собственно, и все. Эти пять техник покрывают 90% сценариев, с которыми я сталкивался. Надеюсь, они сэкономят вам время так же, как и мне.