{ "index": 281, "slug": "editorial-2020-03-mechanism-ci-pipeline", "title": "Артефакт как контракт CI/CD: как не отправить непроверенную сборку", "excerpt": "Зелёные job ещё не доказывают, что deploy отправит тот же каталог, который проверял build. Разбираем границу между checkout, cache и artifact и ставим проверку до сетевого действия.", "contentHtml": "

Симптом появляется в момент поставки: job verify и build завершились успешно, а на staging нет ожидаемого файла или приложение выглядит не так, как после проверки. Лог показывает зелёные статусы, но не отвечает на главный вопрос: какой каталог проверили и какой каталог отправили. Цена ошибки — повторная сборка, потерянное время и откат, который тоже приходится собирать заново. В худшем случае команда принимает непроверенный результат за тот, что прошёл CI.

\n

Причина обычно не в одном флаге GitLab. Pipeline смешивает checkout, cache, рабочую директорию и release artifact. Пока job deploy может заново вызвать npm run build, связь между проверкой и доставкой остаётся предположением. Надёжная граница проще: один job создаёт артефакт, следующие job получают этот артефакт, проверяют его происхождение и не собирают другой каталог.

\n

Что именно должен гарантировать pipeline

\n

Минимальный pipeline отвечает на четыре разных вопроса. Checkout подтверждает исходный commit. npm ci проверяет согласованность manifest и lockfile и устанавливает зависимости. Build создаёт конкретный набор файлов. Deploy отправляет именно этот набор и останавливается до сетевого вызова, если доказательство неполно.

\n

Эти состояния нельзя подменять друг другом. Cache ускоряет установку, но может исчезнуть или устареть. Рабочая директория существует только внутри job. Успешная команда сборки говорит, что команда завершилась с нулевым кодом, но не говорит, какие файлы были приложены к следующему job. Artifact нужен как явный интерфейс между job: он переносит результат, а не надежду на одинаковое окружение.

\n
\"Схема
Путь выпуска должен быть виден по границам: проверки, сборка одного каталога, проверка artifact, затем внешний эффект.
\n

Почему одного commit недостаточно

\n

Один SHA связывает pipeline с исходниками, но не описывает все входы сборки. Результат зависит от lockfile, версии Node.js, package manager, образа runner, переменных и настроек bundler. Если deploy запускает сборку повторно, любой из этих входов может отличаться. Даже одинаковый commit не доказывает одинаковый output.

\n

На раннем шаге полезно намеренно делать установку строгой. npm ci использует существующий lockfile и завершается ошибкой при конфликте с manifest; он не обновляет lockfile. Это выгоднее молчаливого обновления зависимостей: pipeline останавливается там, где нарушен входной контракт. Cache .npm/ можно подключить для скорости, но он не становится release artifact и не должен влиять на его содержимое.

\n
Симптом → причина → проверка → действие
СимптомВероятная причинаПроверкаДействие
В deploy нет dist/index.htmlПуть не указан в artifact или job не получил artifactСверить artifacts:paths, имя job и dependenciesИсправить передачу файлов; не запускать повторный build
REVISION не равен $CI_COMMIT_SHAВзята сборка другого pipeline или checkout пересобран отдельноВывести значение файла и переменной до delivery-командыОстановить job и создать pipeline для нужного commit
Checksum не проходитФайл изменился после создания manifest или manifest неполонВыполнить sha256sum -c внутри полученного artifactНе выполнять сетевой шаг; исправить состав artifact
npm ci завершается ошибкойManifest и lockfile расходятся или не совпадает версия package managerПроверить файлы и версии на чистом runnerИсправить входы; не подменять их cache
Job зелёный, но результат не подтверждёнПроверяли команду, а не содержимое релизного каталогаПоказать список artifact и обязательные файлыДобавить явный контракт и проверку до deploy
\n

Таблица задаёт отрицательный путь. Неизвестное состояние не превращается в сетевой эффект. Retry может повторить случайный результат, но не объяснит, какой вход изменился. Сначала проверяют факт, затем меняют конфигурацию.

\n

Stages задают порядок, dependencies задают вход

\n

Ключ stages показывает порядок работ. Например, verify идёт перед build, а build — перед release. Но стадии сами по себе не описывают файлы, которые получает job. Для этого нужен явный список artifact-зависимостей.

\n

В минимальной схеме build собирает checkout и не получает файлы от verify. Поэтому для него уместно dependencies: []. Deploy получает artifact только от build. Если deploy содержит собственный npm ci и npm run build, граница снова исчезает: job проверяет один результат, а отправляет другой.

\n

В примере используется только dependencies. Если проект добавляет needs, способ получения artifact нужно описать через needs:artifacts и не смешивать два механизма в одном job. Иначе визуальный порядок стадий не гарантирует тот набор файлов, который получил deploy.

\n
image: node:12-alpine\n\nstages:\n  - verify\n  - build\n  - release\n\nverify:\n  stage: verify\n  script:\n    - npm ci --cache .npm --prefer-offline\n    - npm run lint\n    - npm test\n\nbuild:\n  stage: build\n  dependencies: []\n  script:\n    - npm ci --cache .npm --prefer-offline\n    - npm run build\n    - printf \"%s\\n\" \"$CI_COMMIT_SHA\" > dist/REVISION\n    - (cd dist && sha256sum * > SHA256SUMS)\n  artifacts:\n    name: \"web-$CI_COMMIT_SHA\"\n    paths:\n      - dist/\n    expire_in: 7 days\n\ndeploy_staging:\n  stage: release\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    - ./scripts/deploy-staging dist/\n  when: manual\n  allow_failure: false
\n

Код показывает учебную структуру, а не готовый production-файл. Образ, команды, имя выходного каталога и поведение manual job нужно сверить с конкретным GitLab Runner. В строке checksum используется простой плоский каталог и GNU-команда. Для вложенных файлов, другого shell или другой операционной системы нужен отдельный вариант проверки.

\n

REVISION и checksum — разные проверки

\n

Файл REVISION содержит ожидаемый SHA commit-а pipeline. Сравнение с $CI_COMMIT_SHA показывает, что artifact помечен тем же commit, но само по себе не доказывает происхождение всех файлов: значение можно записать вручную. Это проверка происхождения на уровне диагностики, а не цифровая подпись и не замена контролю доступа.

\n

SHA256SUMS проверяет, что перечисленные файлы не изменились после создания manifest. Сначала build записывает обязательные файлы, затем создаёт manifest, затем прикладывает каталог. Deploy проверяет manifest после передачи artifact и до вызова delivery script. Если manifest покрывает только часть каталога, нельзя называть весь artifact проверенным.

\n
# scripts/check-release-artifact.sh\nset -eu\n\ntest -f dist/REVISION\ntest -f dist/SHA256SUMS\ntest \"$(cat dist/REVISION)\" = \"$CI_COMMIT_SHA\"\n\n(cd dist && sha256sum -c SHA256SUMS)\ntest -f dist/index.html
\n

У каждой команды есть цена отказа. Отсутствующий файл останавливает job. Несовпадение revision останавливает job. Ошибка checksum останавливает job. Поэтому проверка должна стоять перед первой командой, которая открывает соединение со staging или production. В лог можно вывести SHA и названия проверенных файлов. Секреты, токены и полные URL доступа в него попадать не должны.

\n

Ручной gate не исправляет плохой artifact

\n

when: manual создаёт паузу перед внешним действием. Оператор может посмотреть результат verify, состав artifact и revision. Но ручное нажатие не доказывает качество каталога. Если deploy получил неизвестный набор файлов, человек лишь вручную запускает неизвестный набор файлов.

\n

Поэтому manual gate ставят после автоматических проверок. Для staging он может быть полезен как ограничитель частых изменений. Для production понадобятся отдельные правила доступа, rollback, миграции данных, наблюдение и согласование. Эта статья не утверждает, что минимальная схема покрывает их. Она закрывает более узкий вопрос: deploy не должен незаметно создавать новый результат после проверки.

\n

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

\n
  1. Зафиксировать commit, команду сборки и путь итогового каталога. Не начинать с cache и ускорения.
  2. Проверить на чистом runner, что manifest и lockfile согласованы, а npm ci завершается без изменения lockfile.
  3. Добавить verify с существующими lint и test-командами. Ненулевой exit code должен блокировать следующую стадию.
  4. Оставить одному job право создавать release output. После сборки записать REVISION, создать checksum-manifest и приложить только нужный каталог.
  5. В deploy указать dependency на build и убрать из него установку зависимостей и повторную сборку.
  6. До delivery-команды проверить revision, checksum и обязательные файлы. При любом сбое не нажимать retry как замену расследованию.
  7. Проверить отрицательный путь: отсутствие artifact, подмена revision и изменение файла после manifest должны завершать job до сетевого вызова.
  8. Только затем провести согласованный staging-прогон. Его результат не выдавать за production-проверку.
\n

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

\n

Этот механизм не доказывает, что пользовательский сценарий работает. Lint не заменяет тесты. Тесты не заменяют smoke на стенде. Checksum не заменяет авторизацию, резервное копирование и rollback. Артефакт также может истечь по сроку хранения, а синтаксис GitLab зависит от версии сервера и Runner. Эти условия нужно проверять отдельно.

\n

Минимальный pipeline готов, когда один запуск показывает один commit, один job-владелец release output и один переданный artifact. Deploy не содержит повторной сборки. До сетевой команды автоматически проверяются REVISION, manifest и обязательный файл. Три отрицательные проверки — отсутствующий artifact, неверный revision и испорченный файл — останавливают job. Если эти условия нельзя показать по YAML и логам, зелёный статус ещё не означает готовый выпуск.

\n

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

\n" }