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` не было отзывом доступа.** Признаком
|
- **Удаление `bootstrap-admin-peer` не было отзывом доступа.** Признаком
|
||||||
|
|||||||
@@ -957,11 +957,19 @@ export GITHUB_TOKEN=<token>
|
|||||||
4. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander;
|
4. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander;
|
||||||
5. собирает orchestrator, frontend и backend, проставляя версию админки из контракта;
|
5. собирает orchestrator, frontend и backend, проставляя версию админки из контракта;
|
||||||
6. прогоняет `go vet` и `go test` для HY2XS admin;
|
6. прогоняет `go vet` и `go test` для HY2XS admin;
|
||||||
7. проверяет граф зависимостей на известные уязвимости (`govulncheck ./...` и `pnpm audit --prod`);
|
7. проверяет граф зависимостей на известные уязвимости (`govulncheck ./...` и `pnpm audit` по всему lock‑графу);
|
||||||
8. формирует архив и прогоняет acceptance‑проверки.
|
8. формирует архив и прогоняет acceptance‑проверки.
|
||||||
|
|
||||||
Любой сбой на шагах 1–7 останавливает сборку до создания пакета.
|
Любой сбой на шагах 1–7 останавливает сборку до создания пакета.
|
||||||
|
|
||||||
|
Тесты и типы (шаги 2 и 6) — такой же обязательный гейт, как проверка
|
||||||
|
зависимостей: переменной, которая их отключает, не существует. Готовый пакет
|
||||||
|
объявляет об этом полем `tests_gate=true` в `metadata/package.env`, и это
|
||||||
|
утверждение опирается на фактический прогон, а не на намерение.
|
||||||
|
|
||||||
|
Для локальной работы обходить нечего: `bun test`, `bun x tsc --noEmit`,
|
||||||
|
`go vet ./...` и `go test ./...` запускаются напрямую и tarball не создают.
|
||||||
|
|
||||||
Переменные, управляющие выбором версии Hysteria:
|
Переменные, управляющие выбором версии Hysteria:
|
||||||
|
|
||||||
| Переменная | По умолчанию | Назначение |
|
| Переменная | По умолчанию | Назначение |
|
||||||
@@ -983,6 +991,13 @@ export GITHUB_TOKEN=<token>
|
|||||||
цикл и падала на последнем шаге. Документированная операция, которую продукт сам
|
цикл и падала на последнем шаге. Документированная операция, которую продукт сам
|
||||||
же запрещает, — хуже отсутствующей.
|
же запрещает, — хуже отсутствующей.
|
||||||
|
|
||||||
|
`pnpm audit` при этом проверяет **весь** lock‑граф frontend, а не только
|
||||||
|
production‑подграф. Причина в том, что build tooling исполняется на build‑машине
|
||||||
|
и порождает production‑бандл: уязвимость в `vite`/`rollup` уезжает в артефакт,
|
||||||
|
хотя сами они на сервер не копируются. Ровно такой случай и был найден — DOM
|
||||||
|
clobbering в Rollup затрагивал генерируемый бандл, а проверка по одному
|
||||||
|
production‑подграфу его не показывала.
|
||||||
|
|
||||||
Если advisory вышло в неудачный момент, чинится это обновлением графа
|
Если advisory вышло в неудачный момент, чинится это обновлением графа
|
||||||
(`apps/go.sum`, `apps/frontend/pnpm-lock.yaml`) или версии toolchain в
|
(`apps/go.sum`, `apps/frontend/pnpm-lock.yaml`) или версии toolchain в
|
||||||
`versions.env`. Для локальной работы обходить нечего: `go test ./...`,
|
`versions.env`. Для локальной работы обходить нечего: `go test ./...`,
|
||||||
|
|||||||
@@ -152,7 +152,20 @@ bundle из кода, который не проходит проверку ти
|
|||||||
| Проверка | Что покрывает | Порог |
|
| Проверка | Что покрывает | Порог |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `govulncheck ./...` | Go-граф **и stdlib**, с анализом достижимости: уязвимость считается только при наличии пути вызова из нашего кода | любая вызываемая |
|
| `govulncheck ./...` | Go-граф **и stdlib**, с анализом достижимости: уязвимость считается только при наличии пути вызова из нашего кода | любая вызываемая |
|
||||||
| `pnpm audit --prod` | production-зависимости frontend, без анализа достижимости | `PNPM_AUDIT_LEVEL` |
|
| `pnpm audit` | **весь** lock-граф frontend, включая build tooling, без анализа достижимости | `PNPM_AUDIT_LEVEL` |
|
||||||
|
|
||||||
|
Про «весь граф» отдельно, потому что здесь стояло `--prod` с обоснованием
|
||||||
|
«devDependencies в артефакт не попадают».
|
||||||
|
|
||||||
|
Для frontend build tooling это обоснование неверно по существу. `vite` и
|
||||||
|
`rollup` действительно не копируются на production-сервер как `node_modules`.
|
||||||
|
Но они **исполняются на build-машине, читают наши исходники и порождают тот
|
||||||
|
самый production-бандл**, который уезжает в артефакт. Уязвимость в них — это
|
||||||
|
уязвимость в том, что мы выпускаем.
|
||||||
|
|
||||||
|
Это не гипотеза: DOM clobbering в Rollup затрагивал именно генерируемый бандл, а
|
||||||
|
проверка по одному production-подграфу его не показывала. По всему графу тот же
|
||||||
|
прогон дал 33 предупреждения против нуля.
|
||||||
|
|
||||||
Версия `govulncheck` пиньтся в `versions.env`, а база уязвимостей подтягивается
|
Версия `govulncheck` пиньтся в `versions.env`, а база уязвимостей подтягивается
|
||||||
на каждом запуске: пин инструмента не должен превращаться в пин знаний о мире.
|
на каждом запуске: пин инструмента не должен превращаться в пин знаний о мире.
|
||||||
@@ -180,6 +193,35 @@ bundle из кода, который не проходит проверку ти
|
|||||||
локальной работы обходить нечего: `go test ./...`, `govulncheck ./...` и
|
локальной работы обходить нечего: `go test ./...`, `govulncheck ./...` и
|
||||||
`pnpm audit` запускаются напрямую и tarball не создают.
|
`pnpm audit` запускаются напрямую и tarball не создают.
|
||||||
|
|
||||||
|
### Тесты и типы
|
||||||
|
|
||||||
|
Та же политика и по той же причине. Аварийного выхода у этого шага **нет**:
|
||||||
|
переменной, отключающей тесты, не существует.
|
||||||
|
|
||||||
|
Проверяется на трёх участках:
|
||||||
|
|
||||||
|
| Шаг сборки | Что запускается |
|
||||||
|
| --- | --- |
|
||||||
|
| `run_orchestrator_tests` | `bun x tsc --noEmit`, `bun test` |
|
||||||
|
| `bundle_ui` | `pnpm run typecheck` (`vue-tsc --noEmit`) до сборки bundle |
|
||||||
|
| `run_admin_tests` | `go vet ./...`, `go test ./...` |
|
||||||
|
|
||||||
|
Готовый пакет объявляет об этом полем `tests_gate=true` в
|
||||||
|
`metadata/package.env` — так же, как `dependency_security_gate` и
|
||||||
|
`hysteria_compat_gate`. Значение у поля ровно одно, потому что не бывает
|
||||||
|
пакета, собранного с пропущенными тестами: обе функции прогона выставляют свой
|
||||||
|
флаг **после** успешного завершения, а `write_metadata` отказывается писать
|
||||||
|
метаданные, если хотя бы один из них не выставлен. То есть поле остаётся
|
||||||
|
утверждением о результате, а не переключателем.
|
||||||
|
|
||||||
|
Здесь существовала переменная, описанная как «аварийное отключение тестов; для
|
||||||
|
release-сборок недопустимо». Недопустимость держалась исключительно на этой
|
||||||
|
фразе: ни metadata, ни финальная приёмка архива не проверяли, что тесты
|
||||||
|
запускались, поэтому сборка с ней доходила до конца и выдавала внешне
|
||||||
|
неотличимый production-tarball. Глушила она при этом не только тесты, но и
|
||||||
|
`tsc --noEmit` с `go vet` — то есть проверку типов и статический анализ того
|
||||||
|
самого кода, который уезжает в production. История — в `CHANGELOG.md`.
|
||||||
|
|
||||||
### Проверка, а не генерация
|
### Проверка, а не генерация
|
||||||
|
|
||||||
`profile.ts`, `package/config/hy2xs.env` и `packageManager` в двух `package.json`
|
`profile.ts`, `package/config/hy2xs.env` и `packageManager` в двух `package.json`
|
||||||
|
|||||||
@@ -534,6 +534,31 @@ metadata. Подробности в [docs/02](02-build-layer-and-package.md); з
|
|||||||
трактовать как «уязвимостей нет». По той же причине недоступность реестра npm
|
трактовать как «уязвимостей нет». По той же причине недоступность реестра npm
|
||||||
для `pnpm audit` — это отказ проверки, а не её отрицательный результат.
|
для `pnpm audit` — это отказ проверки, а не её отрицательный результат.
|
||||||
|
|
||||||
|
`pnpm audit` проверяет **весь** lock-граф frontend, а не production-подграф:
|
||||||
|
build tooling исполняется на build-машине и порождает production-бандл, поэтому
|
||||||
|
уязвимость в нём уезжает в артефакт. Приёмка сборки следит, чтобы `--prod` не
|
||||||
|
вернулся в гейт.
|
||||||
|
|
||||||
|
## A11a. Обязательные тесты (build)
|
||||||
|
|
||||||
|
Гейт тестов устроен так же, как гейт зависимостей: аварийного выхода нет,
|
||||||
|
результат виден по готовому артефакту.
|
||||||
|
|
||||||
|
| Шаг сборки | Что запускается |
|
||||||
|
| --- | --- |
|
||||||
|
| `run_orchestrator_tests` | `bun x tsc --noEmit`, `bun test` |
|
||||||
|
| `bundle_ui` | `pnpm run typecheck` до сборки bundle |
|
||||||
|
| `run_admin_tests` | `go vet ./...`, `go test ./...` |
|
||||||
|
|
||||||
|
Приёмка проверяет:
|
||||||
|
|
||||||
|
- отключающей тесты переменной нет ни в одном модуле сборки, ни в README/docs
|
||||||
|
(место для истории — `CHANGELOG.md`);
|
||||||
|
- `metadata/package.env` содержит `tests_gate=true`;
|
||||||
|
- утверждение о прогоне выставляется **после** самого прогона, а не до него;
|
||||||
|
- `write_metadata` отказывается писать метаданные, если хотя бы один из двух
|
||||||
|
прогонов не подтверждён.
|
||||||
|
|
||||||
## A12. Приёмка проверяет код, а не упоминания
|
## A12. Приёмка проверяет код, а не упоминания
|
||||||
|
|
||||||
Два контракта приёмки на снимке до этого патча **гарантированно роняли сборку на
|
Два контракта приёмки на снимке до этого патча **гарантированно роняли сборку на
|
||||||
@@ -953,7 +978,7 @@ hy2xs-orchestrator doctor
|
|||||||
37. отключённый `bootstrap-admin-peer` остаётся отключённым после перезапуска
|
37. отключённый `bootstrap-admin-peer` остаётся отключённым после перезапуска
|
||||||
38. резервная копия с `includeSecrets=true` завершается ошибкой целиком, если секрет хотя бы одного пира недоступен
|
38. резервная копия с `includeSecrets=true` завершается ошибкой целиком, если секрет хотя бы одного пира недоступен
|
||||||
39. админка не генерирует `HYSTERIA2_TRAFFIC_STATS_SECRET` сама: пустой env при пустой базе — отказ старта
|
39. админка не генерирует `HYSTERIA2_TRAFFIC_STATS_SECRET` сама: пустой env при пустой базе — отказ старта
|
||||||
40. проверка зависимостей на уязвимости не имеет обходов ни в сборке, ни в документации
|
40. проверка зависимостей на уязвимости не имеет обходов ни в сборке, ни в документации, и покрывает весь lock-граф frontend
|
||||||
41. `apps/go.mod` объявляет `toolchain`, совпадающий с `GO_VERSION` из `versions.env`
|
41. `apps/go.mod` объявляет `toolchain`, совпадающий с `GO_VERSION` из `versions.env`
|
||||||
42. `tools/dev/doctor.sh` / `doctor.ps1` показывают расхождение среды разработки с `versions.env`
|
42. `tools/dev/doctor.sh` / `doctor.ps1` показывают расхождение среды разработки с `versions.env`
|
||||||
43. маршруты-алиасы `/:id/client-url` и `/:id/qr` удалены и не входят в публичный API v1
|
43. маршруты-алиасы `/:id/client-url` и `/:id/qr` удалены и не входят в публичный API v1
|
||||||
@@ -967,3 +992,4 @@ hy2xs-orchestrator doctor
|
|||||||
51. невозможность записать состояние отказа не отменяет откат: восстановление выполняется, в журнале остаётся отметка о неудавшейся записи
|
51. невозможность записать состояние отказа не отменяет откат: восстановление выполняется, в журнале остаётся отметка о неудавшейся записи
|
||||||
52. `install-state.json` пишется одним писателем, атомарно и с `fsync` файла и каталога: после потери питания на диске лежит либо прежний полный документ, либо новый полный
|
52. `install-state.json` пишется одним писателем, атомарно и с `fsync` файла и каталога: после потери питания на диске лежит либо прежний полный документ, либо новый полный
|
||||||
53. ownership-флаг маркера установки взводится **до** записи, поэтому отказ на `chown` не даёт `fatal_pre_apply` при уже созданном файле
|
53. ownership-флаг маркера установки взводится **до** записи, поэтому отказ на `chown` не даёт `fatal_pre_apply` при уже созданном файле
|
||||||
|
54. тесты и проверка типов не имеют обходов ни в сборке, ни в документации; `metadata/package.env` содержит `tests_gate=true`, и это утверждение опирается на фактический прогон
|
||||||
|
|||||||
@@ -170,7 +170,10 @@ BUILD FAILED: unsupported Hysteria stable v2.13.0
|
|||||||
| `HYSTERIA_GATE_PORT` | `34443` | UDP-порт для временного запуска Hysteria в gate |
|
| `HYSTERIA_GATE_PORT` | `34443` | UDP-порт для временного запуска Hysteria в gate |
|
||||||
| `HYSTERIA_GATE_STATS_PORT` | `34712` | TCP-порт trafficStats в gate |
|
| `HYSTERIA_GATE_STATS_PORT` | `34712` | TCP-порт trafficStats в gate |
|
||||||
| `GITHUB_TOKEN` | пусто | Опционально: снимает anonymous rate limit GitHub API |
|
| `GITHUB_TOKEN` | пусто | Опционально: снимает anonymous rate limit GitHub API |
|
||||||
| `SKIP_TESTS` | `false` | Аварийное отключение тестов; для release-сборок недопустимо |
|
|
||||||
|
Тесты, проверка типов и проверка зависимостей переменными не управляются: у них
|
||||||
|
нет аварийного выхода. Готовый пакет объявляет об этом полями `tests_gate=true`
|
||||||
|
и `dependency_security_gate=true` в `metadata/package.env`.
|
||||||
|
|
||||||
Обновить lock-файл под текущий upstream:
|
Обновить lock-файл под текущий upstream:
|
||||||
|
|
||||||
|
|||||||
@@ -653,6 +653,61 @@ run_dependency_hygiene_acceptance() {
|
|||||||
grep -q '^dependency_security_gate=true$' "$package_dir/metadata/package.env" \
|
grep -q '^dependency_security_gate=true$' "$package_dir/metadata/package.env" \
|
||||||
|| fail "acceptance: релизный пакет обязан собираться с включённой проверкой зависимостей"
|
|| fail "acceptance: релизный пакет обязан собираться с включённой проверкой зависимостей"
|
||||||
|
|
||||||
|
log_step "Acceptance: the npm gate covers the whole lock graph, not only production deps"
|
||||||
|
# `--prod` не показывал уязвимости в vite/rollup, хотя они исполняются на
|
||||||
|
# build-машине и порождают production-бандл. Ровно такой класс и был найден
|
||||||
|
# (DOM clobbering в Rollup затрагивал генерируемый бандл).
|
||||||
|
! code_without_comments tools/build/lib/security.sh | grep -q 'audit --prod' \
|
||||||
|
|| fail "acceptance: гейт зависимостей снова проверяет только production-подграф npm"
|
||||||
|
code_without_comments tools/build/lib/security.sh \
|
||||||
|
| grep -q 'audit --audit-level "\$PNPM_AUDIT_LEVEL"' \
|
||||||
|
|| fail "acceptance: гейт зависимостей обязан проверять весь lock-граф frontend"
|
||||||
|
|
||||||
|
log_step "Acceptance: the release is built with a mandatory test gate"
|
||||||
|
# SKIP_TESTS=true доходил до конца сборки и выдавал внешне неотличимый
|
||||||
|
# production-артефакт: ни metadata, ни финальная приёмка не проверяли, что
|
||||||
|
# тесты запускались. Глушил он при этом и `tsc --noEmit`, и `go vet`.
|
||||||
|
local test_bypass_file
|
||||||
|
for test_bypass_file in \
|
||||||
|
tools/build/build.sh \
|
||||||
|
tools/build/lib/package.sh \
|
||||||
|
tools/build/lib/security.sh \
|
||||||
|
tools/build/lib/versions.sh \
|
||||||
|
tools/build/lib/deps.sh \
|
||||||
|
tools/build/lib/verify.sh
|
||||||
|
do
|
||||||
|
[ -z "$(code_without_comments "$test_bypass_file" | grep -F -- 'SKIP_TESTS' || true)" ] \
|
||||||
|
|| fail "acceptance: обход тестов SKIP_TESTS вернулся в $test_bypass_file"
|
||||||
|
done
|
||||||
|
! grep -rq 'SKIP_TESTS' README.md docs tools/build/README.md \
|
||||||
|
|| fail "acceptance: README/docs снова описывают обход тестов, которого нет в сборке; место для истории — CHANGELOG.md"
|
||||||
|
grep -q '^tests_gate=true$' "$package_dir/metadata/package.env" \
|
||||||
|
|| fail "acceptance: релизный пакет обязан собираться с пройденными тестами"
|
||||||
|
# Утверждение обязано опираться на результат, а не на намерение.
|
||||||
|
grep -q 'ORCHESTRATOR_TESTS_PASSED' tools/build/lib/package.sh \
|
||||||
|
|| fail "acceptance: прогон тестов оркестратора не фиксируется результатом"
|
||||||
|
grep -q 'ADMIN_TESTS_PASSED' tools/build/lib/package.sh \
|
||||||
|
|| fail "acceptance: прогон тестов админки не фиксируется результатом"
|
||||||
|
"$BUN_BIN" -e '
|
||||||
|
const source = require("node:fs").readFileSync("tools/build/lib/package.sh", "utf8");
|
||||||
|
for (const [fn, flag] of [
|
||||||
|
["run_orchestrator_tests()", "ORCHESTRATOR_TESTS_PASSED=\"true\""],
|
||||||
|
["run_admin_tests()", "ADMIN_TESTS_PASSED=\"true\""]
|
||||||
|
]) {
|
||||||
|
const start = source.indexOf(fn);
|
||||||
|
if (start < 0) throw new Error("не найдена функция " + fn);
|
||||||
|
const rest = source.slice(start);
|
||||||
|
// Границей тела служит закрывающая скобка в первой позиции строки.
|
||||||
|
// Поиск регуляркой, а не подстрокой: файл может быть выгружен с CRLF.
|
||||||
|
const end = rest.search(/\n\}[\r\n]/);
|
||||||
|
const body = end < 0 ? rest : rest.slice(0, end);
|
||||||
|
const set = body.indexOf(flag);
|
||||||
|
const run = body.lastIndexOf("|| fail");
|
||||||
|
if (set < 0) throw new Error(fn + ": результат прогона не фиксируется");
|
||||||
|
if (set < run) throw new Error(fn + ": результат объявляется раньше, чем получен");
|
||||||
|
}
|
||||||
|
' || fail "acceptance: утверждение о пройденных тестах обязано следовать за прогоном"
|
||||||
|
|
||||||
# У шага не должно быть обходов — ни объявленных, ни забытых.
|
# У шага не должно быть обходов — ни объявленных, ни забытых.
|
||||||
#
|
#
|
||||||
# Раньше их было два: ALLOW_VULNERABLE_DEPENDENCIES=true писал в metadata
|
# Раньше их было два: ALLOW_VULNERABLE_DEPENDENCIES=true писал в metadata
|
||||||
|
|||||||
+41
-10
@@ -28,26 +28,42 @@ prepare_stage() {
|
|||||||
printf '%s\n' "$HY2XS_CONFIG_SCHEMA_VERSION" >"$STAGE_DIR/metadata/package.config_schema_version"
|
printf '%s\n' "$HY2XS_CONFIG_SCHEMA_VERSION" >"$STAGE_DIR/metadata/package.config_schema_version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Тесты — обязательный шаг релиза, аварийного выхода у него НЕТ.
|
||||||
|
#
|
||||||
|
# Это то же решение и по той же причине, что и у проверки зависимостей.
|
||||||
|
#
|
||||||
|
# Раньше существовал SKIP_TESTS=true, и README описывал его как «аварийное
|
||||||
|
# отключение тестов; для release-сборок недопустимо». Недопустимость держалась
|
||||||
|
# исключительно на этой фразе: ни metadata, ни финальная приёмка архива не
|
||||||
|
# проверяли, что тесты вообще запускались. То есть
|
||||||
|
#
|
||||||
|
# SKIP_TESTS=true ./tools/build/build.sh
|
||||||
|
#
|
||||||
|
# доходила до конца и выдавала обычный tarball с build_profile=production и
|
||||||
|
# dependency_security_gate=true — артефакт, по которому невозможно отличить
|
||||||
|
# проверенную сборку от непроверенной.
|
||||||
|
#
|
||||||
|
# Глушил он при этом не только тесты: под тем же флагом пропускались
|
||||||
|
# `tsc --noEmit` для оркестратора и `go vet` для админки, то есть проверка типов
|
||||||
|
# и статический анализ того самого кода, который уезжает в production.
|
||||||
|
#
|
||||||
|
# Для локальной работы обходить нечего: `bun test`, `bun x tsc --noEmit`,
|
||||||
|
# `go vet ./...` и `go test ./...` запускаются напрямую и tarball не создают.
|
||||||
run_orchestrator_tests() {
|
run_orchestrator_tests() {
|
||||||
if [ "${SKIP_TESTS:-false}" = "true" ]; then
|
|
||||||
log_info "WARNING: orchestrator tests skipped (SKIP_TESTS=true)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
(
|
(
|
||||||
cd orchestrator
|
cd orchestrator
|
||||||
"$BUN_BIN" install --frozen-lockfile
|
"$BUN_BIN" install --frozen-lockfile
|
||||||
"$BUN_BIN" x tsc --noEmit
|
"$BUN_BIN" x tsc --noEmit
|
||||||
"$BUN_BIN" test
|
"$BUN_BIN" test
|
||||||
) || fail "orchestrator contract tests failed"
|
) || fail "orchestrator contract tests failed"
|
||||||
|
|
||||||
|
# Утверждение о результате выставляется ПОСЛЕ получения результата, ровно как
|
||||||
|
# DEPENDENCY_SECURITY_GATE.
|
||||||
|
ORCHESTRATOR_TESTS_PASSED="true"
|
||||||
|
export ORCHESTRATOR_TESTS_PASSED
|
||||||
}
|
}
|
||||||
|
|
||||||
run_admin_tests() {
|
run_admin_tests() {
|
||||||
if [ "${SKIP_TESTS:-false}" = "true" ]; then
|
|
||||||
log_info "WARNING: HY2XS admin tests skipped (SKIP_TESTS=true)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# `go:embed all:dist` требует собранных frontend-ассетов, поэтому эта
|
# `go:embed all:dist` требует собранных frontend-ассетов, поэтому эта
|
||||||
# функция должна вызываться только после bundle_ui.
|
# функция должна вызываться только после bundle_ui.
|
||||||
[ -d "${UI_SRC:-apps}/frontend/dist" ] \
|
[ -d "${UI_SRC:-apps}/frontend/dist" ] \
|
||||||
@@ -58,6 +74,9 @@ run_admin_tests() {
|
|||||||
GOTOOLCHAIN=local "$GO_BIN" vet ./...
|
GOTOOLCHAIN=local "$GO_BIN" vet ./...
|
||||||
GOTOOLCHAIN=local "$GO_BIN" test ./...
|
GOTOOLCHAIN=local "$GO_BIN" test ./...
|
||||||
) || fail "HY2XS admin contract tests failed"
|
) || fail "HY2XS admin contract tests failed"
|
||||||
|
|
||||||
|
ADMIN_TESTS_PASSED="true"
|
||||||
|
export ADMIN_TESTS_PASSED
|
||||||
}
|
}
|
||||||
|
|
||||||
build_orchestrator() {
|
build_orchestrator() {
|
||||||
@@ -148,6 +167,17 @@ write_metadata() {
|
|||||||
validate_hysteria_version_format "$HYSTERIA_VERSION"
|
validate_hysteria_version_format "$HYSTERIA_VERSION"
|
||||||
validate_hysteria_sha256_format "$HYSTERIA_ARTIFACT_SHA256"
|
validate_hysteria_sha256_format "$HYSTERIA_ARTIFACT_SHA256"
|
||||||
|
|
||||||
|
# tests_gate — утверждение о готовом артефакте, а не переключатель.
|
||||||
|
#
|
||||||
|
# Значение у него ровно одно, потому что не бывает пакета, собранного с
|
||||||
|
# пропущенными тестами: обе функции выставляют свой флаг только после
|
||||||
|
# успешного прогона. Поле нужно, чтобы это было ВИДНО по tarball — раньше
|
||||||
|
# SKIP_TESTS=true выдавала внешне неотличимый production-артефакт.
|
||||||
|
[ "${ORCHESTRATOR_TESTS_PASSED:-false}" = "true" ] \
|
||||||
|
|| fail "write_metadata: контракты оркестратора не проверялись; тесты обязательны для релизного пакета"
|
||||||
|
[ "${ADMIN_TESTS_PASSED:-false}" = "true" ] \
|
||||||
|
|| fail "write_metadata: контракты админки не проверялись; тесты обязательны для релизного пакета"
|
||||||
|
|
||||||
{
|
{
|
||||||
printf 'name=HY2XS\n'
|
printf 'name=HY2XS\n'
|
||||||
printf 'license=AGPL-3.0-only\n'
|
printf 'license=AGPL-3.0-only\n'
|
||||||
@@ -172,6 +202,7 @@ write_metadata() {
|
|||||||
printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)"
|
printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)"
|
||||||
printf 'govulncheck_version=%s\n' "${GOVULNCHECK_VERSION:-unknown}"
|
printf 'govulncheck_version=%s\n' "${GOVULNCHECK_VERSION:-unknown}"
|
||||||
printf 'dependency_security_gate=%s\n' "${DEPENDENCY_SECURITY_GATE:-unknown}"
|
printf 'dependency_security_gate=%s\n' "${DEPENDENCY_SECURITY_GATE:-unknown}"
|
||||||
|
printf 'tests_gate=true\n'
|
||||||
printf 'hysteria_source=official-upstream\n'
|
printf 'hysteria_source=official-upstream\n'
|
||||||
printf 'hysteria_target=linux-amd64\n'
|
printf 'hysteria_target=linux-amd64\n'
|
||||||
printf 'hysteria_version=%s\n' "$HYSTERIA_VERSION"
|
printf 'hysteria_version=%s\n' "$HYSTERIA_VERSION"
|
||||||
|
|||||||
@@ -23,9 +23,24 @@ set -euo pipefail
|
|||||||
# govulncheck — анализ достижимости: уязвимость считается только если до неё
|
# govulncheck — анализ достижимости: уязвимость считается только если до неё
|
||||||
# есть путь вызова из нашего кода. Покрывает и stdlib, то есть
|
# есть путь вызова из нашего кода. Покрывает и stdlib, то есть
|
||||||
# ровно то, что версия Go привносит в production-бинарь;
|
# ровно то, что версия Go привносит в production-бинарь;
|
||||||
# pnpm audit — граф npm без анализа достижимости, поэтому ограничен
|
# pnpm audit — ВЕСЬ lock-граф frontend без анализа достижимости, с порогом
|
||||||
# production-зависимостями и порогом PNPM_AUDIT_LEVEL:
|
# PNPM_AUDIT_LEVEL.
|
||||||
# devDependencies в артефакт не попадают.
|
#
|
||||||
|
# Про «весь граф» отдельно, потому что здесь стояло `--prod` с обоснованием
|
||||||
|
# «devDependencies в артефакт не попадают».
|
||||||
|
#
|
||||||
|
# Для frontend build tooling это обоснование неверно по существу. Да, vite и
|
||||||
|
# rollup не копируются на production-сервер как node_modules. Но они
|
||||||
|
# ИСПОЛНЯЮТСЯ на build-машине, читают наши исходники и ПОРОЖДАЮТ тот самый
|
||||||
|
# production-бандл, который уезжает в артефакт. Уязвимость в них — это
|
||||||
|
# уязвимость в том, что мы выпускаем.
|
||||||
|
#
|
||||||
|
# Это не гипотеза. GHSA по DOM clobbering в Rollup затрагивал именно
|
||||||
|
# генерируемый бандл, то есть уезжал в production, — и `pnpm audit --prod` его
|
||||||
|
# не показывал. По всему графу тот же прогон дал 33 предупреждения против нуля.
|
||||||
|
#
|
||||||
|
# docs/11 формулировал критерий приёмки правильно («pnpm audit по всему графу»)
|
||||||
|
# ещё до того, как это стало правдой в коде.
|
||||||
|
|
||||||
# Аварийного выхода у этого шага НЕТ, и это осознанное решение.
|
# Аварийного выхода у этого шага НЕТ, и это осознанное решение.
|
||||||
#
|
#
|
||||||
@@ -95,15 +110,15 @@ $report"
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Анализ production-зависимостей frontend.
|
# Анализ всего lock-графа frontend, включая build tooling.
|
||||||
run_frontend_vulnerability_gate() {
|
run_frontend_vulnerability_gate() {
|
||||||
local ui_src="${UI_SRC:-apps}"
|
local ui_src="${UI_SRC:-apps}"
|
||||||
local report status
|
local report status
|
||||||
|
|
||||||
log_step "Security: pnpm audit --prod (порог ${PNPM_AUDIT_LEVEL})"
|
log_step "Security: pnpm audit по всему графу (порог ${PNPM_AUDIT_LEVEL})"
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --prod --audit-level "$PNPM_AUDIT_LEVEL" 2>&1)"
|
report="$(cd "$ui_src/frontend" && "$PNPM_BIN" audit --audit-level "$PNPM_AUDIT_LEVEL" 2>&1)"
|
||||||
status=$?
|
status=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user