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:
2026-08-30 18:18:13 +05:00
parent e84fdedc4b
commit 594525dd73
8 changed files with 290 additions and 20 deletions
+41 -10
View File
@@ -28,26 +28,42 @@ prepare_stage() {
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() {
if [ "${SKIP_TESTS:-false}" = "true" ]; then
log_info "WARNING: orchestrator tests skipped (SKIP_TESTS=true)"
return 0
fi
(
cd orchestrator
"$BUN_BIN" install --frozen-lockfile
"$BUN_BIN" x tsc --noEmit
"$BUN_BIN" test
) || fail "orchestrator contract tests failed"
# Утверждение о результате выставляется ПОСЛЕ получения результата, ровно как
# DEPENDENCY_SECURITY_GATE.
ORCHESTRATOR_TESTS_PASSED="true"
export ORCHESTRATOR_TESTS_PASSED
}
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-ассетов, поэтому эта
# функция должна вызываться только после bundle_ui.
[ -d "${UI_SRC:-apps}/frontend/dist" ] \
@@ -58,6 +74,9 @@ run_admin_tests() {
GOTOOLCHAIN=local "$GO_BIN" vet ./...
GOTOOLCHAIN=local "$GO_BIN" test ./...
) || fail "HY2XS admin contract tests failed"
ADMIN_TESTS_PASSED="true"
export ADMIN_TESTS_PASSED
}
build_orchestrator() {
@@ -148,6 +167,17 @@ write_metadata() {
validate_hysteria_version_format "$HYSTERIA_VERSION"
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 'license=AGPL-3.0-only\n'
@@ -172,6 +202,7 @@ write_metadata() {
printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)"
printf 'govulncheck_version=%s\n' "${GOVULNCHECK_VERSION:-unknown}"
printf 'dependency_security_gate=%s\n' "${DEPENDENCY_SECURITY_GATE:-unknown}"
printf 'tests_gate=true\n'
printf 'hysteria_source=official-upstream\n'
printf 'hysteria_target=linux-amd64\n'
printf 'hysteria_version=%s\n' "$HYSTERIA_VERSION"