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:
@@ -170,7 +170,10 @@ BUILD FAILED: unsupported Hysteria stable v2.13.0
|
||||
| `HYSTERIA_GATE_PORT` | `34443` | UDP-порт для временного запуска Hysteria в gate |
|
||||
| `HYSTERIA_GATE_STATS_PORT` | `34712` | TCP-порт trafficStats в gate |
|
||||
| `GITHUB_TOKEN` | пусто | Опционально: снимает anonymous rate limit GitHub API |
|
||||
| `SKIP_TESTS` | `false` | Аварийное отключение тестов; для release-сборок недопустимо |
|
||||
|
||||
Тесты, проверка типов и проверка зависимостей переменными не управляются: у них
|
||||
нет аварийного выхода. Готовый пакет объявляет об этом полями `tests_gate=true`
|
||||
и `dependency_security_gate=true` в `metadata/package.env`.
|
||||
|
||||
Обновить lock-файл под текущий upstream:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+41
-10
@@ -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"
|
||||
|
||||
@@ -23,9 +23,24 @@ set -euo pipefail
|
||||
# govulncheck — анализ достижимости: уязвимость считается только если до неё
|
||||
# есть путь вызова из нашего кода. Покрывает и stdlib, то есть
|
||||
# ровно то, что версия Go привносит в production-бинарь;
|
||||
# pnpm audit — граф npm без анализа достижимости, поэтому ограничен
|
||||
# production-зависимостями и порогом PNPM_AUDIT_LEVEL:
|
||||
# devDependencies в артефакт не попадают.
|
||||
# pnpm audit — ВЕСЬ lock-граф frontend без анализа достижимости, с порогом
|
||||
# PNPM_AUDIT_LEVEL.
|
||||
#
|
||||
# Про «весь граф» отдельно, потому что здесь стояло `--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
|
||||
}
|
||||
|
||||
# Анализ production-зависимостей frontend.
|
||||
# Анализ всего lock-графа frontend, включая build tooling.
|
||||
run_frontend_vulnerability_gate() {
|
||||
local ui_src="${UI_SRC:-apps}"
|
||||
local report status
|
||||
|
||||
log_step "Security: pnpm audit --prod (порог ${PNPM_AUDIT_LEVEL})"
|
||||
log_step "Security: pnpm audit по всему графу (порог ${PNPM_AUDIT_LEVEL})"
|
||||
|
||||
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=$?
|
||||
set -e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user