import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; function paragraph(text) { return '
' + text + '
'; } function heading(text) { return '' + String(code).trim() + '';
}
function figure(src, alt, caption) {
return 'npm run build; иногда берёт каталог из cache; иногда просто не может показать, какой commit лежит внутри. Проблема не в количестве job. У выпуска нет одного названного результата, поэтому при сбое нельзя доказать, что проверяли и что отправили. Цена — ручной разбор после merge, риск выложить не тот bundle и невозможность быстро повторить путь отката.'),
paragraph('В марте 2020 года для первой схемы мне достаточно GitLab CI/CD и трёх стадий: проверить вход, собрать результат, вручную разрешить доставку на staging. Это учебная конфигурация, а не готовый файл для чужого production. В ней нет credentials, фактических длительностей и реальной выгрузки. Цель уже полезнее: один commit, один lockfile, один build-артефакт и явный момент, когда pipeline обязан остановиться.'),
heading('Сначала называем контракт выпуска'),
paragraph('До YAML я записываю четыре вещи. Вход — revision исходного кода и lockfile зависимостей. Проверка — команды, которые могут остановить переход к сборке. Выход — каталог dist/ с отметкой revision и контрольной суммой. Получатель — ручной job deploy, который не пересобирает исходники. Если любой пункт не назван, зелёный значок job не означает, что выпуск повторяем.'),
paragraph('Важно не смешивать cache и артефакт. Cache может ускорить повторную установку пакетов, но не должен быть носителем релизного результата: его содержимое зависит от runner и правила ключа. Артефакт создаёт конкретный job build после успешной команды. Его срок хранения ограничен, поэтому «неизменяемый» здесь означает не вечное хранилище, а дисциплину конкретного pipeline: downstream-job только читает этот результат и не вызывает новую сборку.'),
figure('/assets/editorial/2020/ci-pipeline-contract-2020.svg', 'Вертикальная схема минимального pipeline: commit и lockfile входят в verify, затем build создаёт каталог dist с REVISION и SHA256SUMS; ручной staging deploy получает только этот артефакт и может быть остановлен до побочного эффекта', 'Проверка исходников и доставка разделены. Между ними лежит один артефакт, а не cache и не новый checkout с повторной сборкой.'),
heading('Матрица входов: что именно обязано совпасть'),
dataTable(
'Контракт минимального выпуска',
['Часть', 'Владелец', 'Наблюдаемое доказательство', 'Что блокирует'],
[
['Исходный revision', 'VCS и runner', '$CI_COMMIT_SHA записан в dist/REVISION', 'файл отсутствует или revision не совпадает с job deploy'],
['Зависимости', 'package-lock.json', 'npm ci завершился на lockfile', 'manifest и lockfile расходятся, install не проходит'],
['Проверки', 'job verify', 'lint и test имеют нулевой exit code', 'сбой не пускает job build'],
['Результат сборки', 'job build', 'архив dist/ и SHA256SUMS', 'нет каталога либо checksum не проходит'],
['Побочный эффект', 'ручной deploy', 'оператор запускает job только после просмотра артефакта', 'manual gate не нажат или проверка артефакта не прошла'],
],
),
paragraph('Эта таблица не заменяет правила доступа к серверу. Она отделяет технический вопрос от организационного: CI доказывает происхождение файлов, а право запускать deploy остаётся у процесса команды. Если staging вообще не нужен, последний job можно не создавать. Хуже оставить автоматическую команду, которая выглядит как deploy, но не показывает, какой каталог она отправила.'),
heading('Консервативная GitLab CI-конфигурация'),
paragraph('Ниже использован набор ключей, привычный для GitLab CI/CD начала 2020 года: stages, artifacts, dependencies, only и when: manual. Стадии дают линейный маршрут, а dependencies у deploy ограничивает входящие файлы артефактом build. Перед применением нужно сверить синтаксис с версией GitLab и GitLab Runner на своей установке: это часть входного контракта, а не мелочь шаблона.'),
codeBlock(minimalPipeline),
paragraph('Job verify запускает install из lockfile и две проверки. Никакой вывод о длительности здесь не сделан: cache .npm/ может помочь, а может не попасть на тот же runner. Job build снова устанавливает зависимости, потому что job изолированы. Это дороже короткой команды, но понятнее: build не наследует случайный рабочий каталог предыдущего job. Сначала полезно добиться этого свойства, потом измерять, нужна ли оптимизация.'),
paragraph('В build добавлены REVISION и SHA256SUMS. Первый файл отвечает на вопрос «из какого commit получен каталог». Второй помогает заметить потерю или замену файла после сборки. Команда sha256sum в примере рассчитана на простой плоский dist/; если build создаёт вложенные каталоги или runner работает не в GNU/Linux, команду надо адаптировать и проверить отдельно. Нельзя копировать её и считать, что весь архив уже проверен.'),
heading('Почему deploy не собирает заново'),
paragraph('Повторная сборка в deploy-job ломает границу. Даже при одинаковом commit она может прочитать другой lockfile из ветки, другой образ job, другое окружение или cache. Когда это происходит, тестировал один job, а отправлял другой. Правильный минимум проще: deploy получает dist/ через dependencies: [build], проверяет отметку revision и только после этого вызывает команду доставки.'),
paragraph('Ручной when: manual здесь не является «защитой от всего». Это всего лишь точка остановки до побочного эффекта. Явное allow_failure: false делает намерение видимым в файле, но поведение manual-job и права запуска всё равно нужно проверить в пилотном проекте на своей версии GitLab. Если команда хочет автоматический staging, она должна заменить ручной gate наблюдаемым условием и отдельно описать, кто отвечает за откат.'),
heading('Проверяем артефакт до побочного эффекта'),
paragraph('Перед вызовом deploy-staging job не должен доверять имени архива. Он открывает ожидаемые файлы, сравнивает revision с переменной pipeline и сверяет суммы. Это короткая проверка происхождения, а не криптографическая защита всей цепочки. Она не подтверждает, что HTML полезен пользователю, секреты настроены верно или сервер примет upload. Но она останавливает именно тот класс ошибок, ради которого появился артефакт: deploy не продолжает путь с неизвестным каталогом.'),
codeBlock(artifactVerification),
paragraph('В отдельном проекте вместо sha256sum может использоваться manifest bundler или архив с уже заданной суммой. Важен не инструмент, а порядок: build создаёт доказательство, deploy проверяет то же доказательство. Если verification не проходит, job завершается до сетевого вызова. Лог должен показывать имя job, revision и причину остановки, но не содержимое секретных переменных.'),
heading('Маршрут первой поставки'),
orderedList([
'Зафиксировать текущую команду build, путь результата и revision, из которого команда обычно выпускает приложение. Не начинать с cache и ускорения.',
'Положить lockfile в контролируемый вход и проверить, что npm ci завершается на чистом runner. Ошибку расхождения manifest и lockfile исправить до настройки deploy.',
'Добавить job verify с существующими lint/test-командами. Сбой должен завершать job ненулевым кодом; успешный лог не выдавать за тест пользовательского сценария.',
'Собрать dist/ один раз, положить рядом REVISION и checksum, прикрепить каталог как artifacts с понятным сроком хранения.',
'В deploy-job разрешить зависимости только от build, проверить artifact и поставить ручной gate. Переменные доступа к стенду хранить в настройках CI, а не в YAML и не в статье.',
'Провести один учебный запуск на staging: проверить, что при провале test, отсутствии artifact и несовпадении revision deploy не делает сетевого шага. Не называть этот прогон production-деплоем.',
]),
heading('Границы этого минимального решения'),
paragraph('Схема не покрывает стратегию production-раскатки, rollback, миграции базы и мониторинг после доставки. Она также не утверждает, что конкретный GitLab instance хранит artifact бесконечно или что один checksum защищает от всех классов компрометации. Для первой задачи это нормально: сначала появляется цепочка «commit → проверки → один каталог → ручная остановка». Следующей задачей можно сделать реальный smoke на staging и документированный способ вернуть предыдущий известный артефакт.'),
paragraph('Если после внедрения появляется соблазн добавить retry, второй cache или ещё один build, сначала нужно назвать симптом. Если job просто медленный — измерить время на одинаковом runner. Если artifact не находится — проверить dependencies и срок хранения. Если нужен другой релиз — сделать новый pipeline. Так маленькая конфигурация остаётся местом, где причину видно до действия, а не коллекцией флагов, накопленных после ночных сбоев.'),
],
[gitlabYaml, gitlabArtifacts, npmCi, gitlabLegacyYaml],
);
const mechanismArticle = createRevision(
{
slug: 'editorial-2020-03-mechanism-ci-pipeline',
title: 'Минимальный CI/CD pipeline: почему артефакт — контракт выпуска',
categories: ['CI/CD', 'GitLab', 'Разбор механизма'],
cover: '/assets/editorial/2020/ci-pipeline-gates-2020.svg',
excerpt: 'Разбираем, почему зелёный build не равен готовому релизу: где заканчивается проверка, кто владеет артефактом и на каком условии deploy обязан остановиться.',
readingMinutes: 15,
},
[
paragraph('Симптом плохого pipeline не всегда красный. Часто все job завершились успешно, но у команды нет ответа на два простых вопроса: какой именно каталог проверяли и какой каталог отправили на стенд. Причина — смешаны три состояния: исходный checkout, ускоряющий cache и релизный результат. Цена проявляется при первом откате: приходится собирать заново, надеяться на прежнее окружение и гадать, повторится ли уже проверенный bundle.'),
paragraph('В этой статье артефакт — не модное слово и не вечный объект в хранилище. Это договор внутри одного pipeline: один job создаёт названный выход после известных входов, следующие job получают этот выход и не имеют права незаметно строить другой. Разберём механизм на учебной GitLab CI/CD схеме начала 2020 года. Ни CI-запусков, ни production-доставок, ни измерений времени здесь нет; есть только проверяемые условия и границы, которые нужно подтвердить в конкретном проекте.'),
heading('У pipeline четыре разных состояния'),
paragraph('Checkout — это исходники, которые runner получил для commit. Он нужен job, но сам по себе не доказывает состав результата. Cache — сохранённые файлы для ускорения: например, скачанные npm-пакеты. Его допустимо терять, заменять или не находить; правильная сборка должна остаться корректной. Артефакт — результат job build, прикреплённый к запуску. Наконец, deploy — побочный эффект, который читает artifact и отправляет его в среду. Ошибка начинается, когда deploy обращается к checkout или cache как к заменителю artifact.'),
paragraph('Такое разделение помогает и с ответственностью. Package manager отвечает за установку согласно lockfile. Job verify отвечает за результаты своих команд. Job build отвечает за конкретный каталог и его manifest. Job deploy отвечает только за доставку уже полученного набора файлов. Если build не создал artifact, deploy не «помогает» ему повторной сборкой. Он останавливается: иначе исчезает доказательство, что проверка и доставка говорят об одном объекте.'),
figure('/assets/editorial/2020/ci-pipeline-gates-2020.svg', 'Вертикальная схема выпускных gate: входы commit, lockfile и образ job переходят через verify и build; build прикладывает dist с REVISION и SHA256SUMS; deploy получает только artifact, сверяет его и ждёт ручного разрешения', 'Каждый gate отвечает на отдельный вопрос. Пройденный предыдущий gate не отменяет проверку состава artifact перед deploy.'),
heading('Входы не заканчиваются на commit'),
paragraph('Один SHA commit не делает сборку автоматически повторяемой. Код читает lockfile, настройки bundler, образ job, переменные конфигурации и команды из package scripts. В минимальном контуре не нужно сразу стабилизировать всё: достаточно назвать входы и не подменять их фразой «на CI как-то иначе». Например, npm ci полезен именно тем, что ориентируется на существующий lockfile и завершает установку ошибкой, когда manifest и lockfile противоречат друг другу. Это ранняя остановка, а не косметическая проверка.'),
dataTable(
'Состояние, доказательство и недопустимая подмена',
['Состояние', 'Чем подтверждается', 'Чем его нельзя заменить', 'Действие при расхождении'],
[
['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'],
],
),
paragraph('Здесь специально нет фиктивных длительностей. Сколько занимает npm ci, зависит от runner, сети, cache и размера проекта. Полезный отчёт для команды выглядит иначе: «verify не стартовал, потому что lockfile расходится», «build не приложил dist», «deploy получил artifact от build и остановился на checksum». Это причины, на которые можно отвечать действием, а не число, случайно снятое с одной машины.'),
heading('Почему stages и dependencies образуют маршрут'),
paragraph('В простом GitLab pipeline stages дают порядок: job следующей стадии не начинают обычный путь, пока необходимая предыдущая стадия не прошла. Это удобно для первой схемы, потому что критический маршрут виден в YAML. Но порядок стадий не отвечает на вопрос, какие файлы попадут в job. Для него служит dependencies: deploy явно просит artifacts у build, а не наследует всё, что смог оставить любой ранний job.'),
paragraph('У build-job в примере стоит dependencies: []. Это не украшение. Build не зависит от файлов verify и должен собрать исходники из checkout после своей установки по lockfile. Если ему случайно нужен отчёт или конфигурация предыдущего job, это надо назвать отдельной dependency. Явная пустота помогает заметить скрытый обмен рабочими каталогами до того, как он станет случайной особенностью runner.'),
heading('Артефакт как узкий интерфейс'),
paragraph('Минимальный artifact содержит не всё, что лежит в рабочем каталоге, а только то, что должен получить deploy: dist/, отметку revision и checksum-manifest. Чем шире artifact, тем труднее понять его происхождение и тем выше шанс перенести временный файл. Чем уже он, тем яснее интерфейс между build и deploy. Это тот же приём, что и в коде: внешний модуль получает контракт, а не доступ к чужой памяти.'),
paragraph('Файл REVISION не является подписью и не заменяет access control. Он связывает каталог с переменной pipeline на уровне диагностики. SHA256SUMS не доказывает безопасность сервера; он проверяет, что файлы, которые deploy читает после передачи artifacts, совпадают с тем, что build записал в manifest. Если нужен вложенный каталог, другой shell или Windows runner, алгоритм и команды надо скорректировать. Нельзя заявлять «артефакт неизменяем», если manifest покрывает только часть его файлов.'),
codeBlock(artifactVerification),
paragraph('Команда sha256sum -c должна выполняться до команды, которая открывает соединение со стендом. Это и есть точка остановки. Сбой test -f, mismatch revision или checksum даёт ненулевой exit code и не позволяет скрыть проблему под retry deploy. В лог полезно добавить revision и названия проверенных файлов, но не URL с токеном, ключи или содержимое переменных окружения.'),
heading('Ручной gate и граница побочного эффекта'),
paragraph('В небольшом проекте 2020 года ручной deploy на staging — нормальный способ не превращать каждый merge в немедленный сетевой эффект. Он не оценивает качество релиза, а создаёт место для короткой проверки: посмотреть artifact, открыть результаты verify и убедиться, что выбран нужный revision. Если job оставлен manual, но при ошибке всё равно считается необязательным, команда получает красивую схему без настоящей остановки. Поэтому поведение when: manual и allow_failure нужно проверить на установленной версии GitLab небольшим пилотом.'),
paragraph('Production может требовать другой процесс: отдельные переменные, согласование, резервную копию, миграцию данных или rollback. Минимальный pipeline не прячет эти условия под одной командой deploy. Он честно заканчивается перед внешним изменением, если у команды пока нет проверяемого способа выполнить его. Это полезнее автоматизации, которая не может объяснить, откуда взялся её каталог.'),
heading('Диагностический маршрут вместо повторного запуска'),
orderedList([
'При первом сбое выписать имя job, revision и стадию. Не нажимать retry до понимания, что именно не найдено: вход, команда, artifact или доступ к среде.',
'Если не проходит install, сравнить package.json, lockfile и версию package manager. Не копировать node_modules из cache в качестве «фикса».',
'Если не проходит verify, отделить ошибку теста от ошибки окружения и сохранить отчёт job. Build не должен стартовать как обход этого сигнала.',
'Если build зелёный, но deploy не видит файл, проверить artifacts:paths, имя job и список dependencies. Не добавлять в deploy новый npm run build.',
'Если revision или checksum не совпадают, остановиться до вызова delivery-script, создать новый pipeline для правильного commit и расследовать, где изменился набор файлов.',
'Только после успешной проверки artifact запускать ручной deploy на тестовую среду; production-правила и откат оформлять отдельным следующим шагом.',
]),
heading('Чего механизм не доказывает'),
paragraph('Даже идеальный переход artifacts не доказывает функциональность пользовательского сценария. Lint не заменяет test, test не заменяет smoke на стенде, а checksum не заменяет авторизацию на сервере. В этом и смысл отдельных gate: у каждого есть свой вопрос, наблюдение и действие при отрицательном результате. Попытка назвать всё это одним словом «CI/CD» делает отчёт короче, но расследование дольше.'),
paragraph('Я бы начал с одного приложения и одного staging-окружения. Когда цепочка стала наблюдаемой, можно добавлять report тестов, сохранение предыдущего артефакта или более сложный способ доставки. Но первый признак зрелости не количество секций YAML, а ответ на вопрос: если deploy остановился, можем ли мы без гадания увидеть, какой commit, какой lockfile и какой artifact дошли до этой точки?'),
],
[gitlabYaml, gitlabArtifacts, npmCi, gitlabLegacyYaml],
);
const fieldArticle = createRevision(
{
slug: 'editorial-2020-03-field-ci-pipeline',
title: 'Разбор учебного сбоя: pipeline собрал один commit, а deploy взял другой',
categories: ['CI/CD', 'GitLab', 'Полевой разбор'],
cover: '/assets/editorial/2020/ci-pipeline-diagnosis-2020.svg',
excerpt: 'Учебный разбор сбоя: тесты зелёные, но deploy пересобирает checkout. Ищем границу, проверяем artifact и останавливаем выпуск до внешнего действия.',
readingMinutes: 14,
},
[
paragraph('Симптом: после merge job verify и build отмечены зелёным, но на staging попал каталог без ожидаемого файла. Первая реакция — перезапустить deploy или добавить в него ещё одну сборку. Это усиливает проблему: новая сборка может уже использовать другой cache, другой образ runner или изменившийся набор зависимостей. Цена — команда теряет связь между тем, что проверяла, и тем, что отправила, а следующий сбой невозможно воспроизвести по логам.'),
paragraph('Ниже учебный сценарий, а не история реального production-инцидента. В нём нет настоящих credentials, длительностей job, идентификаторов pipeline и факта выкладки. Он нужен, чтобы отработать маршрут расследования в марте 2020 года: обнаружить повторную сборку в deploy, передать один artifact от build, сверить revision и остановить job до сетевого вызова. Такая фикстура полезна именно своей границей: она показывает, какой вывод можно сделать, а какой ещё нельзя.'),
heading('Что было сломано в учебном YAML'),
paragraph('В антипримере job deploy получает новый checkout и снова вызывает npm ci с npm run build. В результате строка «build passed» выше по pipeline относится к одному каталогу, а команда доставки — к другому. Даже если оба commit совпадают, повторное выполнение не доказывает одинаковость output: у сборки есть зависимости, образ job, настройки и внешние входы. Сначала нужно убрать эту вторую точку создания результата, а не искать случайный флаг cache.'),
codeBlock(failingPipeline),
paragraph('Такой файл не обязательно вызывал бы ошибку на каждом запуске. Это делает его опаснее. В хорошие дни build и deploy могут дать похожий output, и привычка закрепится. В плохой день проявится различие: пропущенный artifact, stale cache, версия package manager, переменная сборки или ручное изменение рабочей директории. Поэтому диагностировать нужно не редкий симптом «нет файла», а нарушенный контракт: deploy не получил конкретный результат build-job.'),
figure('/assets/editorial/2020/ci-pipeline-diagnosis-2020.svg', 'Вертикальная схема диагностики: зелёный verify и build не разрешают deploy пересобрать checkout; deploy должен запросить artifact build, сверить REVISION и SHA256SUMS, а при сбое остановиться до команды доставки', 'Маршрут разбирательства идёт от наблюдаемого файла к владельцу результата. Retry не является первым действием.'),
heading('Собираем факты до изменения конфигурации'),
paragraph('Начинаю не с редактирования YAML, а с короткой карточки фактов. Какой $CI_COMMIT_SHA у pipeline? В каком job впервые появился dist/? Назван ли этот каталог в artifacts:paths? Кто получает его через dependencies? До какой строки script можно гарантировать, что сетевого действия не было? Эти вопросы не требуют доступа к production. Их можно проверить по конфигурации и логам учебного job, не выводя секреты.'),
dataTable(
'Матрица учебной диагностики',
['Наблюдение', 'Вероятная причина', 'Безопасная проверка', 'Блокирует deploy?', 'Следующее действие'],
[
['dist/REVISION отсутствует', 'build не записал контрактный файл или artifact не содержит путь', 'проверить script build и artifacts:paths', 'да', 'исправить build; не запускать delivery-script'],
['revision отличается от $CI_COMMIT_SHA', 'deploy читает старый или чужой каталог', 'сравнить файл из 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 не делать обходом'],
],
),
paragraph('Таблица говорит «вероятная», а не «доказанная» причина. Например, checksum может не пройти потому, что manifest создан до добавления последнего файла, а не потому, что artifact подменён. Поэтому каждая строка содержит маленькую проверку и точку остановки. До тех пор пока unknown состояние не объяснено, deploy не должен превращать его в сетевой эффект.'),
heading('Исправление: переносим границу в artifact'),
paragraph('Минимальное исправление не требует переписывать pipeline. Job build создаёт dist/, записывает REVISION и SHA256SUMS, затем прикладывает каталог как artifacts. Job deploy_staging объявляет dependencies: [build] и не содержит ни npm ci, ни npm run build. Так в конфигурации появляется простое правило ревью: создавать файл релиза разрешено одному job, отправлять — другому.'),
paragraph('Проверка перед deploy может оставаться обычным POSIX-shell. Ниже команды намеренно печатают revision и список файлов, но не адрес стенда и не переменные доступа. Их нужно выполнить внутри job, которому GitLab уже передал artifacts. Это не инструкция для ручного запуска на production-сервере и не журнал настоящего pipeline.'),
codeBlock(diagnosisCommands),
paragraph('Если find показал неожиданный файл, не нужно сразу добавлять его в artifacts. Сначала решаю, является ли он частью релизного контракта. Если нет — оставляю его в рабочей директории или исключаю из output. Если да — build обязан добавить его до создания manifest, а deploy обязан проверить наличие. Так список файлов становится предметом ревью, а не побочным продуктом cache.'),
heading('Проверки идут до ручной остановки'),
paragraph('Полезно представить deploy как последовательность, где последняя команда имеет побочный эффект. До неё должны пройти: получен artifact нужного job, существует REVISION, revision равен переменной pipeline, checksum-manifest сходится, обязательный вход вроде index.html присутствует. Только потом оператор запускает manual-job на staging. Если любой шаг даёт ненулевой exit code, pipeline остаётся на диагностике; «попробовать ещё раз» не исправляет неизвестный результат.'),
paragraph('В GitLab CI/CD список dependencies ограничивает, чьи artifacts job скачивает. Это важнее, чем надежда на порядок стадий: stages задают маршрут выполнения, но не делают содержимое рабочих директорий очевидным. Для первой поставки я оставляю linear stages и один dependency. Ускорять граф needs, добавлять параллельные deploy или строить общую платформенную схему здесь преждевременно — сначала нужен один понятный ответ на вопрос происхождения artifact.'),
heading('Маршрут расследования и исправления'),
orderedList([
'Остановить ручной deploy до сетевой команды и записать revision pipeline. Не перезапускать job, пока не названо, какой контракт нарушен.',
'Открыть YAML и отметить все места, где выполняется npm ci или npm run build. Для релизного output должен остаться один владелец.',
'В build-job создать REVISION, checksum-manifest и список artifacts:paths. Проверить, что manifest формируется после всех файлов output.',
'В deploy-job указать dependencies: [build], убрать повторную сборку и добавить короткую проверку файлов до deploy-staging.',
'Провести учебные отрицательные проверки: убрать artifact, подменить revision в fixture, испортить файл после manifest. Во всех трёх случаях job должна остановиться до внешней команды.',
'После этого выполнить отдельный согласованный staging-прогон и записать, что он подтвердил. Не переносить его длительность или успех на production без отдельной проверки доступа, отката и поведения приложения.',
]),
heading('Что остаётся за пределами разбора'),
paragraph('Учебная фикстура не подтверждает конфигурацию конкретного runner, реальное хранение artifacts, скорость npm, работу secrets, доступ к staging или rollback. Она также не делает checksum заменой контроля доступа. Эти ограничения важны: хороший разбор не превращает один небольшой технический факт в отчёт о надёжности всего релиза.'),
paragraph('Но после такого исправления меняется важное свойство: если deploy снова получит неизвестный каталог, он остановится до внешнего действия и покажет, где искать причину — build, artifact, dependency или manifest. Это меньше, чем полноценная система доставки, но достаточно для следующего шага: добавить реальный smoke-сценарий на staging и оформить отдельный путь отката для предыдущего известного artifact.'),
],
[gitlabYaml, gitlabArtifacts, npmCi, gitlabLegacyYaml],
);
export const revisions = [practiceArticle, mechanismArticle, fieldArticle];
const entryPath = fileURLToPath(import.meta.url);
if (process.argv[1] && resolve(process.argv[1]) === resolve(entryPath)) {
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions));
} else {
process.stderr.write('Usage: node upgrade-2020-03.mjs --print-revisions\\n');
process.exitCode = 1;
}
}