8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 280,
|
||
"slug": "editorial-2020-03-field-ci-pipeline",
|
||
"title": "CI/CD: как связать проверенный build с deploy",
|
||
"excerpt": "После зелёного build на staging оказывается другой каталог. Разбираем границу между сборкой и deploy: передаём один artifact, сверяем commit и останавливаем выпуск до сетевого действия.",
|
||
"contentHtml": "<p>В понедельник утром дежурный инженер открыл staging после merge и увидел старый CSS. Job <code>verify</code> и <code>build</code> были зелёными, а <code>deploy</code> тоже завершился без ошибки. В логах не было ответа на простой вопрос: какой каталог реально отправили на стенд.</p>\n<p>Сначала команда повторила deploy и получила тот же зелёный статус. После этого обнаружилось, что deploy-job снова делала checkout, запускала <code>npm ci</code> и собирала <code>dist/</code>. Цена такой схемы — потеря связи между проверенным и отправленным результатом: нельзя доказать, какой commit собрался, какой output попал в deploy и какие зависимости использовал runner.</p>\n<p>Надёжная граница выглядит так: job <code>build</code> один раз создаёт output, сохраняет его как artifact и записывает идентификатор commit. Job <code>deploy</code> получает этот artifact, проверяет revision, состав файлов и checksum, а затем выполняет одну команду с внешним эффектом. Повторная сборка в deploy должна считаться отдельной ошибкой схемы.</p>\n<h2>Сценарий: зелёный build и чужой каталог</h2>\n<p>В учебном сценарии pipeline состоит из <code>verify</code>, <code>build</code> и <code>deploy_staging</code>. Сначала <code>verify</code> проверяет код. Затем <code>build</code> устанавливает зависимости и создаёт <code>dist/</code>. После этого deploy получает рабочую директорию, но не обязан получить каталог от предыдущей job, если конфигурация не описывает передачу artifact.</p>\n<p>В этот момент два запуска могут выглядеть одинаково в интерфейсе: оба сообщают <code>passed</code>, оба относятся к одному merge request. Но первый запуск произвёл каталог в build-job, а второй мог собрать другой каталог уже после checkout. Между ними меняются cache, образ runner, версия package manager, переменные окружения и рабочая директория.</p>\n<p>Совпадение commit не доказывает совпадение output. Для результата важны lockfile, версия Node.js, настройки bundler, переменные окружения и порядок команд. В 2020 году это уже была практическая граница CI/CD: проверять нужно не только исходное дерево, но и тот набор файлов, который пересекает границу delivery.</p>\n<h2>Четыре состояния, которые нельзя смешивать</h2>\n<p>Расследование становится короче, если назвать объект каждой операции. Checkout даёт исходное дерево, install — окружение зависимостей, build — новый каталог, artifact — сохранённую копию этого каталога. Deploy не должен молча становиться ещё одним build-job.</p>\n<table><caption>Что именно проверяем на границе между build и deploy</caption><thead><tr><th>Состояние</th><th>Что в нём находится</th><th>Какая ошибка возможна</th><th>Проверка</th></tr></thead><tbody><tr><td>Checkout</td><td>Дерево commit pipeline</td><td>Взята другая ветка или revision</td><td>Сверить commit job с <code>CI_COMMIT_SHA</code></td></tr><tr><td>Build output</td><td>Собранный каталог <code>dist/</code></td><td>Каталог неполный или собран повторно</td><td>Проверить обязательный файл и владельца output</td></tr><tr><td>Artifact</td><td>Файлы, сохранённые job и скачанные позже</td><td>Путь не включён или выбран не тот источник</td><td>Проверить <code>artifacts:paths</code> и <code>dependencies</code></td></tr><tr><td>Deploy</td><td>Команда, меняющая staging</td><td>Внешнее действие запущено до gate</td><td>Оставить delivery-script последней командой</td></tr></tbody></table>\n<p>Эта таблица не утверждает, что artifact защищён от всех угроз. Она только разделяет места, где можно проверить происхождение и целостность результата. Права доступа, секреты, бизнес-проверки и откат требуют отдельных механизмов.</p>\n<h2>Антипример: deploy производит результат заново</h2>\n<p>Ниже учебная конфигурация. Она показывает дефект границы; это не запись о реально запущенной job и не обещание, что команды выполнялись в конкретном проекте.</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>. Он не использует output от <code>build</code>, даже если в рабочей директории случайно появляется каталог с тем же именем. Две сборки могут иметь один commit и разные зависимости, переменные или содержимое cache.</p>\n<p><code>npm ci</code> здесь не является плохой командой сама по себе: она предназначена для чистой установки в автоматизированной среде и требует согласованного lockfile. Ошибка в том, что deploy одновременно устанавливает зависимости и заново производит релизный output вместо чтения проверенного artifact.</p>\n<h2>Рабочая схема: один владелец output</h2>\n<p>Исправление переносит производство каталога в <code>build</code>. После сборки job записывает revision и создаёт manifest контрольных сумм. Deploy ограничивает загрузку artifact списком <code>dependencies</code>, проверяет файлы и только затем вызывает скрипт доставки.</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>В GitLab artifact — это файлы и каталоги, прикреплённые к job. По умолчанию job более поздней стадии получает artifacts предыдущих стадий, а <code>dependencies</code> сужает список источников. Поэтому в примере важно не только наличие <code>dist/</code>, но и явная связь deploy с job <code>build</code>.</p>\n<p>Команды предполагают POSIX shell, GNU-совместимые <code>find</code>, <code>sort</code> и <code>sha256sum</code>, а также каталог <code>dist/</code>. В другом shell или на Windows runner их нужно заменить и отдельно проверить. <code>REVISION</code> и <code>SHA256SUMS</code> — диагностический контракт учебного примера, а не подпись релиза.</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<p>У каждой строки ошибки должна быть проверка, которая отделяет одну гипотезу от другой. Например, отсутствие <code>REVISION</code> ещё не доказывает подмену каталога: файл мог не создаваться или мог быть исключён из <code>artifacts:paths</code>. Лог и YAML нужно читать вместе.</p>\n<table><caption>Диагностическая матрица для остановленного deploy</caption><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 и состав скачанного artifact</td><td>Остановить deploy и исправить контракт output</td></tr><tr><td>Revision отличается от <code>$CI_COMMIT_SHA</code></td><td>Получен старый или чужой каталог</td><td>Сравнить файл из artifact с переменной job</td><td>Не вызывать delivery-script; найти источник и создать нужный pipeline</td></tr><tr><td><code>sha256sum -c</code> завершается с ошибкой</td><td>Файл изменён или manifest создан слишком рано</td><td>Сравнить список файлов и порядок команд</td><td>Собрать новый artifact после завершения output</td></tr><tr><td>Deploy запускает <code>npm run build</code></td><td>Deploy стал вторым владельцем результата</td><td>Найти все команды сборки в YAML</td><td>Удалить повторную сборку и оставить dependency на build</td></tr><tr><td>Verify не прошёл</td><td>Ошибка теста, install или окружения</td><td>Посмотреть exit code и отчёт job</td><td>Исправить причину; не обходить gate повторным deploy</td></tr></tbody></table>\n<p>Если checksum не сходится, сначала нужно проверить собственный manifest: в него должны попасть все файлы после завершения сборки, а сам файл manifest не должен хешироваться до момента его создания. Пока причина не установлена, staging-команда остаётся недостижимой.</p>\n<h2>Gate перед внешним действием</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>: тогда лог покажет проблему, но shell продолжит движение к сетевой команде.</p>\n<p>Ручное подтверждение deploy отвечает на вопрос «можно ли сейчас запускать этот шаг», но не доказывает происхождение каталога. Оно может стоять после автоматических gate. Контроль artifact, revision и checksum должно пройти до ручного или автоматического delivery.</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>Удалить повторную сборку из deploy. До delivery-script оставить проверки существования, revision, checksum и обязательного файла.</li><li>Проверить отрицательные пути: убрать artifact, изменить revision и испортить файл после создания manifest. В каждом случае job должна завершиться до внешнего действия.</li><li>Провести staging-проверку с согласованными доступами и откатом. Результат staging не переносить на production без новой проверки.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Artifact сохраняет уже созданный результат, но не делает сборку воспроизводимой автоматически. Для этого дополнительно нужны зафиксированные зависимости, контролируемый образ runner и явные переменные окружения. Cache следует считать ускорителем, а не источником истины для release output.</p>\n<p>Checksum подтверждает целостность набора файлов после создания manifest. Он не подтверждает права доступа, безопасность секретов, корректность бизнес-логики, совместимость со staging или возможность отката. Smoke-тесты и rollback решают другие задачи.</p>\n<p><code>dependencies</code> подходит для линейной схемы с build-job в предыдущей стадии. При переходе к <code>needs</code>, нескольким build-job, child pipeline или межпроектным artifacts нужно отдельно проверить, откуда deploy получает файлы; эти варианты не следует смешивать в одном примере без проверки версии GitLab.</p>\n<p>Изменение готово, если для одного pipeline можно показать commit, job-источник, список artifact и checksum-manifest. Deploy не выполняет сборку повторно. При отсутствии файла, несовпадении revision или неверной checksum он завершается до delivery-script. При корректном artifact он выполняет только согласованный staging-шаг.</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>stages</code>, <code>artifacts</code> и <code>dependencies</code>.</li><li><a href=\"https://docs.gitlab.com/ci/yaml/lint/\" target=\"_blank\" rel=\"noopener\">GitLab Docs: Validate CI/CD configuration</a> — проверка конфигурации до запуска pipeline.</li><li><a href=\"https://docs.npmjs.com/cli/v6/commands/npm-ci/\" target=\"_blank\" rel=\"noopener\">npm Docs: npm ci</a> — чистая установка и требования к lockfile.</li></ul>"
|
||
}
|