{ "index": 282, "slug": "editorial-2020-03-practice-ci-pipeline", "title": "Минимальный CI/CD pipeline: как не отправить непроверенную сборку", "excerpt": "Практический разбор GitLab CI/CD: отделяем проверки от сборки, передаём deploy один артефакт и останавливаем выпуск, если его происхождение нельзя подтвердить.", "contentHtml": "

Симптом знакомый: pipeline зелёный, deploy тоже завершился успешно, но на стенде оказались не те файлы. Иногда deploy заново запускает npm run build. Иногда он читает каталог из cache. Иногда в логе нет ответа на вопрос, из какого commit собран bundle. Цена ошибки — не только один неудачный релиз. Команда тратит время на сравнение каталогов, откладывает откат и рискует повторно отправить тот же неизвестный результат.

\n

Причина обычно не в числе job. Pipeline не назвал единственный результат выпуска. Проверка прошла над одним состоянием, а доставка взяла другое. Исправление начинается с контракта: конкретный commit и lockfile входят в pipeline; job verify проверяет код; job build один раз создаёт каталог; job deploy получает именно этот каталог и не пересобирает проект.

\n

Тезис: deploy должен доставлять объект, а не повторять процесс

\n

У pipeline есть четыре разных состояния. Checkout содержит исходники текущего запуска. Cache ускоряет работу и может исчезнуть без потери корректности. Артефакт — результат конкретного job, прикреплённый к запуску. Deploy создаёт побочный эффект: отправляет артефакт в среду. Эти состояния нельзя смешивать.

\n

Если deploy снова запускает сборку, он становится вторым build-job. У него могут отличаться образ, переменные, lockfile, время получения зависимостей и содержимое cache. Даже при том же SHA он способен получить другой результат. Тестировался один каталог, а отправился другой. Поэтому deploy должен читать результат build и завершаться ошибкой до сетевого вызова, если результат отсутствует или не проходит проверку.

\n

Учебная схема ниже рассчитана на GitLab CI/CD и Node.js. Она показывает границы и проверки, а не готовый production-файл. В ней нет credentials, реального сервера, измерений времени и утверждений о надёжности конкретной команды. Версии GitLab Runner, Node.js и shell нужно сверить с вашим окружением.

\n
\"Схема
Выпуск проходит по одной цепочке: commit и lockfile, проверки, один build-артефакт, затем остановка перед доставкой.
\n

Механизм: входы, доказательства и граница побочного эффекта

\n

Сначала назовите входы. Минимальный набор — revision исходников, lockfile, образ job и команды из package.json. Переменные окружения тоже могут менять результат. Не обязательно стабилизировать все параметры сразу, но их нельзя прятать за фразой «на CI работает иначе».

\n

npm ci полезен для ранней остановки. Он требует существующий lockfile и завершается ошибкой, если manifest и lockfile расходятся. Команда не чинит lockfile сама и не оставляет старый node_modules как доказательство корректности. Это не гарантия воспроизводимости всей сборки: внешний registry, native-модули и версия Node.js остаются отдельными входами.

\n

После verify job build создаёт dist/. Внутрь стоит положить файл REVISION со значением CI_COMMIT_SHA и manifest с контрольными суммами. Так deploy может ответить на два узких вопроса: какой commit породил каталог и не изменились ли его файлы после сборки. Checksum не проверяет бизнес-логику, настройки сервера или безопасность канала. Он только проверяет происхождение и целостность заявленного набора.

\n

Конкретный пример конфигурации

\n

Это учебный пример для простого приложения, которое публикует dist/. Имена job и команды нужно заменить на реальные команды проекта. Ключи и поведение следует проверить через CI Lint и документацию версии GitLab на вашей установке.

\n
image: node:20-alpine\n\nstages:\n  - verify\n  - build\n  - release\n\ncache:\n  key: \"$CI_COMMIT_REF_SLUG\"\n  paths:\n    - .npm/\n\nverify:\n  stage: verify\n  script:\n    - npm ci --cache .npm --prefer-offline\n    - npm run lint\n    - npm test\n\nbuild:\n  stage: build\n  script:\n    - npm ci --cache .npm --prefer-offline\n    - npm run build\n    - printf '%s\\n' \"$CI_COMMIT_SHA\" > dist/REVISION\n    - (cd dist && find . -type f -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS)\n  artifacts:\n    paths:\n      - dist/\n    expire_in: 1 week\n\ndeploy_staging:\n  stage: release\n  when: manual\n  allow_failure: false\n  dependencies:\n    - build\n  script:\n    - set -eu\n    - test \"$(cat dist/REVISION)\" = \"$CI_COMMIT_SHA\"\n    - (cd dist && sha256sum -c SHA256SUMS)\n    - ./scripts/deploy-staging dist/
\n

cache здесь хранит только npm-кэш. Его отсутствие должно замедлить job, но не изменить контракт результата. artifacts прикрепляет каталог к job build. dependencies ограничивает вход deploy этим job. when: manual оставляет явную остановку перед побочным эффектом. Доступ к staging и секреты должны приходить из защищённых настроек CI, а не из YAML.

\n

В примере build повторяет npm ci в отдельном job. Это намеренно консервативный вариант: job не зависит от случайного рабочего каталога verify. Платформа может поддерживать другой способ передачи зависимостей, но оптимизация должна сохранять доказуемую границу. Сначала подтвердите цепочку, потом сокращайте повторную работу измерениями.

\n

Симптомы и точечная диагностика

\n
Что наблюдать до изменения pipeline
СимптомПричинаПроверкаДействие
Зелёный build, другой bundle на стендеdeploy пересобирает checkout или читает cacheНайти build-команды в deploy и вывести источник distПередать artifact через dependencies и убрать вторую сборку
Deploy не находит distartifact не создан, истёк или не скачанПроверить paths, срок хранения и список dependenciesОстановить deploy, исправить передачу artifact
REVISION не совпадает с CI_COMMIT_SHAСмешаны pipeline, ветка или каталогСравнить значение файла, переменную job и commit в интерфейсеНе отправлять файлы; запустить pipeline для нужного revision
npm ci падает до тестовManifest и lockfile расходятся или не совпали флаги npmЗапустить чистую установку тем же образом и прочитать первую ошибкуОбновить lockfile осознанно и закоммитить согласованную пару
Checksum не проходитФайл изменился после build или manifest не соответствует каталогуПроверить содержимое dist и команду создания SHA256SUMSЗавершить job до upload и расследовать источник изменения
\n

Таблица не заменяет логи. Она задаёт короткий маршрут: сначала определить, какой объект потерялся, затем проверить конкретную границу. Не добавляйте retry, новый cache или вторую сборку, пока не назван симптом. Повтор запуска скрывает нестабильность и не доказывает, что проверка и доставка использовали один результат.

\n

Порядок внедрения

\n
  1. Зафиксируйте текущую команду build, путь результата и commit, на котором выполняется проверка.
  2. Проверьте чистый runner: npm ci должен работать с сохранённым lockfile. Исправьте расхождение manifest и lockfile до настройки deploy.
  3. Добавьте verify с реальными lint и test-командами. Ненулевой exit code должен блокировать build.
  4. Соберите dist/ один раз и добавьте REVISION и SHA256SUMS. Прикрепите каталог как artifact.
  5. Настройте deploy только от build. До сетевого вызова сравните revision и контрольные суммы.
  6. Оставьте manual gate для учебного staging-прогона. Отдельно проверьте провалы test, отсутствие artifact и несовпадение revision.
\n

Ограничения и отрицательный путь

\n

Эта схема не решает rollback, миграции базы, стратегию production-раскатки, smoke-тесты после доставки и мониторинг. Artifact имеет срок хранения. Если он нужен для отката, его следует сохранять в подходящем registry или хранилище с правилами доступа и именованием. Нельзя считать недельный срок из примера политикой релизов.

\n

Checksum не защищает от скомпрометированного runner и не подтверждает, что сервер применил файлы. Manual job не заменяет review прав доступа. npm ci не фиксирует версию Node.js и состояние внешнего registry. Эти ограничения не делают минимальный pipeline бесполезным. Они показывают, какие вопросы он не закрывает.

\n

Отрицательный путь обязателен. Если lockfile расходится, verify должен остановиться. Если build не создал artifact, deploy не должен строить заново. Если revision или checksum не совпали, сетевой deploy не должен запускаться. Если manual gate не подтверждён, побочный эффект не происходит. Именно эти остановки делают ошибку наблюдаемой и ограничивают её цену.

\n

Критерий готовности

\n

Учебная реализация готова, когда один запуск на staging показывает цепочку «commit → verify → build → artifact → manual deploy», а журнал позволяет назвать revision и состав artifact. Отдельно должны быть подтверждены три отказа: провал verify блокирует build; отсутствие или несовпадение artifact блокирует deploy; deploy не вызывает build. Это проверяемое условие. Оно не выдаёт учебный прогон за production-результат и оставляет понятный следующий шаг для hardening.

\n

Проверяемые источники

\n" }