8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"index": 282,
|
||
"slug": "editorial-2020-03-practice-ci-pipeline",
|
||
"title": "Минимальный CI/CD pipeline: как не отправить непроверенную сборку",
|
||
"excerpt": "Практический разбор GitLab CI/CD: отделяем проверки от сборки, передаём deploy один артефакт и останавливаем выпуск, если его происхождение нельзя подтвердить.",
|
||
"contentHtml": "<p>Симптом появляется в момент, когда кажется, что всё уже прошло: job <code>verify</code> и <code>build</code> зелёные, а после ручного deploy на staging оказывается другой набор файлов. Иногда deploy снова вызывает <code>npm run build</code>. Иногда он читает каталог из cache. Иногда в логе нет ответа на вопрос, из какого commit собран bundle. Цена ошибки — ручное сравнение каталогов, задержка отката и риск повторно отправить тот же неизвестный результат.</p>\n<p>Для первого контура не нужно строить большой release-комплекс. Достаточно назвать входы, один раз создать результат и передать его следующему job. Ниже — учебная схема GitLab CI/CD в историческом контексте марта 2020 года: образ <code>node:12-alpine</code>, <code>npm ci</code>, три стадии и ручной staging deploy. Она не изображает реальный production-релиз и не заменяет проверку версии GitLab, Runner, shell, прав и команд конкретного проекта.</p>\n<figure><img src=\"/assets/editorial/2020/ci-pipeline-contract-2020.svg\" alt=\"Схема минимального pipeline: commit и lockfile проходят verify, build создаёт dist с REVISION и SHA256SUMS, ручной deploy получает только этот артефакт\" loading=\"lazy\" /><figcaption>Проверка и доставка разделены одним артефактом. Cache ускоряет установку, но не становится источником файлов для deploy.</figcaption></figure>\n<h2>Сначала фиксируем контракт выпуска</h2>\n<p>Входом служат revision исходников и lockfile зависимостей. Job <code>verify</code> запускает lint и тесты; любой ненулевой exit code останавливает следующий этап. Job <code>build</code> создаёт <code>dist/</code>, записывает туда <code>CI_COMMIT_SHA</code> и список контрольных сумм. Job <code>deploy_staging</code> получает только этот результат, проверяет его до сетевого вызова и не собирает проект заново.</p>\n<p>Такой контракт отвечает на два разных вопроса. <code>REVISION</code> связывает каталог с commit текущего pipeline. <code>SHA256SUMS</code> показывает, что файлы внутри полученного artifact не изменились между сборкой и проверкой. Ни один из файлов не является подписью релиза: они не защищают runner, сервер, registry или секреты. Их роль уже: сделать подмену наблюдаемой и остановить job до побочного эффекта.</p>\n<h2>Cache и artifact отвечают за разное</h2>\n<p>Cache хранит данные, которые можно получить заново. В Node-проекте это, например, каталог npm-кэша. Его отсутствие должно увеличить время установки, но не менять заявленный результат выпуска. Artifact — файл или каталог, который конкретный job сохраняет для следующих job. Если deploy читает cache вместо artifact, pipeline теряет владельца результата: неизвестно, кто создал каталог и к какому запуску он относится.</p>\n<p>Порядок стадий задаёт маршрут <code>verify → build → release</code>. При этом одного порядка недостаточно: deploy должен явно указать <code>dependencies: - build</code>, чтобы получить artifact именно этого job. В build можно записать <code>dependencies: []</code>, тем самым не рассчитывать на файлы предыдущих job. Если проекту понадобится передать отчёт из verify, это следует добавить как отдельный, названный контракт, а не использовать общий рабочий каталог.</p>\n<h2>Учебная конфигурация GitLab CI/CD</h2>\n<p>Пример рассчитан на приложение, которое публикует каталог <code>dist/</code>. Имена команд <code>lint</code>, <code>test</code> и <code>./scripts/deploy-staging</code> условны. Перед применением конфигурацию нужно проверить через CI Lint и выполнить на том образе и Runner, которые используются в проекте.</p>\n<pre><code>image: node:12-alpine\n\nstages:\n - verify\n - build\n - release\n\ncache:\n key: \"$CI_COMMIT_REF_SLUG\"\n paths:\n - .npm/\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 && find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS)\n artifacts:\n paths:\n - dist/\n expire_in: 1 week\n\ndeploy_staging:\n stage: release\n when: manual\n allow_failure: false\n dependencies:\n - build\n script:\n - set -eu\n - test \"$(cat dist/REVISION)\" = \"$CI_COMMIT_SHA\"\n - (cd dist && sha256sum -c SHA256SUMS)\n - ./scripts/deploy-staging dist/</code></pre>\n<p>Важная деталь находится в команде создания manifest: <code>SHA256SUMS</code> исключён из списка входных файлов. Если сначала открыть этот файл на запись, а затем включить его в <code>find</code>, manifest начнёт считать сам себя; проверка станет зависеть от момента чтения и может завершиться ошибкой. <code>REVISION</code>, наоборот, создаётся до расчёта сумм и входит в проверяемый набор.</p>\n<p>Build повторяет <code>npm ci</code>, хотя verify уже устанавливал зависимости. Это осознанный обмен: jobs не делят случайный <code>node_modules</code> и каждый начинает с checkout и lockfile. В реальном проекте повтор можно сократить после измерения и явной передачи проверенного набора зависимостей. Нельзя делать cache носителем <code>dist/</code> только ради экономии нескольких минут.</p>\n<h2>Проверяем artifact до сетевого действия</h2>\n<p>Проверка должна идти в deploy до команды, которая меняет staging. Сначала сравнивается revision, затем контрольные суммы, затем наличие минимального ожидаемого файла. Вынесем последовательность в отдельный фрагмент, чтобы её можно было повторить локально на учебном каталоге или внутри job без доступа к production:</p>\n<pre><code>set -eu\n\nprintf 'commit=%s\\n' \"$CI_COMMIT_SHA\"\ntest -f dist/REVISION\ntest -f dist/SHA256SUMS\ntest \"$(cat dist/REVISION)\" = \"$CI_COMMIT_SHA\"\n(cd dist && sha256sum -c SHA256SUMS)\ntest -f dist/index.html\n\n# Только после проверок:\n./scripts/deploy-staging dist/</code></pre>\n<p>Команда <code>sha256sum -c</code> проверяет целостность перечисленных файлов, но не бизнес-логику приложения. Для вложенного output, другого shell или Windows Runner понадобятся другие команды. Секреты и адрес staging должны приходить из защищённых настроек CI; их нельзя добавлять в YAML и выводить в диагностический лог.</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>Зелёный build, но другой bundle</td><td>deploy пересобирает checkout или читает cache</td><td>Найти команды сборки в deploy и источник <code>dist/</code></td><td>Передать artifact build и убрать вторую сборку</td></tr><tr><td>В deploy нет <code>dist/</code></td><td>Artifact не создан, истёк или не скачан</td><td>Проверить <code>artifacts:paths</code>, срок хранения и <code>dependencies</code></td><td>Остановить job и исправить передачу результата</td></tr><tr><td><code>REVISION</code> отличается</td><td>Смешаны pipeline, ветка или каталог</td><td>Сравнить файл с <code>CI_COMMIT_SHA</code> в том же job</td><td>Не отправлять файлы; создать pipeline нужного revision</td></tr><tr><td><code>npm ci</code> падает</td><td>Manifest и lockfile расходятся</td><td>Запустить чистую установку тем же образом</td><td>Согласовать lockfile и manifest отдельным commit</td></tr><tr><td>Checksum не проходит</td><td>Файл изменился либо manifest неполон</td><td>Проверить состав <code>dist/</code> и команду его создания</td><td>Остановиться до upload и расследовать источник изменения</td></tr></tbody></table></div>\n<p>Таблица задаёт порядок проверки, а не список советов на все случаи. Сначала определяется потерянный объект, затем проверяется его граница. Retry до этого шага скрывает нестабильность: следующий запуск может использовать другой cache или окружение и не ответит, почему первый результат отличался.</p>\n<h2>Порядок внедрения</h2>\n<ol><li>Записать commit, lockfile, образ Runner, команду build и фактический путь результата.</li><li>Проверить <code>npm ci</code> на чистой среде. Несогласованный lockfile исправить до настройки deploy.</li><li>Добавить <code>verify</code> с реальными lint и test-командами; убедиться, что ненулевой exit code не запускает build.</li><li>Собрать <code>dist/</code> один раз, добавить <code>REVISION</code> и checksum-manifest, затем объявить каталог artifact.</li><li>Настроить deploy с <code>dependencies: - build</code> и проверками до вызова delivery-скрипта.</li><li>На staging отдельно проверить провал verify, отсутствие artifact, неверный revision и изменение файла.</li></ol>\n<h2>Что эта схема не доказывает</h2>\n<p>Pipeline не доказывает, что staging принял файлы, что миграция базы безопасна или что пользовательский сценарий работает. Нужны отдельные smoke-проверки, мониторинг, правила отката и контроль доступа. Недельный <code>expire_in</code> в примере — срок хранения учебного artifact, а не политика релизов. Для rollback артефакт следует хранить в подходящем registry или хранилище с понятным именованием.</p>\n<p>Checksum не защищает от скомпрометированного Runner и не подтверждает серверную конфигурацию. <code>npm ci</code> не фиксирует версию Node.js и состояние внешнего registry. Если ручной job должен блокировать pipeline, поведение <code>when: manual</code> и <code>allow_failure: false</code> нужно проверить на установленной версии GitLab и с реальными правами запуска.</p>\n<h2>Критерий готовности</h2>\n<p>Минимальная реализация готова, когда один staging-запуск позволяет назвать commit и состав artifact, а deploy не содержит команды сборки. Три отрицательных проверки обязательны: сбой verify блокирует build; отсутствие или несовпадение artifact блокирует deploy; mismatch revision или checksum не вызывает сетевой скрипт. Это проверяемая граница учебного pipeline, а не обещание production-надёжности.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://docs.gitlab.com/ci/yaml/\" target=\"_blank\" rel=\"noopener noreferrer\">GitLab Docs: CI/CD YAML syntax reference</a> — описание <code>stages</code>, <code>artifacts</code>, <code>dependencies</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> — создание artifacts и ограничение их передачи между jobs.</li><li><a href=\"https://docs.gitlab.com/ci/caching/\" target=\"_blank\" rel=\"noopener noreferrer\">GitLab Docs: Caching in GitLab CI/CD</a> — различие cache для повторно получаемых зависимостей и artifacts для результатов 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 и lockfile.</li><li><a href=\"https://gitlab.com/gitlab-org/gitlab/-/blob/v12.9.0-ee/doc/ci/yaml/README.md\" target=\"_blank\" rel=\"noopener noreferrer\">GitLab 12.9.0 CI YAML reference</a> — исторический справочник версии марта 2020 года для проверки доступности использованных базовых ключей.</li></ul>"
|
||
}
|