build: versions.env как единый контракт продукта, платформы и toolchain

Версии были размазаны: PACKAGE_VERSION в build.sh, схема конфигурации в
profile.ts и в hy2xs.env, версии toolchain в deps.sh, Debian 13 в
нескольких местах. Расхождение уже перестало быть теоретическим - пакет
1.0.0 сообщал "HY2XS admin version v0.0.22".

Введён корневой versions.env: версия продукта, линия релиза, схема
конфигурации, целевая платформа, версии и контрольные суммы
Go/Bun/Node/pnpm, политика выбора Hysteria.

Чего в нём нет намеренно:

1. Прикладных зависимостей - у них есть pnpm-lock.yaml, bun.lock,
   go.sum. Второй слой неизбежно разъедется с настоящим графом.
2. Конкретной версии Hysteria - здесь только политика
   HYSTERIA_CHANNEL, результат резолва живёт в hysteria-lock.env.
   Пин версии здесь вернул бы ручное обновление.

Подход - проверка, а не генерация. profile.ts, hy2xs.env и
packageManager в двух package.json остаются обычными файлами, чтобы
bun test, tsc и go test работали из чистого чекаута до сборки. Новый
шаг verify_versions_contract роняет сборку до создания tarball при
расхождении. Контракт оркестратора сверяется не grep'ом по исходникам,
а выводом print-contract.ts: это доказывает, что в бинарь попало то же
значение.

Версия админки перестала быть константой и приезжает через ldflags;
собранный бинарь проверяется запуском hy2xs-admin version.

Контрольные суммы toolchain больше не передаются через окружение. Для
Bun зафиксированы обе суммы: артефакт выбирается по наличию AVX2,
поэтому одной архитектурно недостаточно. Production-сборка снова
запускается одной командой.
This commit is contained in:
2026-08-27 12:15:24 +05:00
parent abdcb881f3
commit 9da61c9482
8 changed files with 430 additions and 25 deletions
+40
View File
@@ -3,6 +3,7 @@ set -euo pipefail
require_repo_layout() {
[ -f "LICENSE" ] || fail "missing LICENSE"
[ -f "versions.env" ] || fail "missing versions.env (product/platform/toolchain contract)"
[ -d "orchestrator/src" ] || fail "missing orchestrator/src"
[ -f "orchestrator/package.json" ] || fail "missing orchestrator/package.json"
[ -f "orchestrator/bun.lock" ] || fail "missing orchestrator/bun.lock"
@@ -58,6 +59,25 @@ verify_archive() {
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.url$' || fail "archive missing pinned Hysteria artifact url"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.sha256$' || fail "archive missing pinned Hysteria sha256"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/hysteria.resolution$' || fail "archive missing Hysteria resolution marker"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/package.release_line$' || fail "archive missing product release line"
printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/package.config_schema_version$' || fail "archive missing packaged config schema version"
# Двухфазный контракт установки проверяется структурно: read-only preflight
# обязан стоять раньше первой мутации, иначе ошибочный запуск поверх
# работающего сервера снова начнёт его ломать.
local packaged_installer preflight_line first_mutation_line
packaged_installer="$(tar -xOzf "$archive" hy2xs-install/install.sh)"
printf '%s\n' "$packaged_installer" | grep -q 'preflight-install' \
|| fail "packaged install.sh does not run the read-only preflight"
preflight_line="$(printf '%s\n' "$packaged_installer" | grep -n 'preflight-install --package-dir' | head -n1 | cut -d: -f1)"
first_mutation_line="$(printf '%s\n' "$packaged_installer" \
| grep -nE '^[[:space:]]*(install -d|install -m|ln -sf|rm -rf|cp -a)' | head -n1 | cut -d: -f1)"
[ -n "$preflight_line" ] || fail "packaged install.sh: could not locate the preflight invocation"
[ -n "$first_mutation_line" ] || fail "packaged install.sh: could not locate the mutation phase"
[ "$preflight_line" -lt "$first_mutation_line" ] \
|| fail "packaged install.sh mutates the host (line $first_mutation_line) before the clean-host preflight (line $preflight_line)"
local license_text
license_text="$(tar -xOzf "$archive" hy2xs-install/LICENSE)"
@@ -118,6 +138,26 @@ verify_archive() {
printf '%s\n' "$meta" | grep -q '^build_profile=production$' || fail "metadata missing build_profile=production"
printf '%s\n' "$meta" | grep -q '^license=AGPL-3.0-only$' || fail "metadata missing license=AGPL-3.0-only"
# Поколение продукта обязано доехать до пакета: install-state строится из
# него, а reconfigure/repair по нему отличают v1 от чужой установки.
printf '%s\n' "$meta" | grep -q "^version=${HY2XS_VERSION}$" \
|| fail "metadata version disagrees with versions.env HY2XS_VERSION=${HY2XS_VERSION}"
printf '%s\n' "$meta" | grep -q "^release_line=${HY2XS_RELEASE_LINE}$" \
|| fail "metadata release_line disagrees with versions.env HY2XS_RELEASE_LINE=${HY2XS_RELEASE_LINE}"
printf '%s\n' "$meta" | grep -q "^config_schema_version=${HY2XS_CONFIG_SCHEMA_VERSION}$" \
|| fail "metadata config_schema_version disagrees with versions.env"
printf '%s\n' "$meta" | grep -q "^target_version=${HY2XS_TARGET_OS_VERSION}$" \
|| fail "metadata target_version disagrees with versions.env"
printf '%s\n' "$meta" | grep -q "^target_arch=${HY2XS_TARGET_ARCH}$" \
|| fail "metadata target_arch disagrees with versions.env"
local packaged_schema
packaged_schema="$(grep -E '^HY2XS_CONFIG_SCHEMA_VERSION=' "$tmp/hy2xs-install/config/hy2xs.env" | head -n1 | cut -d= -f2-)"
[ "$packaged_schema" = "$HY2XS_CONFIG_SCHEMA_VERSION" ] \
|| fail "packaged hy2xs.env declares schema $packaged_schema, versions.env declares $HY2XS_CONFIG_SCHEMA_VERSION"
verify_admin_version_contract "$tmp/hy2xs-install/ui/hy2xs-admin/hy2xs-admin"
if declare -F run_fix20_acceptance_subset >/dev/null 2>&1; then
run_fix20_acceptance_subset "$tmp/hy2xs-install"
fi