8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 281,
|
||
"slug": "editorial-2020-03-mechanism-ci-pipeline",
|
||
"title": "Артефакт как контракт CI/CD: как не отправить непроверенную сборку",
|
||
"excerpt": "Зелёные job ещё не доказывают, что deploy отправит тот же каталог, который проверял build. Разбираем границу между checkout, cache и artifact и ставим проверку до сетевого действия.",
|
||
"contentHtml": "<p>Симптом появляется в момент поставки: job <code>verify</code> и <code>build</code> завершились успешно, а на staging нет ожидаемого файла или приложение выглядит не так, как после проверки. Лог показывает зелёные статусы, но не отвечает на главный вопрос: какой каталог проверили и какой каталог отправили. Цена ошибки — повторная сборка, потерянное время и откат, который тоже приходится собирать заново. В худшем случае команда принимает непроверенный результат за тот, что прошёл CI.</p>\n<p>Причина обычно не в одном флаге GitLab. Pipeline смешивает checkout, cache, рабочую директорию и release artifact. Пока deploy может заново вызвать <code>npm run build</code>, связь между проверкой и доставкой остаётся предположением. Надёжная граница проще: один job создаёт артефакт, следующие job получают этот артефакт, проверяют его происхождение и не собирают другой каталог.</p>\n<h2>Что именно должен гарантировать pipeline</h2>\n<p>Минимальный pipeline отвечает на четыре разных вопроса. Checkout подтверждает исходный commit. Установка зависимостей подтверждает согласованный lockfile. Build создаёт конкретный набор файлов. Deploy отправляет именно этот набор и останавливается до сетевого вызова, если доказательство неполно.</p>\n<p>Эти состояния нельзя подменять друг другом. Cache ускоряет установку, но может исчезнуть или устареть. Рабочая директория существует только внутри job. Успешная команда сборки говорит, что команда завершилась с нулевым кодом, но не говорит, какие файлы были приложены к следующему job. Artifact нужен как явный интерфейс между job: он переносит результат, а не надежду на одинаковое окружение.</p>\n<figure><img src=\"/assets/editorial/2020/ci-pipeline-gates-2020.svg\" alt=\"Схема pipeline: commit, lockfile и образ job проходят verify и build, затем dist с REVISION и SHA256SUMS передаётся в deploy\" loading=\"lazy\" /><figcaption>Путь выпуска должен быть виден по границам: проверки, сборка одного каталога, проверка artifact, затем внешний эффект.</figcaption></figure>\n<h2>Почему одного commit недостаточно</h2>\n<p>Один SHA связывает pipeline с исходниками, но не описывает все входы сборки. Результат зависит от lockfile, версии Node.js, package manager, образа runner, переменных и настроек bundler. Если deploy запускает сборку повторно, любой из этих входов может отличаться. Даже одинаковый commit не доказывает одинаковый output.</p>\n<p>На раннем шаге полезно намеренно делать установку строгой. <code>npm ci</code> использует существующий lockfile и завершается ошибкой при конфликте с manifest. Это выгоднее молчаливого обновления зависимостей: pipeline останавливается там, где нарушен входной контракт. Cache <code>.npm/</code> можно подключить для скорости, но результат не должен зависеть от его наличия.</p>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>В deploy нет <code>dist/index.html</code></td><td>Путь не указан в artifact или job не получил artifact</td><td>Сверить <code>artifacts:paths</code>, имя job и <code>dependencies</code></td><td>Исправить передачу файлов; не запускать повторный build</td></tr><tr><td><code>REVISION</code> не равен <code>$CI_COMMIT_SHA</code></td><td>Взята сборка другого pipeline или checkout пересобран отдельно</td><td>Вывести значение файла и переменной до delivery-команды</td><td>Остановить job и создать pipeline для нужного commit</td></tr><tr><td>Checksum не проходит</td><td>Файл изменился после создания manifest или manifest неполон</td><td>Выполнить <code>sha256sum -c</code> внутри полученного artifact</td><td>Не выполнять сетевой шаг; исправить состав artifact</td></tr><tr><td><code>npm ci</code> завершается ошибкой</td><td>Manifest и lockfile расходятся или не совпадает версия package manager</td><td>Проверить файлы и версии на чистом runner</td><td>Исправить входы; не подменять их cache</td></tr><tr><td>Job зелёный, но результат не подтверждён</td><td>Проверяли команду, а не содержимое релизного каталога</td><td>Показать список artifact и обязательные файлы</td><td>Добавить явный контракт и проверку до deploy</td></tr></tbody></table></div>\n<p>Таблица задаёт отрицательный путь. Неизвестное состояние не превращается в сетевой эффект. Retry может повторить случайный результат, но не объяснит, какой вход изменился. Сначала проверяют факт, затем меняют конфигурацию.</p>\n<h2>Stages задают порядок, dependencies задают вход</h2>\n<p>Ключ <code>stages</code> показывает порядок работ. Например, <code>verify</code> идёт перед <code>build</code>, а <code>build</code> — перед <code>release</code>. Но стадии сами по себе не описывают файлы, которые получает job. Для этого нужен явный список artifact-зависимостей.</p>\n<p>В минимальной схеме <code>build</code> собирает checkout и не получает файлы от <code>verify</code>. Поэтому для него уместно <code>dependencies: []</code>. Deploy получает artifact только от <code>build</code>. Если deploy содержит собственный <code>npm ci</code> и <code>npm run build</code>, граница снова исчезает: job проверяет один результат, а отправляет другой.</p>\n<pre><code>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</code></pre>\n<p>Код показывает учебную структуру, а не готовый production-файл. Образ, команды, имя выходного каталога и поведение manual job нужно сверить с конкретным GitLab Runner. В строке checksum используется простой плоский каталог и GNU-команда. Для вложенных файлов, другого shell или другой операционной системы нужен отдельный вариант проверки.</p>\n<h2>REVISION и checksum — разные проверки</h2>\n<p>Файл <code>REVISION</code> связывает содержимое каталога с commit, который выполняет pipeline. Если в нём другой SHA, deploy читает не тот результат или файл создан не из текущего checkout. Это проверка происхождения на уровне диагностики. Она не является цифровой подписью и не заменяет контроль доступа.</p>\n<p><code>SHA256SUMS</code> проверяет, что файлы не изменились после создания manifest. Сначала build записывает все обязательные файлы, затем создаёт manifest, затем прикладывает каталог. Deploy проверяет manifest после передачи artifact и до вызова delivery script. Если manifest покрывает только часть каталога, нельзя называть весь artifact проверенным.</p>\n<pre><code># 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</code></pre>\n<p>У каждой команды есть цена отказа. Отсутствующий файл останавливает job. Несовпадение revision останавливает job. Ошибка checksum останавливает job. Поэтому проверка должна стоять перед первой командой, которая открывает соединение со staging или production. В лог можно вывести SHA и названия проверенных файлов. Секреты, токены и полные URL доступа в него попадать не должны.</p>\n<h2>Ручной gate не исправляет плохой artifact</h2>\n<p><code>when: manual</code> создаёт паузу перед внешним действием. Оператор может посмотреть результат verify, состав artifact и revision. Но ручное нажатие не доказывает качество каталога. Если deploy получил неизвестный набор файлов, человек лишь вручную запускает неизвестный набор файлов.</p>\n<p>Поэтому manual gate ставят после автоматических проверок. Для staging он может быть полезен как ограничитель частых изменений. Для production понадобятся отдельные правила доступа, rollback, миграции данных, наблюдение и согласование. Эта статья не утверждает, что минимальная схема покрывает их. Она закрывает более узкий вопрос: deploy не должен незаметно создавать новый результат после проверки.</p>\n<h2>Порядок действий</h2>\n<ol><li>Зафиксировать commit, команду сборки и путь итогового каталога. Не начинать с cache и ускорения.</li><li>Проверить на чистом runner, что manifest и lockfile согласованы, а <code>npm ci</code> завершается без изменения lockfile.</li><li>Добавить <code>verify</code> с существующими lint и test-командами. Ненулевой exit code должен блокировать следующую стадию.</li><li>Оставить одному job право создавать release output. После сборки записать <code>REVISION</code>, создать checksum-manifest и приложить только нужный каталог.</li><li>В deploy указать dependency на build и убрать из него установку зависимостей и повторную сборку.</li><li>До delivery-команды проверить revision, checksum и обязательные файлы. При любом сбое не нажимать retry как замену расследованию.</li><li>Проверить отрицательный путь: отсутствие artifact, подмена revision и изменение файла после manifest должны завершать job до сетевого вызова.</li><li>Только затем провести согласованный staging-прогон. Его результат не выдавать за production-проверку.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Этот механизм не доказывает, что пользовательский сценарий работает. Lint не заменяет тесты. Тесты не заменяют smoke на стенде. Checksum не заменяет авторизацию, резервное копирование и rollback. Артефакт также может истечь по сроку хранения, а синтаксис GitLab зависит от версии сервера и Runner. Эти условия нужно проверять отдельно.</p>\n<p>Минимальный pipeline готов, когда один запуск показывает один commit, один job-владелец release output и один переданный artifact. Deploy не содержит повторной сборки. До сетевой команды автоматически проверяются <code>REVISION</code>, manifest и обязательный файл. Три отрицательные проверки — отсутствующий artifact, неверный revision и испорченный файл — останавливают job. Если эти условия нельзя показать по YAML и логам, зелёный статус ещё не означает готовый выпуск.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.gitlab.com/ee/ci/yaml/\" target=\"_blank\" rel=\"noopener noreferrer\">GitLab Docs: CI/CD YAML syntax reference</a> — описание <code>stages</code>, <code>dependencies</code>, <code>artifacts</code> и <code>when</code>.</li><li><a href=\"https://docs.gitlab.com/ci/jobs/job_artifacts/\" target=\"_blank\" rel=\"noopener noreferrer\">GitLab Docs: Job artifacts</a> — правила хранения и передачи файлов между job.</li><li><a href=\"https://docs.npmjs.com/cli/v6/commands/npm-ci\" target=\"_blank\" rel=\"noopener noreferrer\">npm CLI v6: npm ci</a> — поведение установки по lockfile и проверка согласованности manifest.</li></ul>"
|
||
}
|