DarkRiDDeR15 мин

Минимальный CI/CD pipeline: почему артефакт — контракт выпуска

CI/CDGitLabРазбор механизма

Симптом плохого pipeline не всегда красный. Часто все job завершились успешно, но у команды нет ответа на два простых вопроса: какой именно каталог проверяли и какой каталог отправили на стенд. Причина — смешаны три состояния: исходный checkout, ускоряющий cache и релизный результат. Цена проявляется при первом откате: приходится собирать заново, надеяться на прежнее окружение и гадать, повторится ли уже проверенный bundle.

В этой статье артефакт — не модное слово и не вечный объект в хранилище. Это договор внутри одного pipeline: один job создаёт названный выход после известных входов, следующие job получают этот выход и не имеют права незаметно строить другой. Разберём механизм на учебной GitLab CI/CD схеме начала 2020 года. Ни CI-запусков, ни production-доставок, ни измерений времени здесь нет; есть только проверяемые условия и границы, которые нужно подтвердить в конкретном проекте.

У pipeline четыре разных состояния

Checkout — это исходники, которые runner получил для commit. Он нужен job, но сам по себе не доказывает состав результата. Cache — сохранённые файлы для ускорения: например, скачанные npm-пакеты. Его допустимо терять, заменять или не находить; правильная сборка должна остаться корректной. Артефакт — результат job build, прикреплённый к запуску. Наконец, deploy — побочный эффект, который читает artifact и отправляет его в среду. Ошибка начинается, когда deploy обращается к checkout или cache как к заменителю artifact.

Такое разделение помогает и с ответственностью. Package manager отвечает за установку согласно lockfile. Job verify отвечает за результаты своих команд. Job build отвечает за конкретный каталог и его manifest. Job deploy отвечает только за доставку уже полученного набора файлов. Если build не создал artifact, deploy не «помогает» ему повторной сборкой. Он останавливается: иначе исчезает доказательство, что проверка и доставка говорят об одном объекте.

Вертикальная схема выпускных gate: входы commit, lockfile и образ job переходят через verify и build; build прикладывает dist с REVISION и SHA256SUMS; deploy получает только artifact, сверяет его и ждёт ручного разрешения
Каждый gate отвечает на отдельный вопрос. Пройденный предыдущий gate не отменяет проверку состава artifact перед deploy.

Входы не заканчиваются на commit

Один SHA commit не делает сборку автоматически повторяемой. Код читает lockfile, настройки bundler, образ job, переменные конфигурации и команды из package scripts. В минимальном контуре не нужно сразу стабилизировать всё: достаточно назвать входы и не подменять их фразой «на CI как-то иначе». Например, npm ci полезен именно тем, что ориентируется на существующий lockfile и завершает установку ошибкой, когда manifest и lockfile противоречат друг другу. Это ранняя остановка, а не косметическая проверка.

Состояние, доказательство и недопустимая подмена
СостояниеЧем подтверждаетсяЧем его нельзя заменитьДействие при расхождении
Checkout$CI_COMMIT_SHA в логах jobветкой в интерфейсе или локальной рабочей копиейперезапустить pipeline для нужного revision
Зависимостиуспешный npm ci по lockfileкаталогом node_modules из cacheисправить lockfile либо версию package manager
Build-выходdist/, REVISION, SHA256SUMSуспешной строкой npm run build в другом jobне запускать deploy, расследовать build-job
Проверкаexit code и тестовый отчёт job verifyфразой «локально проходило»починить тест либо подтвердить, что тест проверяет верную границу
Доставкаmanual job с ограниченными dependenciesновой сборкой в deploy-jobостановить до сетевого вызова и проверить artifact

Здесь специально нет фиктивных длительностей. Сколько занимает npm ci, зависит от runner, сети, cache и размера проекта. Полезный отчёт для команды выглядит иначе: «verify не стартовал, потому что lockfile расходится», «build не приложил dist», «deploy получил artifact от build и остановился на checksum». Это причины, на которые можно отвечать действием, а не число, случайно снятое с одной машины.

Почему stages и dependencies образуют маршрут

В простом GitLab pipeline stages дают порядок: job следующей стадии не начинают обычный путь, пока необходимая предыдущая стадия не прошла. Это удобно для первой схемы, потому что критический маршрут виден в YAML. Но порядок стадий не отвечает на вопрос, какие файлы попадут в job. Для него служит dependencies: deploy явно просит artifacts у build, а не наследует всё, что смог оставить любой ранний job.

У build-job в примере стоит dependencies: []. Это не украшение. Build не зависит от файлов verify и должен собрать исходники из checkout после своей установки по lockfile. Если ему случайно нужен отчёт или конфигурация предыдущего job, это надо назвать отдельной dependency. Явная пустота помогает заметить скрытый обмен рабочими каталогами до того, как он станет случайной особенностью runner.

Артефакт как узкий интерфейс

Минимальный artifact содержит не всё, что лежит в рабочем каталоге, а только то, что должен получить deploy: dist/, отметку revision и checksum-manifest. Чем шире artifact, тем труднее понять его происхождение и тем выше шанс перенести временный файл. Чем уже он, тем яснее интерфейс между build и deploy. Это тот же приём, что и в коде: внешний модуль получает контракт, а не доступ к чужой памяти.

Файл REVISION не является подписью и не заменяет access control. Он связывает каталог с переменной pipeline на уровне диагностики. SHA256SUMS не доказывает безопасность сервера; он проверяет, что файлы, которые deploy читает после передачи artifacts, совпадают с тем, что build записал в manifest. Если нужен вложенный каталог, другой shell или Windows runner, алгоритм и команды надо скорректировать. Нельзя заявлять «артефакт неизменяем», если manifest покрывает только часть его файлов.

# scripts/check-release-artifact.sh
set -eu

test -f dist/REVISION
test -f dist/SHA256SUMS
test "$(cat dist/REVISION)" = "$CI_COMMIT_SHA"

(cd dist && sha256sum -c SHA256SUMS)
test -f dist/index.html
echo "Артефакт относится к текущему commit и содержит ожидаемый вход"

Команда sha256sum -c должна выполняться до команды, которая открывает соединение со стендом. Это и есть точка остановки. Сбой test -f, mismatch revision или checksum даёт ненулевой exit code и не позволяет скрыть проблему под retry deploy. В лог полезно добавить revision и названия проверенных файлов, но не URL с токеном, ключи или содержимое переменных окружения.

Ручной gate и граница побочного эффекта

В небольшом проекте 2020 года ручной deploy на staging — нормальный способ не превращать каждый merge в немедленный сетевой эффект. Он не оценивает качество релиза, а создаёт место для короткой проверки: посмотреть artifact, открыть результаты verify и убедиться, что выбран нужный revision. Если job оставлен manual, но при ошибке всё равно считается необязательным, команда получает красивую схему без настоящей остановки. Поэтому поведение when: manual и allow_failure нужно проверить на установленной версии GitLab небольшим пилотом.

Production может требовать другой процесс: отдельные переменные, согласование, резервную копию, миграцию данных или rollback. Минимальный pipeline не прячет эти условия под одной командой deploy. Он честно заканчивается перед внешним изменением, если у команды пока нет проверяемого способа выполнить его. Это полезнее автоматизации, которая не может объяснить, откуда взялся её каталог.

Диагностический маршрут вместо повторного запуска

  1. При первом сбое выписать имя job, revision и стадию. Не нажимать retry до понимания, что именно не найдено: вход, команда, artifact или доступ к среде.
  2. Если не проходит install, сравнить package.json, lockfile и версию package manager. Не копировать node_modules из cache в качестве «фикса».
  3. Если не проходит verify, отделить ошибку теста от ошибки окружения и сохранить отчёт job. Build не должен стартовать как обход этого сигнала.
  4. Если build зелёный, но deploy не видит файл, проверить artifacts:paths, имя job и список dependencies. Не добавлять в deploy новый npm run build.
  5. Если revision или checksum не совпадают, остановиться до вызова delivery-script, создать новый pipeline для правильного commit и расследовать, где изменился набор файлов.
  6. Только после успешной проверки artifact запускать ручной deploy на тестовую среду; production-правила и откат оформлять отдельным следующим шагом.

Чего механизм не доказывает

Даже идеальный переход artifacts не доказывает функциональность пользовательского сценария. Lint не заменяет test, test не заменяет smoke на стенде, а checksum не заменяет авторизацию на сервере. В этом и смысл отдельных gate: у каждого есть свой вопрос, наблюдение и действие при отрицательном результате. Попытка назвать всё это одним словом «CI/CD» делает отчёт короче, но расследование дольше.

Я бы начал с одного приложения и одного staging-окружения. Когда цепочка стала наблюдаемой, можно добавлять report тестов, сохранение предыдущего артефакта или более сложный способ доставки. Но первый признак зрелости не количество секций YAML, а ответ на вопрос: если deploy остановился, можем ли мы без гадания увидеть, какой commit, какой lockfile и какой artifact дошли до этой точки?

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

  • GitLab Docs: CI/CD YAML syntax reference — справочник ключей stages, dependencies, artifacts и when; точную поддержку нужно сверять с версией GitLab на своей установке
  • GitLab Docs: Job artifacts — описывает передачу файлов между job и ограничение входящих артефактов через dependencies
  • npm CLI v6: npm ci — фиксирует установку из существующего lockfile и ошибку при расхождении manifest и lockfile
  • GitLab 12.9.0 CI YAML reference, March 2020 — официальный справочник версии, выпущенной в марте 2020 года; в нём есть базовые artifacts, dependencies и ручной when из примера