DarkRiDDeR14 мин

Разбор учебного сбоя: pipeline собрал один commit, а deploy взял другой

CI/CDGitLabПолевой разбор

Симптом: после merge job verify и build отмечены зелёным, но на staging попал каталог без ожидаемого файла. Первая реакция — перезапустить deploy или добавить в него ещё одну сборку. Это усиливает проблему: новая сборка может уже использовать другой cache, другой образ runner или изменившийся набор зависимостей. Цена — команда теряет связь между тем, что проверяла, и тем, что отправила, а следующий сбой невозможно воспроизвести по логам.

Ниже учебный сценарий, а не история реального production-инцидента. В нём нет настоящих credentials, длительностей job, идентификаторов pipeline и факта выкладки. Он нужен, чтобы отработать маршрут расследования в марте 2020 года: обнаружить повторную сборку в deploy, передать один artifact от build, сверить revision и остановить job до сетевого вызова. Такая фикстура полезна именно своей границей: она показывает, какой вывод можно сделать, а какой ещё нельзя.

Что было сломано в учебном YAML

В антипримере job deploy получает новый checkout и снова вызывает npm ci с npm run build. В результате строка «build passed» выше по pipeline относится к одному каталогу, а команда доставки — к другому. Даже если оба commit совпадают, повторное выполнение не доказывает одинаковость output: у сборки есть зависимости, образ job, настройки и внешние входы. Сначала нужно убрать эту вторую точку создания результата, а не искать случайный флаг cache.

# Учебный антипример: deploy снова собирает checkout.
deploy_staging:
  stage: release
  script:
    - npm ci
    - npm run build
    - ./scripts/deploy-staging dist/

# Здесь deploy не получает результат job build.
# Лог зелёного build не доказывает, что выгружен тот же каталог.

Такой файл не обязательно вызывал бы ошибку на каждом запуске. Это делает его опаснее. В хорошие дни build и deploy могут дать похожий output, и привычка закрепится. В плохой день проявится различие: пропущенный artifact, stale cache, версия package manager, переменная сборки или ручное изменение рабочей директории. Поэтому диагностировать нужно не редкий симптом «нет файла», а нарушенный контракт: deploy не получил конкретный результат build-job.

Вертикальная схема диагностики: зелёный verify и build не разрешают deploy пересобрать checkout; deploy должен запросить artifact build, сверить REVISION и SHA256SUMS, а при сбое остановиться до команды доставки
Маршрут разбирательства идёт от наблюдаемого файла к владельцу результата. Retry не является первым действием.

Собираем факты до изменения конфигурации

Начинаю не с редактирования YAML, а с короткой карточки фактов. Какой $CI_COMMIT_SHA у pipeline? В каком job впервые появился dist/? Назван ли этот каталог в artifacts:paths? Кто получает его через dependencies? До какой строки script можно гарантировать, что сетевого действия не было? Эти вопросы не требуют доступа к production. Их можно проверить по конфигурации и логам учебного job, не выводя секреты.

Матрица учебной диагностики
НаблюдениеВероятная причинаБезопасная проверкаБлокирует deploy?Следующее действие
dist/REVISION отсутствуетbuild не записал контрактный файл или artifact не содержит путьпроверить script build и artifacts:pathsдаисправить build; не запускать delivery-script
revision отличается от $CI_COMMIT_SHAdeploy читает старый или чужой каталогсравнить файл из artifact с переменной jobдасоздать pipeline нужного commit и выяснить источник каталога
checksum не проходитпередан неполный набор файлов или manifest не покрывает путьвыполнить sha256sum -c до deployдаостановиться и сузить artifact/исправить manifest
deploy запускает npm run buildрезультат build не передаётся как dependencyпрочитать YAML, не нажимая retryдаубрать повторную сборку, добавить dependencies: [build]
verify не прошёлтест или install сообщает об ошибке входапрочитать exit code и отчёт jobдаисправить причину, build не делать обходом

Таблица говорит «вероятная», а не «доказанная» причина. Например, checksum может не пройти потому, что manifest создан до добавления последнего файла, а не потому, что artifact подменён. Поэтому каждая строка содержит маленькую проверку и точку остановки. До тех пор пока unknown состояние не объяснено, deploy не должен превращать его в сетевой эффект.

Исправление: переносим границу в artifact

Минимальное исправление не требует переписывать pipeline. Job build создаёт dist/, записывает REVISION и SHA256SUMS, затем прикладывает каталог как artifacts. Job deploy_staging объявляет dependencies: [build] и не содержит ни npm ci, ни npm run build. Так в конфигурации появляется простое правило ревью: создавать файл релиза разрешено одному job, отправлять — другому.

Проверка перед deploy может оставаться обычным POSIX-shell. Ниже команды намеренно печатают revision и список файлов, но не адрес стенда и не переменные доступа. Их нужно выполнить внутри job, которому GitLab уже передал artifacts. Это не инструкция для ручного запуска на production-сервере и не журнал настоящего pipeline.

# Команды для job deploy, не для локального production-доступа.
set -eu
printf "commit=%s\n" "$CI_COMMIT_SHA"
find dist -maxdepth 1 -type f -print | sort
test -f dist/REVISION
test "$(cat dist/REVISION)" = "$CI_COMMIT_SHA"
(cd dist && sha256sum -c SHA256SUMS)

Если find показал неожиданный файл, не нужно сразу добавлять его в artifacts. Сначала решаю, является ли он частью релизного контракта. Если нет — оставляю его в рабочей директории или исключаю из output. Если да — build обязан добавить его до создания manifest, а deploy обязан проверить наличие. Так список файлов становится предметом ревью, а не побочным продуктом cache.

Проверки идут до ручной остановки

Полезно представить deploy как последовательность, где последняя команда имеет побочный эффект. До неё должны пройти: получен artifact нужного job, существует REVISION, revision равен переменной pipeline, checksum-manifest сходится, обязательный вход вроде index.html присутствует. Только потом оператор запускает manual-job на staging. Если любой шаг даёт ненулевой exit code, pipeline остаётся на диагностике; «попробовать ещё раз» не исправляет неизвестный результат.

В GitLab CI/CD список dependencies ограничивает, чьи artifacts job скачивает. Это важнее, чем надежда на порядок стадий: stages задают маршрут выполнения, но не делают содержимое рабочих директорий очевидным. Для первой поставки я оставляю linear stages и один dependency. Ускорять граф needs, добавлять параллельные deploy или строить общую платформенную схему здесь преждевременно — сначала нужен один понятный ответ на вопрос происхождения artifact.

Маршрут расследования и исправления

  1. Остановить ручной deploy до сетевой команды и записать revision pipeline. Не перезапускать job, пока не названо, какой контракт нарушен.
  2. Открыть YAML и отметить все места, где выполняется npm ci или npm run build. Для релизного output должен остаться один владелец.
  3. В build-job создать REVISION, checksum-manifest и список artifacts:paths. Проверить, что manifest формируется после всех файлов output.
  4. В deploy-job указать dependencies: [build], убрать повторную сборку и добавить короткую проверку файлов до deploy-staging.
  5. Провести учебные отрицательные проверки: убрать artifact, подменить revision в fixture, испортить файл после manifest. Во всех трёх случаях job должна остановиться до внешней команды.
  6. После этого выполнить отдельный согласованный staging-прогон и записать, что он подтвердил. Не переносить его длительность или успех на production без отдельной проверки доступа, отката и поведения приложения.

Что остаётся за пределами разбора

Учебная фикстура не подтверждает конфигурацию конкретного runner, реальное хранение artifacts, скорость npm, работу secrets, доступ к staging или rollback. Она также не делает checksum заменой контроля доступа. Эти ограничения важны: хороший разбор не превращает один небольшой технический факт в отчёт о надёжности всего релиза.

Но после такого исправления меняется важное свойство: если deploy снова получит неизвестный каталог, он остановится до внешнего действия и покажет, где искать причину — build, artifact, dependency или manifest. Это меньше, чем полноценная система доставки, но достаточно для следующего шага: добавить реальный smoke-сценарий на staging и оформить отдельный путь отката для предыдущего известного 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 из примера