Files
HY2XS_flamy/tools/build/lib/package.sh
T
founder 594525dd73 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, а утверждение о прогоне обязано следовать за прогоном.
2026-08-30 18:18:13 +05:00

238 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
STAGE_DIR="tools/build/output/hy2xs-install"
ADMIN_BUILD_DIR="tools/build/output/hy2xs-admin-build"
prepare_stage() {
local version="$1"
local build_id="$2"
rm -rf "$STAGE_DIR"
mkdir -p "$STAGE_DIR" "dist"
copy_dir_contents "package" "$STAGE_DIR"
chmod 0755 "$STAGE_DIR/install.sh"
find "$STAGE_DIR" -type d -exec chmod 0755 {} +
find "$STAGE_DIR" -type f -name "*.service" -exec chmod 0644 {} +
find "$STAGE_DIR" -type f -name "*.tpl" -exec chmod 0644 {} +
find "$STAGE_DIR/config" -type f -exec chmod 0644 {} +
rm -rf "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui" "$STAGE_DIR/metadata"
mkdir -p "$STAGE_DIR/orchestrator" "$STAGE_DIR/ui/hy2xs-admin" "$STAGE_DIR/metadata"
install -m 0644 LICENSE "$STAGE_DIR/LICENSE"
printf '%s\n' "$version" >"$STAGE_DIR/metadata/package.version"
printf '%s\n' "$build_id" >"$STAGE_DIR/metadata/package.build_id"
printf '%s\n' "$HY2XS_RELEASE_LINE" >"$STAGE_DIR/metadata/package.release_line"
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() {
(
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() {
# `go:embed all:dist` требует собранных frontend-ассетов, поэтому эта
# функция должна вызываться только после bundle_ui.
[ -d "${UI_SRC:-apps}/frontend/dist" ] \
|| fail "run_admin_tests must run after bundle_ui: ${UI_SRC:-apps}/frontend/dist is missing"
(
cd "${UI_SRC:-apps}"
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() {
local bun_compile_target="${BUN_COMPILE_TARGET:-bun-linux-x64}"
log_info "Building orchestrator with Bun compile target: $bun_compile_target"
(
cd orchestrator
"$BUN_BIN" install --frozen-lockfile
"$BUN_BIN" build src/cli.ts --compile --target="$bun_compile_target" --outfile ../"$STAGE_DIR"/orchestrator/hy2xs-orchestrator
)
chmod 0755 "$STAGE_DIR/orchestrator/hy2xs-orchestrator"
}
bundle_ui() {
local ui_src="${UI_SRC:-apps}"
rm -rf "$ADMIN_BUILD_DIR"
mkdir -p "$ADMIN_BUILD_DIR"
(
cd "$ui_src/frontend"
local frontend_node_old_space_size="${FRONTEND_NODE_OLD_SPACE_SIZE:-2048}"
local frontend_node_options="${NODE_OPTIONS:-}"
if [[ "$frontend_node_options" != *"--max-old-space-size="* ]]; then
frontend_node_options="${frontend_node_options:+$frontend_node_options }--max-old-space-size=${frontend_node_old_space_size}"
fi
export NODE_OPTIONS="$frontend_node_options"
log_info "Frontend Node.js options: $NODE_OPTIONS"
"$PNPM_BIN" install --frozen-lockfile
# Проверка типов идёт ПЕРЕД сборкой bundle, и это обязательный шаг релиза.
#
# Раньше её здесь не было вовсе: `build:prod` означал
# `vite build && vue-tsc --noEmit`, то есть сборка сначала тратила время на
# production bundle и только потом сообщала о типовой ошибке. Сам же
# `vue-tsc` был версии 0.35 и не находил ничего — на современном он даёт 142
# ошибки на том же коде.
#
# Теперь скрипты разделены, и порядок обратный: смысла собирать bundle из
# кода, который не проходит проверку типов, нет.
"$PNPM_BIN" run typecheck
"$PNPM_BIN" run build:prod
)
# Версия админки приходит из versions.env через ldflags: один и тот же
# исполняемый файл обязан знать версию пакета, из которого он собран.
local admin_ldflags="-s -w -X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}'"
(
cd "$ui_src"
GOOS="$HY2XS_TARGET_GOOS" GOARCH="$HY2XS_TARGET_ARCH" CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" mod download
GOOS="$HY2XS_TARGET_GOOS" GOARCH="$HY2XS_TARGET_ARCH" CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" build \
-trimpath -ldflags "$admin_ldflags" -o "../$ADMIN_BUILD_DIR/hy2xs-admin" .
)
verify_admin_version_contract "$ADMIN_BUILD_DIR/hy2xs-admin"
install -m 0755 "$ADMIN_BUILD_DIR/hy2xs-admin" "$STAGE_DIR/ui/hy2xs-admin/hy2xs-admin"
if [ -f "$ui_src/docs/sql/schema.sql" ]; then
mkdir -p "$STAGE_DIR/ui/hy2xs-admin/docs/sql"
install -m 0644 "$ui_src/docs/sql/schema.sql" "$STAGE_DIR/ui/hy2xs-admin/docs/sql/schema.sql"
fi
}
write_metadata() {
local version="$1"
local build_id="$2"
local source_git_commit
local dirty_tree="false"
source_git_commit="$(git rev-parse --short=12 HEAD 2>/dev/null || echo unknown)"
if [ -n "$(git status --porcelain 2>/dev/null || true)" ]; then
dirty_tree="true"
fi
# Значения приходят из resolve_and_verify_hysteria: они уже проверены
# по SHA-256, по `hysteria version` и по compatibility gate.
[ -n "${HYSTERIA_VERSION:-}" ] || fail "HYSTERIA_VERSION was not resolved before write_metadata"
[ -n "${HYSTERIA_ARTIFACT_URL:-}" ] || fail "HYSTERIA_ARTIFACT_URL was not resolved before write_metadata"
[ -n "${HYSTERIA_ARTIFACT_SHA256:-}" ] || fail "HYSTERIA_ARTIFACT_SHA256 was not resolved before 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'
printf 'version=%s\n' "$version"
printf 'release_line=%s\n' "$HY2XS_RELEASE_LINE"
printf 'config_schema_version=%s\n' "$HY2XS_CONFIG_SCHEMA_VERSION"
printf 'build_id=%s\n' "$build_id"
printf 'build_host_os=%s%s\n' "$HY2XS_BUILD_OS" "$HY2XS_BUILD_OS_VERSION"
printf 'build_host_arch=%s\n' "$HY2XS_BUILD_ARCH"
printf 'target_os=%s\n' "$HY2XS_TARGET_GOOS"
printf 'target_arch=%s\n' "$HY2XS_TARGET_ARCH"
printf 'target_distro=%s\n' "$HY2XS_TARGET_OS"
printf 'target_version=%s\n' "$HY2XS_TARGET_OS_VERSION"
printf 'source_git_commit=%s\n' "$source_git_commit"
printf 'dirty_tree=%s\n' "$dirty_tree"
printf 'build_profile=production\n'
printf 'orchestrator_stack=Bun+TypeScript\n'
printf 'go_version=%s\n' "$($GO_BIN version)"
printf 'bun_version=%s\n' "$($BUN_BIN --version)"
printf 'bun_compile_target=%s\n' "${BUN_COMPILE_TARGET:-unknown}"
printf 'node_version=%s\n' "$($NODE_BIN --version)"
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"
printf 'hysteria_artifact_url=%s\n' "$HYSTERIA_ARTIFACT_URL"
printf 'hysteria_artifact_sha256=%s\n' "$HYSTERIA_ARTIFACT_SHA256"
printf 'hysteria_sha_source=%s\n' "${HYSTERIA_SHA_SOURCE:-unknown}"
printf 'hysteria_channel=%s\n' "${HYSTERIA_CHANNEL:-unknown}"
printf 'hysteria_resolution=%s\n' "${HYSTERIA_RESOLUTION:-unknown}"
printf 'hysteria_resolved_at=%s\n' "${HYSTERIA_RESOLVED_AT:-unknown}"
printf 'hysteria_compat_gate=%s\n' "${HYSTERIA_COMPAT_GATE:-unknown}"
} >"$STAGE_DIR/metadata/package.env"
printf '%s\n' "$HYSTERIA_VERSION" >"$STAGE_DIR/metadata/hysteria.version"
printf '%s\n' "$HYSTERIA_ARTIFACT_URL" >"$STAGE_DIR/metadata/hysteria.url"
printf '%s\n' "$HYSTERIA_ARTIFACT_SHA256" >"$STAGE_DIR/metadata/hysteria.sha256"
printf '%s\n' "${HYSTERIA_RESOLUTION:-unknown}" >"$STAGE_DIR/metadata/hysteria.resolution"
(
cd "$STAGE_DIR"
find . -type f ! -path './metadata/checksums.txt' -print0 \
| sort -z \
| xargs -0 sha256sum >metadata/checksums.txt
)
}
create_archive() {
local version="$1"
local archive="dist/hy2xs-install-${version}.tar.gz"
rm -f "$archive"
tar --owner=0 --group=0 --numeric-owner -C "tools/build/output" -czf "$archive" "hy2xs-install"
}