8 lines
14 KiB
JSON
8 lines
14 KiB
JSON
{
|
||
"index": 280,
|
||
"slug": "editorial-2020-03-field-ci-pipeline",
|
||
"title": "CI/CD: как не отправить в deploy результат другой сборки",
|
||
"excerpt": "После зелёного build на стенд попадает другой каталог. Разбираем границу между сборкой и deploy, передаём один artifact, сверяем commit и останавливаем выпуск до сетевого действия.",
|
||
"contentHtml": "<p>После merge job <code>verify</code> и <code>build</code> завершаются успешно, но на staging нет ожидаемого файла. Иногда deploy завершается зелёным, а приложение открывает старую версию. Иногда падает команда доставки с сообщением о пропущенном каталоге. Повторный запуск может убрать симптом и одновременно скрыть причину.</p>\n<p>Цена ошибки — потеря связи между проверенным и отправленным результатом. Команда не знает, какой commit собрался, какой каталог попал в deploy и какие зависимости использовал runner. В таком состоянии нельзя уверенно повторить сбой или доказать, что исправление относится к нужному релизу.</p>\n<p>Тезис простой: результат сборки должен создаваться один раз, передаваться как artifact и проверяться перед внешним действием. Job deploy не должна заново получать исходники и выполнять <code>npm ci</code> или <code>npm run build</code>. Она должна получить конкретный output от job <code>build</code>, сверить его с commit pipeline и остановиться при любом расхождении.</p>\n<h2>Где ломается граница</h2>\n<p>В плохой конфигурации <code>build</code> собирает приложение, а <code>deploy</code> снова делает checkout, устанавливает зависимости и собирает каталог. Две строки <code>build passed</code> тогда относятся к разным запускам. Между ними могут измениться cache, образ runner, версия package manager, переменные окружения и рабочая директория.</p>\n<p>Совпадение commit не доказывает совпадение output. Сборка зависит не только от Git-дерева. Важны lockfile, версия Node.js, настройки bundler, переменные окружения и порядок команд. Поэтому повторная сборка в deploy создаёт вторую точку производства релизного результата.</p>\n<p>В GitLab job artifact задаёт явную границу: ранняя job сохраняет каталог, поздняя job получает копию этого каталога. Поле <code>dependencies</code> ограничивает список job, чьи artifacts нужно скачать. Это делает происхождение файлов видимым в YAML и в логах.</p>\n<h2>Антипример и рабочая схема</h2>\n<p>Ниже учебный пример. Он показывает механизм, но не описывает реальный production-инцидент и не доказывает длительность, надёжность или успешность выкладки.</p>\n<pre><code>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/</code></pre>\n<p>В этом варианте <code>deploy_staging</code> не использует <code>dist/</code> от <code>build</code>. Он создаёт новый каталог. Даже если команда обычно получает тот же результат, pipeline не хранит доказательство этого равенства.</p>\n<p>Исправление переносит единственную сборку в <code>build</code>. Там же создаются идентификатор commit и контрольные суммы. Deploy скачивает artifact, проверяет его и только потом вызывает скрипт, который меняет внешнюю среду.</p>\n<pre><code>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/</code></pre>\n<p>Команды в примере предполагают POSIX shell и каталог <code>dist/</code>. Название входного файла, способ доставки и формат checksum нужно заменить на правила конкретного приложения. Сам принцип не меняется: artifact создаёт один job, deploy только читает и проверяет его.</p>\n<figure><img src=\"/assets/editorial/2020/ci-pipeline-diagnosis-2020.svg\" alt=\"Схема проверки artifact перед deploy: build создаёт результат, deploy сверяет revision и checksum, а при ошибке останавливается до внешнего действия\"><figcaption>Граница между build и deploy: неизвестный artifact не должен становиться сетевым действием.</figcaption></figure>\n<h2>Как читать симптом</h2>\n<table><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td><code>dist/REVISION</code> отсутствует</td><td>Build не создаёт контрактный файл или artifact не содержит путь</td><td>Проверить script build и <code>artifacts:paths</code></td><td>Остановить deploy, исправить состав результата</td></tr><tr><td>Revision отличается от <code>$CI_COMMIT_SHA</code></td><td>Deploy читает старый или чужой каталог</td><td>Сравнить файл из artifact с переменной job</td><td>Создать pipeline нужного commit и найти источник каталога</td></tr><tr><td><code>sha256sum -c</code> завершается с ошибкой</td><td>Файл изменился после manifest или передан неполный набор</td><td>Проверить момент создания manifest и список files</td><td>Не вызывать delivery-script; собрать новый artifact</td></tr><tr><td>Deploy запускает <code>npm run build</code></td><td>Результат build не передаётся как dependency</td><td>Прочитать YAML и список скачанных artifacts</td><td>Убрать повторную сборку, указать <code>dependencies: [build]</code></td></tr><tr><td>Verify не прошёл</td><td>Ошибка теста, install или окружения</td><td>Посмотреть exit code и отчёт job</td><td>Исправить причину; build не использовать как обход</td></tr></tbody></table>\n<p>«Вероятная причина» в таблице не равна доказанной. Например, checksum может не сойтись из-за того, что manifest создали до появления последнего файла. Проверка должна отделить этот случай от подмены каталога. Пока причина неизвестна, deploy остаётся заблокированным.</p>\n<h2>Проверка до внешнего действия</h2>\n<p>Последняя команда job имеет побочный эффект: она отправляет файлы, вызывает API или изменяет staging. До неё pipeline должен проверить четыре свойства. Artifact пришёл от ожидаемого job. В нём есть revision. Revision совпадает с commit pipeline. Контрольные суммы и обязательные файлы сходятся.</p>\n<p>Проверки должны быть жёсткими. <code>test -f</code>, сравнение строк и <code>sha256sum -c</code> должны возвращать ненулевой код при ошибке. Не стоит превращать mismatch в предупреждение или добавлять <code>|| true</code>. Иначе лог покажет проблему, но pipeline продолжит движение к сетевой команде.</p>\n<p>Manual deploy не заменяет эти проверки. Ручное подтверждение отвечает на вопрос «можно ли сейчас запускать этот шаг», но не доказывает происхождение каталога. Оно полезно после автоматических gate, а не вместо них.</p>\n<h2>Порядок действий</h2>\n<ol><li>Остановить deploy до команды, которая меняет внешнюю среду. Записать имя job, revision и стадию сбоя.</li><li>Найти в YAML все вызовы <code>npm ci</code>, <code>npm install</code> и <code>npm run build</code>. Для релизного каталога должен остаться один владелец.</li><li>В build-job создать output, <code>REVISION</code> и checksum-manifest после появления всех файлов.</li><li>Включить каталог в <code>artifacts:paths</code>. В deploy-job указать dependency на build и удалить повторную сборку.</li><li>Добавить проверки существования, revision, checksum и обязательного файла. Каждую проверку оставить до delivery-script.</li><li>Провести отрицательные проверки: убрать artifact, изменить revision и испортить файл после создания manifest. Во всех случаях job должна завершиться до внешнего действия.</li><li>Отдельно проверить staging с согласованными доступами и откатом. Результат staging не переносить на production без новой проверки.</li></ol>\n<h2>Ограничения</h2>\n<p>Artifact не делает сборку воспроизводимой сам по себе. Он сохраняет уже созданный результат. Для воспроизводимости дополнительно нужны зафиксированные зависимости, контролируемый образ runner и понятные переменные окружения.</p>\n<p>Checksum подтверждает целостность набора файлов после создания manifest. Он не подтверждает права доступа, безопасность секретов, корректность бизнес-логики или совместимость приложения со staging. Smoke-тесты и rollback решают другие задачи.</p>\n<p><code>dependencies</code> подходит для простой последовательной схемы. При переходе к <code>needs</code>, нескольким build-job, child pipeline или межпроектным artifacts нужно отдельно проверить, откуда deploy получает файлы. Название job само по себе не является доказательством правильного источника.</p>\n<h2>Критерий готовности</h2>\n<p>Изменение готово, если для одного pipeline можно показать commit, job-источник, список artifact и checksum-manifest. Deploy не выполняет сборку повторно. При отсутствии файла, mismatch revision или неверной checksum он завершается до delivery-script. При корректном artifact он выполняет только согласованный staging-шаг.</p>\n<p>Это проверяемый критерий, а не обещание production-результата. Он показывает, что pipeline знает происхождение отправляемого каталога и умеет остановиться до внешнего действия.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.gitlab.com/ci/jobs/job_artifacts/\" target=\"_blank\" rel=\"noopener\">GitLab Docs: Job artifacts</a> — создание artifacts и управление тем, какие job их получают.</li><li><a href=\"https://docs.gitlab.com/ci/yaml/\" target=\"_blank\" rel=\"noopener\">GitLab Docs: CI/CD YAML syntax reference</a> — назначение <code>dependencies</code> и других ключей pipeline.</li><li><a href=\"https://docs.gitlab.com/ci/yaml/lint/\" target=\"_blank\" rel=\"noopener\">GitLab Docs: Validate CI/CD configuration</a> — проверка синтаксиса и моделирование создания pipeline.</li></ul>"
|
||
}
|