diff --git a/editorial/agent-rewrites/282.json b/editorial/agent-rewrites/282.json index af6f67a..592e72a 100644 --- a/editorial/agent-rewrites/282.json +++ b/editorial/agent-rewrites/282.json @@ -3,5 +3,5 @@ "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. Цена ошибки — не только один неудачный релиз. Команда тратит время на сравнение каталогов, откладывает откат и рискует повторно отправить тот же неизвестный результат.
Причина обычно не в числе job. Pipeline не назвал единственный результат выпуска. Проверка прошла над одним состоянием, а доставка взяла другое. Исправление начинается с контракта: конкретный commit и lockfile входят в pipeline; job verify проверяет код; job build один раз создаёт каталог; job 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Сначала назовите входы. Минимальный набор — revision исходников, lockfile, образ job и команды из package.json. Переменные окружения тоже могут менять результат. Не обязательно стабилизировать все параметры сразу, но их нельзя прятать за фразой «на CI работает иначе».
npm ci полезен для ранней остановки. Он требует существующий lockfile и завершается ошибкой, если manifest и lockfile расходятся. Команда не чинит lockfile сама и не оставляет старый node_modules как доказательство корректности. Это не гарантия воспроизводимости всей сборки: внешний registry, native-модули и версия Node.js остаются отдельными входами.
После verify job build создаёт dist/. Внутрь стоит положить файл REVISION со значением CI_COMMIT_SHA и manifest с контрольными суммами. Так deploy может ответить на два узких вопроса: какой commit породил каталог и не изменились ли его файлы после сборки. Checksum не проверяет бизнес-логику, настройки сервера или безопасность канала. Он только проверяет происхождение и целостность заявленного набора.
Это учебный пример для простого приложения, которое публикует dist/. Имена job и команды нужно заменить на реальные команды проекта. Ключи и поведение следует проверить через CI Lint и документацию версии GitLab на вашей установке.
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/\ncache здесь хранит только npm-кэш. Его отсутствие должно замедлить job, но не изменить контракт результата. artifacts прикрепляет каталог к job build. dependencies ограничивает вход deploy этим job. when: manual оставляет явную остановку перед побочным эффектом. Доступ к staging и секреты должны приходить из защищённых настроек CI, а не из YAML.
В примере build повторяет npm ci в отдельном job. Это намеренно консервативный вариант: job не зависит от случайного рабочего каталога verify. Платформа может поддерживать другой способ передачи зависимостей, но оптимизация должна сохранять доказуемую границу. Сначала подтвердите цепочку, потом сокращайте повторную работу измерениями.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Зелёный build, другой bundle на стенде | deploy пересобирает checkout или читает cache | Найти build-команды в deploy и вывести источник dist | Передать artifact через dependencies и убрать вторую сборку |
| Deploy не находит dist | artifact не создан, истёк или не скачан | Проверить 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 и расследовать источник изменения |
Таблица не заменяет логи. Она задаёт короткий маршрут: сначала определить, какой объект потерялся, затем проверить конкретную границу. Не добавляйте retry, новый cache или вторую сборку, пока не назван симптом. Повтор запуска скрывает нестабильность и не доказывает, что проверка и доставка использовали один результат.
\nnpm ci должен работать с сохранённым lockfile. Исправьте расхождение manifest и lockfile до настройки deploy.dist/ один раз и добавьте REVISION и SHA256SUMS. Прикрепите каталог как artifact.Эта схема не решает rollback, миграции базы, стратегию production-раскатки, smoke-тесты после доставки и мониторинг. Artifact имеет срок хранения. Если он нужен для отката, его следует сохранять в подходящем registry или хранилище с правилами доступа и именованием. Нельзя считать недельный срок из примера политикой релизов.
\nChecksum не защищает от скомпрометированного runner и не подтверждает, что сервер применил файлы. Manual job не заменяет review прав доступа. npm ci не фиксирует версию Node.js и состояние внешнего registry. Эти ограничения не делают минимальный pipeline бесполезным. Они показывают, какие вопросы он не закрывает.
Отрицательный путь обязателен. Если lockfile расходится, verify должен остановиться. Если build не создал artifact, deploy не должен строить заново. Если revision или checksum не совпали, сетевой deploy не должен запускаться. Если manual gate не подтверждён, побочный эффект не происходит. Именно эти остановки делают ошибку наблюдаемой и ограничивают её цену.
\nУчебная реализация готова, когда один запуск на staging показывает цепочку «commit → verify → build → artifact → manual deploy», а журнал позволяет назвать revision и состав artifact. Отдельно должны быть подтверждены три отказа: провал verify блокирует build; отсутствие или несовпадение artifact блокирует deploy; deploy не вызывает build. Это проверяемое условие. Оно не выдаёт учебный прогон за production-результат и оставляет понятный следующий шаг для hardening.
\nСимптом появляется в момент, когда кажется, что всё уже прошло: job verify и build зелёные, а после ручного deploy на staging оказывается другой набор файлов. Иногда deploy снова вызывает npm run build. Иногда он читает каталог из cache. Иногда в логе нет ответа на вопрос, из какого commit собран bundle. Цена ошибки — ручное сравнение каталогов, задержка отката и риск повторно отправить тот же неизвестный результат.
Для первого контура не нужно строить большой release-комплекс. Достаточно назвать входы, один раз создать результат и передать его следующему job. Ниже — учебная схема GitLab CI/CD в историческом контексте марта 2020 года: образ node:12-alpine, npm ci, три стадии и ручной staging deploy. Она не изображает реальный production-релиз и не заменяет проверку версии GitLab, Runner, shell, прав и команд конкретного проекта.
Входом служат revision исходников и lockfile зависимостей. Job verify запускает lint и тесты; любой ненулевой exit code останавливает следующий этап. Job build создаёт dist/, записывает туда CI_COMMIT_SHA и список контрольных сумм. Job deploy_staging получает только этот результат, проверяет его до сетевого вызова и не собирает проект заново.
Такой контракт отвечает на два разных вопроса. REVISION связывает каталог с commit текущего pipeline. SHA256SUMS показывает, что файлы внутри полученного artifact не изменились между сборкой и проверкой. Ни один из файлов не является подписью релиза: они не защищают runner, сервер, registry или секреты. Их роль уже: сделать подмену наблюдаемой и остановить job до побочного эффекта.
Cache хранит данные, которые можно получить заново. В Node-проекте это, например, каталог npm-кэша. Его отсутствие должно увеличить время установки, но не менять заявленный результат выпуска. Artifact — файл или каталог, который конкретный job сохраняет для следующих job. Если deploy читает cache вместо artifact, pipeline теряет владельца результата: неизвестно, кто создал каталог и к какому запуску он относится.
\nПорядок стадий задаёт маршрут verify → build → release. При этом одного порядка недостаточно: deploy должен явно указать dependencies: - build, чтобы получить artifact именно этого job. В build можно записать dependencies: [], тем самым не рассчитывать на файлы предыдущих job. Если проекту понадобится передать отчёт из verify, это следует добавить как отдельный, названный контракт, а не использовать общий рабочий каталог.
Пример рассчитан на приложение, которое публикует каталог dist/. Имена команд lint, test и ./scripts/deploy-staging условны. Перед применением конфигурацию нужно проверить через CI Lint и выполнить на том образе и Runner, которые используются в проекте.
image: node:12-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 dependencies: []\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 ! -name SHA256SUMS -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Важная деталь находится в команде создания manifest: SHA256SUMS исключён из списка входных файлов. Если сначала открыть этот файл на запись, а затем включить его в find, manifest начнёт считать сам себя; проверка станет зависеть от момента чтения и может завершиться ошибкой. REVISION, наоборот, создаётся до расчёта сумм и входит в проверяемый набор.
Build повторяет npm ci, хотя verify уже устанавливал зависимости. Это осознанный обмен: jobs не делят случайный node_modules и каждый начинает с checkout и lockfile. В реальном проекте повтор можно сократить после измерения и явной передачи проверенного набора зависимостей. Нельзя делать cache носителем dist/ только ради экономии нескольких минут.
Проверка должна идти в deploy до команды, которая меняет staging. Сначала сравнивается revision, затем контрольные суммы, затем наличие минимального ожидаемого файла. Вынесем последовательность в отдельный фрагмент, чтобы её можно было повторить локально на учебном каталоге или внутри job без доступа к production:
\nset -eu\n\nprintf 'commit=%s\\n' \"$CI_COMMIT_SHA\"\ntest -f dist/REVISION\ntest -f dist/SHA256SUMS\ntest \"$(cat dist/REVISION)\" = \"$CI_COMMIT_SHA\"\n(cd dist && sha256sum -c SHA256SUMS)\ntest -f dist/index.html\n\n# Только после проверок:\n./scripts/deploy-staging dist/\nКоманда sha256sum -c проверяет целостность перечисленных файлов, но не бизнес-логику приложения. Для вложенного output, другого shell или Windows Runner понадобятся другие команды. Секреты и адрес staging должны приходить из защищённых настроек CI; их нельзя добавлять в YAML и выводить в диагностический лог.
| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
| Зелёный build, но другой bundle | deploy пересобирает checkout или читает cache | Найти команды сборки в deploy и источник dist/ | Передать artifact build и убрать вторую сборку |
В deploy нет dist/ | Artifact не создан, истёк или не скачан | Проверить artifacts:paths, срок хранения и dependencies | Остановить job и исправить передачу результата |
REVISION отличается | Смешаны pipeline, ветка или каталог | Сравнить файл с CI_COMMIT_SHA в том же job | Не отправлять файлы; создать pipeline нужного revision |
npm ci падает | Manifest и lockfile расходятся | Запустить чистую установку тем же образом | Согласовать lockfile и manifest отдельным commit |
| Checksum не проходит | Файл изменился либо manifest неполон | Проверить состав dist/ и команду его создания | Остановиться до upload и расследовать источник изменения |
Таблица задаёт порядок проверки, а не список советов на все случаи. Сначала определяется потерянный объект, затем проверяется его граница. Retry до этого шага скрывает нестабильность: следующий запуск может использовать другой cache или окружение и не ответит, почему первый результат отличался.
\nnpm ci на чистой среде. Несогласованный lockfile исправить до настройки deploy.verify с реальными lint и test-командами; убедиться, что ненулевой exit code не запускает build.dist/ один раз, добавить REVISION и checksum-manifest, затем объявить каталог artifact.dependencies: - build и проверками до вызова delivery-скрипта.Pipeline не доказывает, что staging принял файлы, что миграция базы безопасна или что пользовательский сценарий работает. Нужны отдельные smoke-проверки, мониторинг, правила отката и контроль доступа. Недельный expire_in в примере — срок хранения учебного artifact, а не политика релизов. Для rollback артефакт следует хранить в подходящем registry или хранилище с понятным именованием.
Checksum не защищает от скомпрометированного Runner и не подтверждает серверную конфигурацию. npm ci не фиксирует версию Node.js и состояние внешнего registry. Если ручной job должен блокировать pipeline, поведение when: manual и allow_failure: false нужно проверить на установленной версии GitLab и с реальными правами запуска.
Минимальная реализация готова, когда один staging-запуск позволяет назвать commit и состав artifact, а deploy не содержит команды сборки. Три отрицательных проверки обязательны: сбой verify блокирует build; отсутствие или несовпадение artifact блокирует deploy; mismatch revision или checksum не вызывает сетевой скрипт. Это проверяемая граница учебного pipeline, а не обещание production-надёжности.
\nstages, artifacts, dependencies и when; актуальная документация, поэтому поддержку на старой установке нужно сверять отдельно.