diff --git a/editorial/agent-rewrites/280.json b/editorial/agent-rewrites/280.json index 69a6929..fa69043 100644 --- a/editorial/agent-rewrites/280.json +++ b/editorial/agent-rewrites/280.json @@ -1,7 +1,7 @@ { "index": 280, "slug": "editorial-2020-03-field-ci-pipeline", - "title": "CI/CD: как не отправить в deploy результат другой сборки", - "excerpt": "После зелёного build на стенд попадает другой каталог. Разбираем границу между сборкой и deploy, передаём один artifact, сверяем commit и останавливаем выпуск до сетевого действия.", - "contentHtml": "

После merge job verify и build завершаются успешно, но на staging нет ожидаемого файла. Иногда deploy завершается зелёным, а приложение открывает старую версию. Иногда падает команда доставки с сообщением о пропущенном каталоге. Повторный запуск может убрать симптом и одновременно скрыть причину.

\n

Цена ошибки — потеря связи между проверенным и отправленным результатом. Команда не знает, какой commit собрался, какой каталог попал в deploy и какие зависимости использовал runner. В таком состоянии нельзя уверенно повторить сбой или доказать, что исправление относится к нужному релизу.

\n

Тезис простой: результат сборки должен создаваться один раз, передаваться как artifact и проверяться перед внешним действием. Job deploy не должна заново получать исходники и выполнять npm ci или npm run build. Она должна получить конкретный output от job build, сверить его с commit pipeline и остановиться при любом расхождении.

\n

Где ломается граница

\n

В плохой конфигурации build собирает приложение, а deploy снова делает checkout, устанавливает зависимости и собирает каталог. Две строки build passed тогда относятся к разным запускам. Между ними могут измениться cache, образ runner, версия package manager, переменные окружения и рабочая директория.

\n

Совпадение commit не доказывает совпадение output. Сборка зависит не только от Git-дерева. Важны lockfile, версия Node.js, настройки bundler, переменные окружения и порядок команд. Поэтому повторная сборка в deploy создаёт вторую точку производства релизного результата.

\n

В GitLab job artifact задаёт явную границу: ранняя job сохраняет каталог, поздняя job получает копию этого каталога. Поле dependencies ограничивает список job, чьи artifacts нужно скачать. Это делает происхождение файлов видимым в YAML и в логах.

\n

Антипример и рабочая схема

\n

Ниже учебный пример. Он показывает механизм, но не описывает реальный production-инцидент и не доказывает длительность, надёжность или успешность выкладки.

\n
build:\n  stage: build\n  script:\n    - npm ci\n    - npm run build\n  artifacts:\n    paths:\n      - dist/\n\ndeploy_staging:\n  stage: deploy\n  script:\n    - npm ci\n    - npm run build\n    - ./deploy-staging.sh dist/
\n

В этом варианте deploy_staging не использует dist/ от build. Он создаёт новый каталог. Даже если команда обычно получает тот же результат, pipeline не хранит доказательство этого равенства.

\n

Исправление переносит единственную сборку в build. Там же создаются идентификатор commit и контрольные суммы. Deploy скачивает artifact, проверяет его и только потом вызывает скрипт, который меняет внешнюю среду.

\n
build:\n  stage: build\n  script:\n    - npm ci\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\ndeploy_staging:\n  stage: deploy\n  dependencies:\n    - build\n  script:\n    - test -f dist/REVISION\n    - test \"$(cat dist/REVISION)\" = \"$CI_COMMIT_SHA\"\n    - (cd dist && sha256sum -c SHA256SUMS)\n    - test -f dist/index.html\n    - ./deploy-staging.sh dist/
\n

Команды в примере предполагают POSIX shell и каталог dist/. Название входного файла, способ доставки и формат checksum нужно заменить на правила конкретного приложения. Сам принцип не меняется: artifact создаёт один job, deploy только читает и проверяет его.

\n
\"Схема
Граница между build и deploy: неизвестный artifact не должен становиться сетевым действием.
\n

Как читать симптом

\n
СимптомПричинаПроверкаДействие
dist/REVISION отсутствуетBuild не создаёт контрактный файл или artifact не содержит путьПроверить script build и artifacts:pathsОстановить deploy, исправить состав результата
Revision отличается от $CI_COMMIT_SHADeploy читает старый или чужой каталогСравнить файл из artifact с переменной jobСоздать pipeline нужного commit и найти источник каталога
sha256sum -c завершается с ошибкойФайл изменился после manifest или передан неполный наборПроверить момент создания manifest и список filesНе вызывать delivery-script; собрать новый artifact
Deploy запускает npm run buildРезультат build не передаётся как dependencyПрочитать YAML и список скачанных artifactsУбрать повторную сборку, указать dependencies: [build]
Verify не прошёлОшибка теста, install или окруженияПосмотреть exit code и отчёт jobИсправить причину; build не использовать как обход
\n

«Вероятная причина» в таблице не равна доказанной. Например, checksum может не сойтись из-за того, что manifest создали до появления последнего файла. Проверка должна отделить этот случай от подмены каталога. Пока причина неизвестна, deploy остаётся заблокированным.

\n

Проверка до внешнего действия

\n

Последняя команда job имеет побочный эффект: она отправляет файлы, вызывает API или изменяет staging. До неё pipeline должен проверить четыре свойства. Artifact пришёл от ожидаемого job. В нём есть revision. Revision совпадает с commit pipeline. Контрольные суммы и обязательные файлы сходятся.

\n

Проверки должны быть жёсткими. test -f, сравнение строк и sha256sum -c должны возвращать ненулевой код при ошибке. Не стоит превращать mismatch в предупреждение или добавлять || true. Иначе лог покажет проблему, но pipeline продолжит движение к сетевой команде.

\n

Manual deploy не заменяет эти проверки. Ручное подтверждение отвечает на вопрос «можно ли сейчас запускать этот шаг», но не доказывает происхождение каталога. Оно полезно после автоматических gate, а не вместо них.

\n

Порядок действий

\n
  1. Остановить deploy до команды, которая меняет внешнюю среду. Записать имя job, revision и стадию сбоя.
  2. Найти в YAML все вызовы npm ci, npm install и npm run build. Для релизного каталога должен остаться один владелец.
  3. В build-job создать output, REVISION и checksum-manifest после появления всех файлов.
  4. Включить каталог в artifacts:paths. В deploy-job указать dependency на build и удалить повторную сборку.
  5. Добавить проверки существования, revision, checksum и обязательного файла. Каждую проверку оставить до delivery-script.
  6. Провести отрицательные проверки: убрать artifact, изменить revision и испортить файл после создания manifest. Во всех случаях job должна завершиться до внешнего действия.
  7. Отдельно проверить staging с согласованными доступами и откатом. Результат staging не переносить на production без новой проверки.
\n

Ограничения

\n

Artifact не делает сборку воспроизводимой сам по себе. Он сохраняет уже созданный результат. Для воспроизводимости дополнительно нужны зафиксированные зависимости, контролируемый образ runner и понятные переменные окружения.

\n

Checksum подтверждает целостность набора файлов после создания manifest. Он не подтверждает права доступа, безопасность секретов, корректность бизнес-логики или совместимость приложения со staging. Smoke-тесты и rollback решают другие задачи.

\n

dependencies подходит для простой последовательной схемы. При переходе к needs, нескольким build-job, child pipeline или межпроектным artifacts нужно отдельно проверить, откуда deploy получает файлы. Название job само по себе не является доказательством правильного источника.

\n

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

\n

Изменение готово, если для одного pipeline можно показать commit, job-источник, список artifact и checksum-manifest. Deploy не выполняет сборку повторно. При отсутствии файла, mismatch revision или неверной checksum он завершается до delivery-script. При корректном artifact он выполняет только согласованный staging-шаг.

\n

Это проверяемый критерий, а не обещание production-результата. Он показывает, что pipeline знает происхождение отправляемого каталога и умеет остановиться до внешнего действия.

\n

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

\n" + "title": "CI/CD: как связать проверенный build с deploy", + "excerpt": "После зелёного build на staging оказывается другой каталог. Разбираем границу между сборкой и deploy: передаём один artifact, сверяем commit и останавливаем выпуск до сетевого действия.", + "contentHtml": "

В понедельник утром дежурный инженер открыл staging после merge и увидел старый CSS. Job verify и build были зелёными, а deploy тоже завершился без ошибки. В логах не было ответа на простой вопрос: какой каталог реально отправили на стенд.

\n

Сначала команда повторила deploy и получила тот же зелёный статус. После этого обнаружилось, что deploy-job снова делала checkout, запускала npm ci и собирала dist/. Цена такой схемы — потеря связи между проверенным и отправленным результатом: нельзя доказать, какой commit собрался, какой output попал в deploy и какие зависимости использовал runner.

\n

Надёжная граница выглядит так: job build один раз создаёт output, сохраняет его как artifact и записывает идентификатор commit. Job deploy получает этот artifact, проверяет revision, состав файлов и checksum, а затем выполняет одну команду с внешним эффектом. Повторная сборка в deploy должна считаться отдельной ошибкой схемы.

\n

Сценарий: зелёный build и чужой каталог

\n

В учебном сценарии pipeline состоит из verify, build и deploy_staging. Сначала verify проверяет код. Затем build устанавливает зависимости и создаёт dist/. После этого deploy получает рабочую директорию, но не обязан получить каталог от предыдущей job, если конфигурация не описывает передачу artifact.

\n

В этот момент два запуска могут выглядеть одинаково в интерфейсе: оба сообщают passed, оба относятся к одному merge request. Но первый запуск произвёл каталог в build-job, а второй мог собрать другой каталог уже после checkout. Между ними меняются cache, образ runner, версия package manager, переменные окружения и рабочая директория.

\n

Совпадение commit не доказывает совпадение output. Для результата важны lockfile, версия Node.js, настройки bundler, переменные окружения и порядок команд. В 2020 году это уже была практическая граница CI/CD: проверять нужно не только исходное дерево, но и тот набор файлов, который пересекает границу delivery.

\n

Четыре состояния, которые нельзя смешивать

\n

Расследование становится короче, если назвать объект каждой операции. Checkout даёт исходное дерево, install — окружение зависимостей, build — новый каталог, artifact — сохранённую копию этого каталога. Deploy не должен молча становиться ещё одним build-job.

\n
Что именно проверяем на границе между build и deploy
СостояниеЧто в нём находитсяКакая ошибка возможнаПроверка
CheckoutДерево commit pipelineВзята другая ветка или revisionСверить commit job с CI_COMMIT_SHA
Build outputСобранный каталог dist/Каталог неполный или собран повторноПроверить обязательный файл и владельца output
ArtifactФайлы, сохранённые job и скачанные позжеПуть не включён или выбран не тот источникПроверить artifacts:paths и dependencies
DeployКоманда, меняющая stagingВнешнее действие запущено до gateОставить delivery-script последней командой
\n

Эта таблица не утверждает, что artifact защищён от всех угроз. Она только разделяет места, где можно проверить происхождение и целостность результата. Права доступа, секреты, бизнес-проверки и откат требуют отдельных механизмов.

\n

Антипример: deploy производит результат заново

\n

Ниже учебная конфигурация. Она показывает дефект границы; это не запись о реально запущенной job и не обещание, что команды выполнялись в конкретном проекте.

\n
build:\n  stage: build\n  script:\n    - npm ci\n    - npm run build\n  artifacts:\n    paths:\n      - dist/\n\ndeploy_staging:\n  stage: deploy\n  script:\n    - npm ci\n    - npm run build\n    - ./deploy-staging.sh dist/
\n

В этом варианте deploy_staging создаёт собственный dist/. Он не использует output от build, даже если в рабочей директории случайно появляется каталог с тем же именем. Две сборки могут иметь один commit и разные зависимости, переменные или содержимое cache.

\n

npm ci здесь не является плохой командой сама по себе: она предназначена для чистой установки в автоматизированной среде и требует согласованного lockfile. Ошибка в том, что deploy одновременно устанавливает зависимости и заново производит релизный output вместо чтения проверенного artifact.

\n

Рабочая схема: один владелец output

\n

Исправление переносит производство каталога в build. После сборки job записывает revision и создаёт manifest контрольных сумм. Deploy ограничивает загрузку artifact списком dependencies, проверяет файлы и только затем вызывает скрипт доставки.

\n
build:\n  stage: build\n  script:\n    - npm ci\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\ndeploy_staging:\n  stage: deploy\n  dependencies:\n    - build\n  script:\n    - test -f dist/REVISION\n    - test "$(cat dist/REVISION)" = "$CI_COMMIT_SHA"\n    - (cd dist && sha256sum -c SHA256SUMS)\n    - test -f dist/index.html\n    - ./deploy-staging.sh dist/
\n

В GitLab artifact — это файлы и каталоги, прикреплённые к job. По умолчанию job более поздней стадии получает artifacts предыдущих стадий, а dependencies сужает список источников. Поэтому в примере важно не только наличие dist/, но и явная связь deploy с job build.

\n

Команды предполагают POSIX shell, GNU-совместимые find, sort и sha256sum, а также каталог dist/. В другом shell или на Windows runner их нужно заменить и отдельно проверить. REVISION и SHA256SUMS — диагностический контракт учебного примера, а не подпись релиза.

\n
\"Схема
Граница между build и deploy: неизвестный artifact не должен становиться сетевым действием.
\n

Как читать симптом и проверять гипотезу

\n

У каждой строки ошибки должна быть проверка, которая отделяет одну гипотезу от другой. Например, отсутствие REVISION ещё не доказывает подмену каталога: файл мог не создаваться или мог быть исключён из artifacts:paths. Лог и YAML нужно читать вместе.

\n
Диагностическая матрица для остановленного deploy
СимптомГипотезаПроверкаДействие
dist/REVISION отсутствуетBuild не создаёт файл или artifact не содержит путьПроверить script build и состав скачанного artifactОстановить deploy и исправить контракт output
Revision отличается от $CI_COMMIT_SHAПолучен старый или чужой каталогСравнить файл из artifact с переменной jobНе вызывать delivery-script; найти источник и создать нужный pipeline
sha256sum -c завершается с ошибкойФайл изменён или manifest создан слишком раноСравнить список файлов и порядок командСобрать новый artifact после завершения output
Deploy запускает npm run buildDeploy стал вторым владельцем результатаНайти все команды сборки в YAMLУдалить повторную сборку и оставить dependency на build
Verify не прошёлОшибка теста, install или окруженияПосмотреть exit code и отчёт jobИсправить причину; не обходить gate повторным deploy
\n

Если checksum не сходится, сначала нужно проверить собственный manifest: в него должны попасть все файлы после завершения сборки, а сам файл manifest не должен хешироваться до момента его создания. Пока причина не установлена, staging-команда остаётся недостижимой.

\n

Gate перед внешним действием

\n

Последняя команда job имеет побочный эффект: она отправляет файлы, вызывает API или изменяет staging. До неё pipeline проверяет четыре свойства. Artifact пришёл от ожидаемого job. В нём есть revision. Revision совпадает с commit pipeline. Контрольные суммы и обязательные файлы сходятся.

\n

test -f, сравнение строк и sha256sum -c должны возвращать ненулевой код при ошибке. Нельзя превращать mismatch в предупреждение или добавлять || true: тогда лог покажет проблему, но shell продолжит движение к сетевой команде.

\n

Ручное подтверждение deploy отвечает на вопрос «можно ли сейчас запускать этот шаг», но не доказывает происхождение каталога. Оно может стоять после автоматических gate. Контроль artifact, revision и checksum должно пройти до ручного или автоматического delivery.

\n

Порядок действий при исправлении

\n
  1. Остановить deploy до команды, меняющей внешнюю среду. Записать имя job, revision и стадию сбоя.
  2. Найти в YAML все вызовы npm ci, npm install и npm run build. Для релизного каталога оставить одного владельца.
  3. В build-job создать output, добавить REVISION и построить checksum-manifest после появления всех файлов.
  4. Включить каталог в artifacts:paths и в deploy-job явно указать dependency на build.
  5. Удалить повторную сборку из deploy. До delivery-script оставить проверки существования, revision, checksum и обязательного файла.
  6. Проверить отрицательные пути: убрать artifact, изменить revision и испортить файл после создания manifest. В каждом случае job должна завершиться до внешнего действия.
  7. Провести staging-проверку с согласованными доступами и откатом. Результат staging не переносить на production без новой проверки.
\n

Ограничения и критерий готовности

\n

Artifact сохраняет уже созданный результат, но не делает сборку воспроизводимой автоматически. Для этого дополнительно нужны зафиксированные зависимости, контролируемый образ runner и явные переменные окружения. Cache следует считать ускорителем, а не источником истины для release output.

\n

Checksum подтверждает целостность набора файлов после создания manifest. Он не подтверждает права доступа, безопасность секретов, корректность бизнес-логики, совместимость со staging или возможность отката. Smoke-тесты и rollback решают другие задачи.

\n

dependencies подходит для линейной схемы с build-job в предыдущей стадии. При переходе к needs, нескольким build-job, child pipeline или межпроектным artifacts нужно отдельно проверить, откуда deploy получает файлы; эти варианты не следует смешивать в одном примере без проверки версии GitLab.

\n

Изменение готово, если для одного pipeline можно показать commit, job-источник, список artifact и checksum-manifest. Deploy не выполняет сборку повторно. При отсутствии файла, несовпадении revision или неверной checksum он завершается до delivery-script. При корректном artifact он выполняет только согласованный staging-шаг.

\n

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

\n" }