GitLab CI/CD: Пять лайфхаков для управления пайплайнами, которые сэкономят вам часы работы

Когда я впервые столкнулся с 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% сценариев, с которыми я сталкивался. Надеюсь, они сэкономят вам время так же, как и мне.

Оставить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *