{ "index": 280, "slug": "editorial-2020-03-field-ci-pipeline", "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" }