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
+55
View File
@@ -653,6 +653,61 @@ run_dependency_hygiene_acceptance() {
grep -q '^dependency_security_gate=true$' "$package_dir/metadata/package.env" \
|| 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