build: закрыть обходы релизного гейта тестов и проверять весь граф npm
Два гейта сборки проверяли не то, что обещали.
1. pnpm audit проверял production-подграф вместо всего lock-графа.
Гейт запускался с --prod под обоснованием «devDependencies в артефакт не
попадают». Для frontend build tooling это неверно по существу: vite и
rollup действительно не копируются на production-сервер как node_modules,
но они ИСПОЛНЯЮТСЯ на build-машине, читают наши исходники и порождают тот
самый production-бандл, который уезжает в артефакт.
Это не гипотеза: DOM clobbering в Rollup затрагивал именно генерируемый
бандл, и `pnpm audit --prod` его не показывал — по всему графу тот же
прогон дал 33 предупреждения против нуля. Критерий приёмки №47 в docs/11
формулировал «по всему графу» правильно ещё до того, как это стало правдой
в коде.
На текущем lock-файле полный граф на пороге high чист.
2. SKIP_TESTS позволял собрать production-артефакт без тестов.
Переменная была описана как «аварийное отключение тестов; для
release-сборок недопустимо». Недопустимость держалась исключительно на этой
фразе: ни metadata, ни финальная приёмка архива не проверяли, что тесты
запускались. То есть
SKIP_TESTS=true ./tools/build/build.sh
доходила до конца и выдавала обычный tarball с build_profile=production и
dependency_security_gate=true — артефакт, по которому невозможно отличить
проверенную сборку от непроверенной.
Глушила она при этом не только тесты: под тем же флагом пропускались
`tsc --noEmit` для оркестратора и `go vet` для админки, то есть проверка
типов и статический анализ того самого кода, который уезжает в production.
Выбран тот же строгий вариант, что уже принят для проверки зависимостей:
обхода нет. Готовый пакет объявляет tests_gate=true в metadata, и это
утверждение опирается на результат — обе функции прогона выставляют свой
флаг только после успешного завершения, а write_metadata отказывается
писать метаданные, если хотя бы один не подтверждён.
Приёмка закрепляет оба инварианта: --prod не может вернуться в гейт, SKIP_TESTS
не может вернуться ни в один модуль сборки и ни в README/docs, tests_gate=true
обязателен в metadata, а утверждение о прогоне обязано следовать за прогоном.
This commit is contained in:
@@ -22,6 +22,89 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
неполной; диагностика, обрывающая соединения; аварийный выход сборки, которым
|
||||
невозможно воспользоваться.
|
||||
|
||||
Четвёртый проход — failure path и релизные гейты: восстановление после
|
||||
неудачной установки, которое умело отменить само себя, и два гейта сборки,
|
||||
проверявшие не то, что обещали.
|
||||
|
||||
### Исправлено — восстановление после неудачной операции
|
||||
|
||||
- **Запись состояния отказа отменяла откат.** Обработчик ошибки в `install` и
|
||||
`reconfigure` первым делом писал в `install-state.json` фазу отказа обычным
|
||||
`await` и только потом откатывался. Эта запись — `mkdir`, `write` и `chown` в
|
||||
`/var/lib/hy2xs`, то есть она падает ровно там, где откат нужнее всего:
|
||||
заполненный диск, read-only ФС, ошибка ввода-вывода. Бросок уносил управление
|
||||
наружу, и обязательное восстановление не выполнялось вовсе — применённый
|
||||
firewall и развёрнутые сервисы оставались на сервере.
|
||||
|
||||
Необязательная телеметрия состояния стояла перед обязательным
|
||||
восстановлением. Для сбора диагностики это уже было закрыто прошлым проходом,
|
||||
для записи состояния — нет. Теперь запись обёрнута так же: неудача попадает в
|
||||
журнал строкой `failed to persist failure state, continuing with the mandatory
|
||||
rollback`, а откат продолжается.
|
||||
|
||||
- **Откат отменял сам себя.** Он был написан цепочкой `await`, а каждая его
|
||||
стадия — `systemctl`, `cp`, `rm -rf` или `nft`, то есть умеет упасть сама.
|
||||
Отказ первой стадии отменял все последующие. В `reconfigure` это означало
|
||||
сервер одновременно с применённым сломанным firewall **и** без
|
||||
восстановленных из `/etc/hy2xs/backups` конфигов — худший сценарий отказа
|
||||
лишался обеих половин восстановления сразу.
|
||||
|
||||
Внутри `rollbackCurrentState` болезнь была та же: единственная команда без
|
||||
`|| true` (`systemctl daemon-reload`) отменяла перезапуск сервисов строкой
|
||||
ниже, и восстановленные unit-файлы так и не применялись.
|
||||
|
||||
Стадии стали независимыми: выполняются все и в объявленном порядке,
|
||||
отказавшие перечисляются в журнале, наружу уходит исходная ошибка операции.
|
||||
|
||||
- **У маркера установки было два писателя с разными гарантиями.** `install`
|
||||
перезаписывал файл на месте, `reconfigure` подставлял атомарно; слабейшая
|
||||
гарантия досталась команде, которая этот файл создаёт. Перезапись на месте
|
||||
укорачивает файл до нуля и только потом наполняет — отказ между этими
|
||||
моментами оставляет половину JSON, которая не разбирается: `reconfigure`
|
||||
видит такой маркер как отсутствующий, clean-host — как присутствующий, а хост
|
||||
к этому моменту уже изменён.
|
||||
|
||||
Атомарности при этом было бы мало: `rename()` без `fsync` даёт атомарность
|
||||
видимости без долговечности, и после потери питания ext4 штатно отдаёт по
|
||||
этому пути нулевой файл. Порядок теперь: права и владелец → `fsync` файла →
|
||||
`rename` → `fsync` каталога.
|
||||
|
||||
- **Ownership-флаг маркера отвечал не на тот вопрос.** Он назывался
|
||||
`stateWritten` и взводился ПОСЛЕ успешной записи, хотя запись — это три
|
||||
операции. Отказ на `chown` оставлял файл на диске при невзведённом флаге, то
|
||||
есть давал классификацию `fatal_pre_apply` — «на сервере ничего не изменено» —
|
||||
при уже существующем `/var/lib/hy2xs/install-state.json`, который ломал
|
||||
следующую чистую установку. Флаг переименован в `stateTouched` и взводится до
|
||||
первой операции записи, как все остальные.
|
||||
|
||||
### Изменено — релизные гейты сборки
|
||||
|
||||
- **`pnpm audit` проверяет весь lock-граф, а не production-подграф.** Гейт
|
||||
запускался с `--prod` под обоснованием «devDependencies в артефакт не
|
||||
попадают». Для frontend build tooling это неверно по существу: `vite` и
|
||||
`rollup` не копируются на сервер, но исполняются на build-машине и порождают
|
||||
тот самый production-бандл. Ровно такой случай и был найден в этом же
|
||||
релизном цикле — DOM clobbering в Rollup затрагивал генерируемый бандл, а
|
||||
`--prod` его не показывал; по всему графу тот же прогон дал 33 предупреждения
|
||||
против нуля. Критерий приёмки №47 в `docs/11` формулировал это правильно ещё
|
||||
до того, как стало правдой в коде.
|
||||
|
||||
- **Удалён `SKIP_TESTS`.** Переменная была описана как «аварийное отключение
|
||||
тестов; для release-сборок недопустимо». Недопустимость держалась
|
||||
исключительно на этой фразе: ни metadata, ни финальная приёмка архива не
|
||||
проверяли, что тесты запускались, поэтому `SKIP_TESTS=true ./tools/build/build.sh`
|
||||
доходила до конца и выдавала обычный tarball с `build_profile=production` и
|
||||
`dependency_security_gate=true` — артефакт, по которому невозможно отличить
|
||||
проверенную сборку от непроверенной. Глушила она при этом не только тесты, но
|
||||
и `tsc --noEmit` с `go vet`.
|
||||
|
||||
Выбран тот же строгий вариант, что и для проверки зависимостей: обхода нет,
|
||||
а готовый пакет объявляет `tests_gate=true` в `metadata/package.env`. Поле
|
||||
опирается на фактический прогон — `write_metadata` отказывается писать
|
||||
метаданные, если хотя бы один из двух прогонов не подтверждён. Для локальной
|
||||
работы обходить нечего: `bun test`, `tsc --noEmit`, `go vet` и `go test`
|
||||
запускаются напрямую и tarball не создают.
|
||||
|
||||
### Исправлено — операции, не выполняющие обещанного
|
||||
|
||||
- **Удаление `bootstrap-admin-peer` не было отзывом доступа.** Признаком
|
||||
|
||||
Reference in New Issue
Block a user