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 завершается зелёным, а приложение открывает старую версию. Иногда падает команда доставки с сообщением о пропущенном каталоге. Повторный запуск может убрать симптом и одновременно скрыть причину.
Цена ошибки — потеря связи между проверенным и отправленным результатом. Команда не знает, какой commit собрался, какой каталог попал в deploy и какие зависимости использовал runner. В таком состоянии нельзя уверенно повторить сбой или доказать, что исправление относится к нужному релизу.
\nТезис простой: результат сборки должен создаваться один раз, передаваться как artifact и проверяться перед внешним действием. Job deploy не должна заново получать исходники и выполнять npm ci или npm run build. Она должна получить конкретный output от job build, сверить его с commit pipeline и остановиться при любом расхождении.
В плохой конфигурации build собирает приложение, а deploy снова делает checkout, устанавливает зависимости и собирает каталог. Две строки build passed тогда относятся к разным запускам. Между ними могут измениться cache, образ runner, версия package manager, переменные окружения и рабочая директория.
Совпадение commit не доказывает совпадение output. Сборка зависит не только от Git-дерева. Важны lockfile, версия Node.js, настройки bundler, переменные окружения и порядок команд. Поэтому повторная сборка в deploy создаёт вторую точку производства релизного результата.
\nВ GitLab job artifact задаёт явную границу: ранняя job сохраняет каталог, поздняя job получает копию этого каталога. Поле dependencies ограничивает список job, чьи artifacts нужно скачать. Это делает происхождение файлов видимым в YAML и в логах.
Ниже учебный пример. Он показывает механизм, но не описывает реальный production-инцидент и не доказывает длительность, надёжность или успешность выкладки.
\nbuild:\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 не хранит доказательство этого равенства.
Исправление переносит единственную сборку в build. Там же создаются идентификатор commit и контрольные суммы. Deploy скачивает artifact, проверяет его и только потом вызывает скрипт, который меняет внешнюю среду.
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 только читает и проверяет его.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
dist/REVISION отсутствует | Build не создаёт контрактный файл или artifact не содержит путь | Проверить script build и artifacts:paths | Остановить deploy, исправить состав результата |
Revision отличается от $CI_COMMIT_SHA | Deploy читает старый или чужой каталог | Сравнить файл из 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 не использовать как обход |
«Вероятная причина» в таблице не равна доказанной. Например, checksum может не сойтись из-за того, что manifest создали до появления последнего файла. Проверка должна отделить этот случай от подмены каталога. Пока причина неизвестна, deploy остаётся заблокированным.
\nПоследняя команда job имеет побочный эффект: она отправляет файлы, вызывает API или изменяет staging. До неё pipeline должен проверить четыре свойства. Artifact пришёл от ожидаемого job. В нём есть revision. Revision совпадает с commit pipeline. Контрольные суммы и обязательные файлы сходятся.
\nПроверки должны быть жёсткими. test -f, сравнение строк и sha256sum -c должны возвращать ненулевой код при ошибке. Не стоит превращать mismatch в предупреждение или добавлять || true. Иначе лог покажет проблему, но pipeline продолжит движение к сетевой команде.
Manual deploy не заменяет эти проверки. Ручное подтверждение отвечает на вопрос «можно ли сейчас запускать этот шаг», но не доказывает происхождение каталога. Оно полезно после автоматических gate, а не вместо них.
\nnpm ci, npm install и npm run build. Для релизного каталога должен остаться один владелец.REVISION и checksum-manifest после появления всех файлов.artifacts:paths. В deploy-job указать dependency на build и удалить повторную сборку.Artifact не делает сборку воспроизводимой сам по себе. Он сохраняет уже созданный результат. Для воспроизводимости дополнительно нужны зафиксированные зависимости, контролируемый образ runner и понятные переменные окружения.
\nChecksum подтверждает целостность набора файлов после создания manifest. Он не подтверждает права доступа, безопасность секретов, корректность бизнес-логики или совместимость приложения со staging. Smoke-тесты и rollback решают другие задачи.
\ndependencies подходит для простой последовательной схемы. При переходе к needs, нескольким build-job, child pipeline или межпроектным artifacts нужно отдельно проверить, откуда deploy получает файлы. Название job само по себе не является доказательством правильного источника.
Изменение готово, если для одного pipeline можно показать commit, job-источник, список artifact и checksum-manifest. Deploy не выполняет сборку повторно. При отсутствии файла, mismatch revision или неверной checksum он завершается до delivery-script. При корректном artifact он выполняет только согласованный staging-шаг.
\nЭто проверяемый критерий, а не обещание production-результата. Он показывает, что pipeline знает происхождение отправляемого каталога и умеет остановиться до внешнего действия.
\ndependencies и других ключей pipeline.В понедельник утром дежурный инженер открыл staging после merge и увидел старый CSS. Job verify и build были зелёными, а deploy тоже завершился без ошибки. В логах не было ответа на простой вопрос: какой каталог реально отправили на стенд.
Сначала команда повторила deploy и получила тот же зелёный статус. После этого обнаружилось, что deploy-job снова делала checkout, запускала npm ci и собирала dist/. Цена такой схемы — потеря связи между проверенным и отправленным результатом: нельзя доказать, какой commit собрался, какой output попал в deploy и какие зависимости использовал runner.
Надёжная граница выглядит так: job build один раз создаёт output, сохраняет его как artifact и записывает идентификатор commit. Job deploy получает этот artifact, проверяет revision, состав файлов и checksum, а затем выполняет одну команду с внешним эффектом. Повторная сборка в deploy должна считаться отдельной ошибкой схемы.
В учебном сценарии pipeline состоит из verify, build и deploy_staging. Сначала verify проверяет код. Затем build устанавливает зависимости и создаёт dist/. После этого deploy получает рабочую директорию, но не обязан получить каталог от предыдущей job, если конфигурация не описывает передачу artifact.
В этот момент два запуска могут выглядеть одинаково в интерфейсе: оба сообщают passed, оба относятся к одному merge request. Но первый запуск произвёл каталог в build-job, а второй мог собрать другой каталог уже после checkout. Между ними меняются cache, образ runner, версия package manager, переменные окружения и рабочая директория.
Совпадение commit не доказывает совпадение output. Для результата важны lockfile, версия Node.js, настройки bundler, переменные окружения и порядок команд. В 2020 году это уже была практическая граница CI/CD: проверять нужно не только исходное дерево, но и тот набор файлов, который пересекает границу delivery.
\nРасследование становится короче, если назвать объект каждой операции. Checkout даёт исходное дерево, install — окружение зависимостей, build — новый каталог, artifact — сохранённую копию этого каталога. Deploy не должен молча становиться ещё одним build-job.
\n| Состояние | Что в нём находится | Какая ошибка возможна | Проверка |
|---|---|---|---|
| Checkout | Дерево commit pipeline | Взята другая ветка или revision | Сверить commit job с CI_COMMIT_SHA |
| Build output | Собранный каталог dist/ | Каталог неполный или собран повторно | Проверить обязательный файл и владельца output |
| Artifact | Файлы, сохранённые job и скачанные позже | Путь не включён или выбран не тот источник | Проверить artifacts:paths и dependencies |
| Deploy | Команда, меняющая staging | Внешнее действие запущено до gate | Оставить delivery-script последней командой |
Эта таблица не утверждает, что artifact защищён от всех угроз. Она только разделяет места, где можно проверить происхождение и целостность результата. Права доступа, секреты, бизнес-проверки и откат требуют отдельных механизмов.
\nНиже учебная конфигурация. Она показывает дефект границы; это не запись о реально запущенной job и не обещание, что команды выполнялись в конкретном проекте.
\nbuild:\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.
npm ci здесь не является плохой командой сама по себе: она предназначена для чистой установки в автоматизированной среде и требует согласованного lockfile. Ошибка в том, что deploy одновременно устанавливает зависимости и заново производит релизный output вместо чтения проверенного artifact.
Исправление переносит производство каталога в build. После сборки job записывает revision и создаёт manifest контрольных сумм. Deploy ограничивает загрузку artifact списком dependencies, проверяет файлы и только затем вызывает скрипт доставки.
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.
Команды предполагают POSIX shell, GNU-совместимые find, sort и sha256sum, а также каталог dist/. В другом shell или на Windows runner их нужно заменить и отдельно проверить. REVISION и SHA256SUMS — диагностический контракт учебного примера, а не подпись релиза.
У каждой строки ошибки должна быть проверка, которая отделяет одну гипотезу от другой. Например, отсутствие REVISION ещё не доказывает подмену каталога: файл мог не создаваться или мог быть исключён из artifacts:paths. Лог и YAML нужно читать вместе.
| Симптом | Гипотеза | Проверка | Действие |
|---|---|---|---|
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 build | Deploy стал вторым владельцем результата | Найти все команды сборки в YAML | Удалить повторную сборку и оставить dependency на build |
| Verify не прошёл | Ошибка теста, install или окружения | Посмотреть exit code и отчёт job | Исправить причину; не обходить gate повторным deploy |
Если checksum не сходится, сначала нужно проверить собственный manifest: в него должны попасть все файлы после завершения сборки, а сам файл manifest не должен хешироваться до момента его создания. Пока причина не установлена, staging-команда остаётся недостижимой.
\nПоследняя команда job имеет побочный эффект: она отправляет файлы, вызывает API или изменяет staging. До неё pipeline проверяет четыре свойства. Artifact пришёл от ожидаемого job. В нём есть revision. Revision совпадает с commit pipeline. Контрольные суммы и обязательные файлы сходятся.
\ntest -f, сравнение строк и sha256sum -c должны возвращать ненулевой код при ошибке. Нельзя превращать mismatch в предупреждение или добавлять || true: тогда лог покажет проблему, но shell продолжит движение к сетевой команде.
Ручное подтверждение deploy отвечает на вопрос «можно ли сейчас запускать этот шаг», но не доказывает происхождение каталога. Оно может стоять после автоматических gate. Контроль artifact, revision и checksum должно пройти до ручного или автоматического delivery.
\nnpm ci, npm install и npm run build. Для релизного каталога оставить одного владельца.REVISION и построить checksum-manifest после появления всех файлов.artifacts:paths и в deploy-job явно указать dependency на build.Artifact сохраняет уже созданный результат, но не делает сборку воспроизводимой автоматически. Для этого дополнительно нужны зафиксированные зависимости, контролируемый образ runner и явные переменные окружения. Cache следует считать ускорителем, а не источником истины для release output.
\nChecksum подтверждает целостность набора файлов после создания manifest. Он не подтверждает права доступа, безопасность секретов, корректность бизнес-логики, совместимость со staging или возможность отката. Smoke-тесты и rollback решают другие задачи.
\ndependencies подходит для линейной схемы с build-job в предыдущей стадии. При переходе к needs, нескольким build-job, child pipeline или межпроектным artifacts нужно отдельно проверить, откуда deploy получает файлы; эти варианты не следует смешивать в одном примере без проверки версии GitLab.
Изменение готово, если для одного pipeline можно показать commit, job-источник, список artifact и checksum-manifest. Deploy не выполняет сборку повторно. При отсутствии файла, несовпадении revision или неверной checksum он завершается до delivery-script. При корректном artifact он выполняет только согласованный staging-шаг.
\nstages, artifacts и dependencies.