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. Цена ошибки — не только один неудачный релиз. Команда тратит время на сравнение каталогов, откладывает откат и рискует повторно отправить тот же неизвестный результат.

\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" + "contentHtml": "

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

\n

Для первого контура не нужно строить большой release-комплекс. Достаточно назвать входы, один раз создать результат и передать его следующему job. Ниже — учебная схема GitLab CI/CD в историческом контексте марта 2020 года: образ node:12-alpine, npm ci, три стадии и ручной staging deploy. Она не изображает реальный production-релиз и не заменяет проверку версии GitLab, Runner, shell, прав и команд конкретного проекта.

\n
\"Схема
Проверка и доставка разделены одним артефактом. Cache ускоряет установку, но не становится источником файлов для deploy.
\n

Сначала фиксируем контракт выпуска

\n

Входом служат revision исходников и lockfile зависимостей. Job verify запускает lint и тесты; любой ненулевой exit code останавливает следующий этап. Job build создаёт dist/, записывает туда CI_COMMIT_SHA и список контрольных сумм. Job deploy_staging получает только этот результат, проверяет его до сетевого вызова и не собирает проект заново.

\n

Такой контракт отвечает на два разных вопроса. REVISION связывает каталог с commit текущего pipeline. SHA256SUMS показывает, что файлы внутри полученного artifact не изменились между сборкой и проверкой. Ни один из файлов не является подписью релиза: они не защищают runner, сервер, registry или секреты. Их роль уже: сделать подмену наблюдаемой и остановить job до побочного эффекта.

\n

Cache и artifact отвечают за разное

\n

Cache хранит данные, которые можно получить заново. В Node-проекте это, например, каталог npm-кэша. Его отсутствие должно увеличить время установки, но не менять заявленный результат выпуска. Artifact — файл или каталог, который конкретный job сохраняет для следующих job. Если deploy читает cache вместо artifact, pipeline теряет владельца результата: неизвестно, кто создал каталог и к какому запуску он относится.

\n

Порядок стадий задаёт маршрут verify → build → release. При этом одного порядка недостаточно: deploy должен явно указать dependencies: - build, чтобы получить artifact именно этого job. В build можно записать dependencies: [], тем самым не рассчитывать на файлы предыдущих job. Если проекту понадобится передать отчёт из verify, это следует добавить как отдельный, названный контракт, а не использовать общий рабочий каталог.

\n

Учебная конфигурация GitLab CI/CD

\n

Пример рассчитан на приложение, которое публикует каталог dist/. Имена команд lint, test и ./scripts/deploy-staging условны. Перед применением конфигурацию нужно проверить через CI Lint и выполнить на том образе и Runner, которые используются в проекте.

\n
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, наоборот, создаётся до расчёта сумм и входит в проверяемый набор.

\n

Build повторяет npm ci, хотя verify уже устанавливал зависимости. Это осознанный обмен: jobs не делят случайный node_modules и каждый начинает с checkout и lockfile. В реальном проекте повтор можно сократить после измерения и явной передачи проверенного набора зависимостей. Нельзя делать cache носителем dist/ только ради экономии нескольких минут.

\n

Проверяем artifact до сетевого действия

\n

Проверка должна идти в deploy до команды, которая меняет staging. Сначала сравнивается revision, затем контрольные суммы, затем наличие минимального ожидаемого файла. Вынесем последовательность в отдельный фрагмент, чтобы её можно было повторить локально на учебном каталоге или внутри job без доступа к production:

\n
set -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 и выводить в диагностический лог.

\n
Наблюдаемый симптом и безопасное действие
СимптомГипотезаПроверкаДействие
Зелёный build, но другой bundledeploy пересобирает 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 и расследовать источник изменения
\n

Таблица задаёт порядок проверки, а не список советов на все случаи. Сначала определяется потерянный объект, затем проверяется его граница. Retry до этого шага скрывает нестабильность: следующий запуск может использовать другой cache или окружение и не ответит, почему первый результат отличался.

\n

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

\n
  1. Записать commit, lockfile, образ Runner, команду build и фактический путь результата.
  2. Проверить npm ci на чистой среде. Несогласованный lockfile исправить до настройки deploy.
  3. Добавить verify с реальными lint и test-командами; убедиться, что ненулевой exit code не запускает build.
  4. Собрать dist/ один раз, добавить REVISION и checksum-manifest, затем объявить каталог artifact.
  5. Настроить deploy с dependencies: - build и проверками до вызова delivery-скрипта.
  6. На staging отдельно проверить провал verify, отсутствие artifact, неверный revision и изменение файла.
\n

Что эта схема не доказывает

\n

Pipeline не доказывает, что staging принял файлы, что миграция базы безопасна или что пользовательский сценарий работает. Нужны отдельные smoke-проверки, мониторинг, правила отката и контроль доступа. Недельный expire_in в примере — срок хранения учебного artifact, а не политика релизов. Для rollback артефакт следует хранить в подходящем registry или хранилище с понятным именованием.

\n

Checksum не защищает от скомпрометированного Runner и не подтверждает серверную конфигурацию. npm ci не фиксирует версию Node.js и состояние внешнего registry. Если ручной job должен блокировать pipeline, поведение when: manual и allow_failure: false нужно проверить на установленной версии GitLab и с реальными правами запуска.

\n

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

\n

Минимальная реализация готова, когда один staging-запуск позволяет назвать commit и состав artifact, а deploy не содержит команды сборки. Три отрицательных проверки обязательны: сбой verify блокирует build; отсутствие или несовпадение artifact блокирует deploy; mismatch revision или checksum не вызывает сетевой скрипт. Это проверяемая граница учебного pipeline, а не обещание production-надёжности.

\n

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

\n" }