From 9da61c9482ed39005bddc9eb2fa650bb507dc1c5 Mon Sep 17 00:00:00 2001 From: Crimson Date: Thu, 27 Aug 2026 12:15:24 +0500 Subject: [PATCH] =?UTF-8?q?build:=20versions.env=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=BA=D1=82=20=D0=BF=D1=80=D0=BE=D0=B4=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D0=B0,=20=D0=BF=D0=BB=D0=B0=D1=82=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D1=8B=20=D0=B8=20toolchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Версии были размазаны: 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-сборка снова запускается одной командой. --- apps/model/constant/system.go | 12 +- orchestrator/tools/print-contract.ts | 24 +++ tools/build/build.sh | 13 +- tools/build/lib/deps.sh | 44 +++-- tools/build/lib/package.sh | 28 +++- tools/build/lib/verify.sh | 40 +++++ tools/build/lib/versions.sh | 232 +++++++++++++++++++++++++++ versions.env | 62 +++++++ 8 files changed, 430 insertions(+), 25 deletions(-) create mode 100644 orchestrator/tools/print-contract.ts create mode 100644 tools/build/lib/versions.sh create mode 100644 versions.env diff --git a/apps/model/constant/system.go b/apps/model/constant/system.go index 4d3ad82..e7be77e 100644 --- a/apps/model/constant/system.go +++ b/apps/model/constant/system.go @@ -30,5 +30,15 @@ var ( const ( TokenType = "Bearer" - Version = "v0.0.22" ) + +// Version — версия пакета HY2XS, из которого собран этот бинарник. +// +// Значение проставляется на сборке через +// `-ldflags -X hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}`, +// где HY2XS_VERSION приходит из корневого versions.env. Собственной +// константы здесь нет намеренно: захардкоженная версия админки уже успела +// разъехаться с версией пакета. +// +// "dev" остаётся только для сборок вне tools/build/build.sh. +var Version = "dev" diff --git a/orchestrator/tools/print-contract.ts b/orchestrator/tools/print-contract.ts new file mode 100644 index 0000000..8827723 --- /dev/null +++ b/orchestrator/tools/print-contract.ts @@ -0,0 +1,24 @@ +/** + * Build-time CLI: печатает контракт, СКОМПИЛИРОВАННЫЙ в оркестратор. + * + * Нужен, чтобы verify_versions_contract сравнивал versions.env не с текстом + * исходников (grep), а с фактическими константами, которые попадут в бинарь. + * + * bun run tools/print-contract.ts + */ + +import { + HY2XS_CONFIG_SCHEMA_VERSION, + HY2XS_RELEASE_LINE, + HY2XS_TARGET_ARCH, + HY2XS_TARGET_DEBIAN_VERSION +} from "../src/config/profile"; + +const lines = [ + `HY2XS_CONFIG_SCHEMA_VERSION=${HY2XS_CONFIG_SCHEMA_VERSION}`, + `HY2XS_RELEASE_LINE=${HY2XS_RELEASE_LINE}`, + `HY2XS_TARGET_OS_VERSION=${HY2XS_TARGET_DEBIAN_VERSION}`, + `HY2XS_TARGET_ARCH=${HY2XS_TARGET_ARCH}` +]; + +process.stdout.write(`${lines.join("\n")}\n`); diff --git a/tools/build/build.sh b/tools/build/build.sh index c9910eb..2ae3bf1 100755 --- a/tools/build/build.sh +++ b/tools/build/build.sh @@ -6,6 +6,11 @@ BUILD_DIR="$ROOT_DIR/tools/build" # shellcheck source=tools/build/lib/common.sh . "$BUILD_DIR/lib/common.sh" +# versions.sh подключается до deps.sh: deps.sh больше не носит собственных +# значений по умолчанию и берёт весь toolchain-контракт отсюда. +# shellcheck source=tools/build/lib/versions.sh +. "$BUILD_DIR/lib/versions.sh" +load_versions_contract # shellcheck source=tools/build/lib/deps.sh . "$BUILD_DIR/lib/deps.sh" # shellcheck source=tools/build/lib/verify.sh @@ -20,15 +25,19 @@ BUILD_DIR="$ROOT_DIR/tools/build" main() { cd "$ROOT_DIR" - require_linux_debian13_amd64 + require_build_host_platform require_repo_layout verify_source_tree_policy ensure_build_dependencies ensure_toolchain - PACKAGE_VERSION="${PACKAGE_VERSION:-1.0.0}" + # Версия пакета приходит из versions.env: собственного значения по + # умолчанию у сборки больше нет. + PACKAGE_VERSION="${PACKAGE_VERSION:-$HY2XS_VERSION}" BUILD_ID="${BUILD_ID:-$(date -u +%Y%m%dT%H%M%SZ)}" + verify_versions_contract + log_step "Checking orchestrator contracts" run_orchestrator_tests diff --git a/tools/build/lib/deps.sh b/tools/build/lib/deps.sh index 59ec613..53b4015 100644 --- a/tools/build/lib/deps.sh +++ b/tools/build/lib/deps.sh @@ -1,15 +1,17 @@ #!/usr/bin/env bash set -euo pipefail -GO_REQUIRED="${GO_REQUIRED:-1.21.13}" -BUN_REQUIRED="${BUN_REQUIRED:-1.3.13}" -NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}" -PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}" +# Версии и контрольные суммы toolchain приходят из versions.env через +# load_versions_contract (tools/build/lib/versions.sh). Собственных значений +# по умолчанию здесь нет намеренно: второй источник истины неизбежно +# разъезжается с первым. +[ -n "${GO_REQUIRED:-}" ] || fail "deps.sh: GO_REQUIRED is not set; load_versions_contract must run first" +[ -n "${BUN_REQUIRED:-}" ] || fail "deps.sh: BUN_REQUIRED is not set; load_versions_contract must run first" +[ -n "${NODE_REQUIRED:-}" ] || fail "deps.sh: NODE_REQUIRED is not set; load_versions_contract must run first" +[ -n "${PNPM_REQUIRED:-}" ] || fail "deps.sh: PNPM_REQUIRED is not set; load_versions_contract must run first" + TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}" VERIFY_TOOLCHAIN_CHECKSUMS="${VERIFY_TOOLCHAIN_CHECKSUMS:-true}" -GO_ARCHIVE_SHA256="${GO_ARCHIVE_SHA256:-}" -NODE_ARCHIVE_SHA256="${NODE_ARCHIVE_SHA256:-}" -BUN_ARCHIVE_SHA256="${BUN_ARCHIVE_SHA256:-}" BUN_FLAVOR="${BUN_FLAVOR:-auto}" verify_archive_sha256() { @@ -21,24 +23,36 @@ verify_archive_sha256() { return 0 fi - [ -n "$expected" ] || fail "missing expected SHA256 for $label (set ${label}_SHA256 env)" + [ -n "$expected" ] || fail "missing expected SHA256 for $label (declare it in versions.env)" local actual actual="$(sha256sum "$archive" | awk '{print $1}')" [ "$actual" = "$expected" ] || fail "$label sha256 mismatch: expected $expected, got $actual" } -require_linux_debian13_amd64() { - [ "$(uname -s)" = "Linux" ] || fail "production builder supports only Linux Debian 13 amd64" +require_build_host_platform() { + local want_os="$HY2XS_BUILD_OS" + local want_version="$HY2XS_BUILD_OS_VERSION" + local want_arch="$HY2XS_BUILD_ARCH" + + [ "$(uname -s)" = "Linux" ] \ + || fail "production builder supports only Linux ${want_os} ${want_version} ${want_arch}" local arch arch="$(uname -m)" - [ "$arch" = "x86_64" ] || [ "$arch" = "amd64" ] || fail "production builder supports only amd64, got: $arch" + if [ "$want_arch" = "amd64" ]; then + [ "$arch" = "x86_64" ] || [ "$arch" = "amd64" ] \ + || fail "production builder supports only ${want_arch}, got: $arch" + else + [ "$arch" = "$want_arch" ] || fail "production builder supports only ${want_arch}, got: $arch" + fi [ -f /etc/os-release ] || fail "missing /etc/os-release" # shellcheck disable=SC1091 . /etc/os-release - [ "${ID:-}" = "debian" ] || fail "production builder supports only Debian 13, got: ${ID:-unknown}" - [ "${VERSION_ID:-}" = "13" ] || fail "production builder supports only Debian 13, got version: ${VERSION_ID:-unknown}" + [ "${ID:-}" = "$want_os" ] \ + || fail "production builder supports only ${want_os} ${want_version}, got: ${ID:-unknown}" + [ "${VERSION_ID:-}" = "$want_version" ] \ + || fail "production builder supports only ${want_os} ${want_version}, got version: ${VERSION_ID:-unknown}" } apt_install_missing() { @@ -190,7 +204,9 @@ ensure_bun() { local archive="$TOOLCHAIN_DIR/downloads/${BUN_ARTIFACT}-${BUN_REQUIRED}.zip" download_file "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_REQUIRED}/${BUN_ARTIFACT}.zip" "$archive" - verify_archive_sha256 "$archive" "$BUN_ARCHIVE_SHA256" "BUN_ARCHIVE" + # Ожидаемый digest выбирается ПОСЛЕ select_bun_artifact: x64 и + # x64-baseline — разные архивы с разными суммами. + verify_archive_sha256 "$archive" "$(expected_bun_sha256)" "BUN_ARCHIVE(${BUN_ARTIFACT})" rm -rf "$TOOLCHAIN_DIR/bun-tmp" "$TOOLCHAIN_DIR/bun" mkdir -p "$TOOLCHAIN_DIR/bun-tmp" diff --git a/tools/build/lib/package.sh b/tools/build/lib/package.sh index 8248592..2c51f88 100644 --- a/tools/build/lib/package.sh +++ b/tools/build/lib/package.sh @@ -24,6 +24,8 @@ prepare_stage() { 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" } run_orchestrator_tests() { @@ -95,12 +97,19 @@ bundle_ui() { "$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=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" mod download - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" build -trimpath -ldflags "-s -w" -o "../$ADMIN_BUILD_DIR/hy2xs-admin" . + 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/h_ui_db.sql" ]; then mkdir -p "$STAGE_DIR/ui/hy2xs-admin/docs/sql" @@ -131,13 +140,15 @@ 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=debian13\n' - printf 'build_host_arch=amd64\n' - printf 'target_os=linux\n' - printf 'target_arch=amd64\n' - printf 'target_distro=debian\n' - printf 'target_version=13\n' + 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' @@ -152,6 +163,7 @@ write_metadata() { 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}" diff --git a/tools/build/lib/verify.sh b/tools/build/lib/verify.sh index 3973924..c5a01dd 100644 --- a/tools/build/lib/verify.sh +++ b/tools/build/lib/verify.sh @@ -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 diff --git a/tools/build/lib/versions.sh b/tools/build/lib/versions.sh new file mode 100644 index 0000000..f6cbec5 --- /dev/null +++ b/tools/build/lib/versions.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Загрузка и проверка контракта versions.env. +# +# versions.env отвечает на один вопрос: «какую версию продукта, на какой +# платформе и каким toolchain мы воспроизводимо собираем». Всё остальное +# (прикладные зависимости, конкретная версия Hysteria) живёт в собственных +# lock-механизмах. +# +# Подход — проверка, а не генерация. Генерируемые исходники ломают чистый +# чекаут: `bun test`, `tsc` и `go test` должны работать до запуска сборки. +# Поэтому profile.ts / package.json / hy2xs.env остаются обычными файлами, +# а verify_versions_contract роняет сборку при рассинхроне. + +VERSIONS_ENV_FILE="${VERSIONS_ENV_FILE:-$ROOT_DIR/versions.env}" + +REQUIRED_CONTRACT_VARS=( + HY2XS_VERSION + HY2XS_RELEASE_LINE + HY2XS_CONFIG_SCHEMA_VERSION + HY2XS_BUILD_OS + HY2XS_BUILD_OS_VERSION + HY2XS_BUILD_ARCH + HY2XS_TARGET_OS + HY2XS_TARGET_OS_VERSION + HY2XS_TARGET_ARCH + GO_VERSION + GO_LINUX_AMD64_SHA256 + BUN_VERSION + BUN_LINUX_X64_SHA256 + BUN_LINUX_X64_BASELINE_SHA256 + NODE_VERSION + NODE_LINUX_X64_SHA256 + PNPM_VERSION + HYSTERIA_CHANNEL +) + +validate_sha256_value() { + local name="$1" + local value="$2" + printf '%s' "$value" | grep -Eq '^[a-f0-9]{64}$' \ + || fail "versions.env: $name must be a 64-char lowercase hex SHA256, got: $value" +} + +load_versions_contract() { + [ -f "$VERSIONS_ENV_FILE" ] || fail "missing versions contract: $VERSIONS_ENV_FILE" + + # HYSTERIA_CHANNEL допускает override из окружения: офлайн-сборка из + # закоммиченного lock не должна требовать правки versions.env. + local preset_channel="${HYSTERIA_CHANNEL:-}" + + # shellcheck disable=SC1090 + . "$VERSIONS_ENV_FILE" + + if [ -n "$preset_channel" ]; then + HYSTERIA_CHANNEL="$preset_channel" + fi + + local var value + for var in "${REQUIRED_CONTRACT_VARS[@]}"; do + value="${!var:-}" + [ -n "$value" ] || fail "versions.env: missing required $var" + done + + validate_sha256_value GO_LINUX_AMD64_SHA256 "$GO_LINUX_AMD64_SHA256" + validate_sha256_value BUN_LINUX_X64_SHA256 "$BUN_LINUX_X64_SHA256" + validate_sha256_value BUN_LINUX_X64_BASELINE_SHA256 "$BUN_LINUX_X64_BASELINE_SHA256" + validate_sha256_value NODE_LINUX_X64_SHA256 "$NODE_LINUX_X64_SHA256" + + printf '%s' "$HY2XS_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' \ + || fail "versions.env: HY2XS_VERSION must be X.Y.Z, got: $HY2XS_VERSION" + printf '%s' "$HY2XS_RELEASE_LINE" | grep -Eq '^[0-9]+$' \ + || fail "versions.env: HY2XS_RELEASE_LINE must be an integer, got: $HY2XS_RELEASE_LINE" + printf '%s' "$HY2XS_CONFIG_SCHEMA_VERSION" | grep -Eq '^[0-9]+$' \ + || fail "versions.env: HY2XS_CONFIG_SCHEMA_VERSION must be an integer" + + # HY2XS_TARGET_OS — это дистрибутив (debian), а GOOS/`target_os` в metadata + # оперируют ядром. Производная величина, а не второй источник истины. + case "$HY2XS_TARGET_OS" in + debian) HY2XS_TARGET_GOOS=linux ;; + *) fail "versions.env: unsupported HY2XS_TARGET_OS: $HY2XS_TARGET_OS" ;; + esac + export HY2XS_TARGET_GOOS + + # Мост к deps.sh: там больше нет собственных значений по умолчанию. + GO_REQUIRED="$GO_VERSION" + BUN_REQUIRED="$BUN_VERSION" + NODE_REQUIRED="$NODE_VERSION" + PNPM_REQUIRED="$PNPM_VERSION" + GO_ARCHIVE_SHA256="$GO_LINUX_AMD64_SHA256" + NODE_ARCHIVE_SHA256="$NODE_LINUX_X64_SHA256" + + export HY2XS_VERSION HY2XS_RELEASE_LINE HY2XS_CONFIG_SCHEMA_VERSION + export HY2XS_BUILD_OS HY2XS_BUILD_OS_VERSION HY2XS_BUILD_ARCH + export HY2XS_TARGET_OS HY2XS_TARGET_OS_VERSION HY2XS_TARGET_ARCH + export GO_VERSION GO_LINUX_AMD64_SHA256 + export BUN_VERSION BUN_LINUX_X64_SHA256 BUN_LINUX_X64_BASELINE_SHA256 + export NODE_VERSION NODE_LINUX_X64_SHA256 PNPM_VERSION HYSTERIA_CHANNEL + export GO_REQUIRED BUN_REQUIRED NODE_REQUIRED PNPM_REQUIRED + export GO_ARCHIVE_SHA256 NODE_ARCHIVE_SHA256 +} + +# Ожидаемая контрольная сумма Bun зависит от выбранного артефакта: на CPU без +# AVX2 скачивается bun-linux-x64-baseline, и одного digest'а не хватает. +expected_bun_sha256() { + case "${BUN_ARTIFACT:-}" in + bun-linux-x64) + printf '%s' "$BUN_LINUX_X64_SHA256" + ;; + bun-linux-x64-baseline) + printf '%s' "$BUN_LINUX_X64_BASELINE_SHA256" + ;; + *) + fail "expected_bun_sha256: unknown BUN_ARTIFACT: ${BUN_ARTIFACT:-}" + ;; + esac +} + +read_json_string_field() { + local file="$1" + local field="$2" + grep -Eo "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$file" \ + | head -n1 \ + | sed -E "s/.*:[[:space:]]*\"([^\"]*)\"/\1/" +} + +read_env_field() { + local file="$1" + local key="$2" + grep -E "^${key}=" "$file" | head -n1 | cut -d= -f2- +} + +expect_equal() { + local label="$1" + local actual="$2" + local expected="$3" + [ "$actual" = "$expected" ] \ + || fail "versions contract mismatch: $label is '$actual', versions.env declares '$expected'" +} + +# Сравнение идёт с константами, СКОМПИЛИРОВАННЫМИ в оркестратор, а не с +# текстом исходников: grep по файлу не доказывает, что в бинарь попало то же. +verify_orchestrator_contract() { + local output + output="$(cd orchestrator && "$BUN_BIN" run tools/print-contract.ts)" \ + || fail "versions contract: orchestrator contract dump failed" + + local line key value + while IFS= read -r line; do + [ -n "$line" ] || continue + key="${line%%=*}" + value="${line#*=}" + case "$key" in + HY2XS_CONFIG_SCHEMA_VERSION) + expect_equal "orchestrator HY2XS_CONFIG_SCHEMA_VERSION" "$value" "$HY2XS_CONFIG_SCHEMA_VERSION" + ;; + HY2XS_RELEASE_LINE) + expect_equal "orchestrator HY2XS_RELEASE_LINE" "$value" "$HY2XS_RELEASE_LINE" + ;; + HY2XS_TARGET_OS_VERSION) + expect_equal "orchestrator target OS version" "$value" "$HY2XS_TARGET_OS_VERSION" + ;; + HY2XS_TARGET_ARCH) + expect_equal "orchestrator target arch" "$value" "$HY2XS_TARGET_ARCH" + ;; + *) + fail "versions contract: unexpected orchestrator contract line: $line" + ;; + esac + done <<<"$output" +} + +verify_go_toolchain_contract() { + local declared_go_line contract_minor go_minor + declared_go_line="$(grep -E '^go[[:space:]]+[0-9]' apps/go.mod | head -n1 | awk '{print $2}')" + [ -n "$declared_go_line" ] || fail "versions contract: apps/go.mod has no 'go' directive" + + contract_minor="$(printf '%s' "$GO_VERSION" | cut -d. -f2)" + go_minor="$(printf '%s' "$declared_go_line" | cut -d. -f2)" + + [ "$(printf '%s' "$GO_VERSION" | cut -d. -f1)" = "$(printf '%s' "$declared_go_line" | cut -d. -f1)" ] \ + || fail "versions contract: apps/go.mod major version $declared_go_line is incompatible with GO_VERSION $GO_VERSION" + [ "$go_minor" -le "$contract_minor" ] \ + || fail "versions contract: apps/go.mod requires go $declared_go_line, but GO_VERSION is $GO_VERSION" +} + +verify_versions_contract() { + log_step "Verifying versions contract (versions.env)" + + expect_equal "PACKAGE_VERSION" "${PACKAGE_VERSION:-}" "$HY2XS_VERSION" + + expect_equal \ + "orchestrator/package.json packageManager" \ + "$(read_json_string_field orchestrator/package.json packageManager)" \ + "bun@$BUN_VERSION" + + expect_equal \ + "apps/frontend/package.json packageManager" \ + "$(read_json_string_field apps/frontend/package.json packageManager)" \ + "pnpm@$PNPM_VERSION" + + expect_equal \ + "package/config/hy2xs.env HY2XS_CONFIG_SCHEMA_VERSION" \ + "$(read_env_field package/config/hy2xs.env HY2XS_CONFIG_SCHEMA_VERSION)" \ + "$HY2XS_CONFIG_SCHEMA_VERSION" + + verify_orchestrator_contract + verify_go_toolchain_contract + + expect_equal "build host OS" "$HY2XS_BUILD_OS" "debian" + expect_equal "build host arch" "$HY2XS_BUILD_ARCH" "amd64" + expect_equal "target OS" "$HY2XS_TARGET_OS" "debian" + expect_equal "target arch" "$HY2XS_TARGET_ARCH" "amd64" + + log_info "versions contract is consistent: HY2XS $HY2XS_VERSION (release line $HY2XS_RELEASE_LINE, config schema $HY2XS_CONFIG_SCHEMA_VERSION)" +} + +# Проверяется уже собранный бинарь: это единственное доказательство, что +# ldflags действительно доехали до артефакта. +verify_admin_version_contract() { + local binary="$1" + local reported expected + + [ -x "$binary" ] || fail "versions contract: admin binary not found: $binary" + + expected="v$HY2XS_VERSION" + reported="$("$binary" version 2>/dev/null | grep -Eo 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)" + + [ -n "$reported" ] || fail "versions contract: admin binary did not report a version" + expect_equal "hy2xs-admin reported version" "$reported" "$expected" +} diff --git a/versions.env b/versions.env new file mode 100644 index 0000000..f571b85 --- /dev/null +++ b/versions.env @@ -0,0 +1,62 @@ +# HY2XS: единственный источник истины для контракта +# «продукт / платформа / toolchain». +# +# Что здесь ЕСТЬ: +# версия продукта, линия релиза, схема конфигурации, целевая платформа, +# версии и контрольные суммы build-toolchain, политика выбора Hysteria. +# +# Чего здесь НЕТ и быть не должно: +# 1. Прикладные зависимости (Vue, Gin, GORM, npm/Go модули). У них уже есть +# канонические lock-механизмы: apps/frontend/pnpm-lock.yaml, +# orchestrator/bun.lock, apps/go.sum. Второй слой неизбежно разъедется +# с настоящим графом зависимостей. +# 2. Конкретная версия Hysteria. Здесь живёт только ПОЛИТИКА выбора +# (HYSTERIA_CHANNEL). Результат резолва конкретной сборки замораживается +# в tools/build/hysteria-lock.env. Пин версии здесь вернул бы ручное +# обновление, от которого мы ушли. +# +# Файл читается сборкой (tools/build/lib/versions.sh) и проверяется шагом +# verify_versions_contract: рассинхрон с package.json / profile.ts / +# hy2xs.env / go.mod роняет сборку до создания tarball. +# +# Синтаксис — POSIX shell: файл должен безопасно подключаться через `.` +# без jq, Node и Python. + +# ------------------------------------------------------------- продукт --- + +HY2XS_VERSION=1.0.0 +HY2XS_RELEASE_LINE=1 +HY2XS_CONFIG_SCHEMA_VERSION=2 + +# ------------------------------------------------------------ платформа --- + +HY2XS_BUILD_OS=debian +HY2XS_BUILD_OS_VERSION=13 +HY2XS_BUILD_ARCH=amd64 + +HY2XS_TARGET_OS=debian +HY2XS_TARGET_OS_VERSION=13 +HY2XS_TARGET_ARCH=amd64 + +# ------------------------------------------------------------ toolchain --- + +GO_VERSION=1.21.13 +GO_LINUX_AMD64_SHA256=502fc16d5910562461e6a6631fb6377de2322aad7304bf2bcd23500ba9dab4a7 + +# Bun выбирает артефакт по наличию AVX2, поэтому одной контрольной суммы +# архитектурно недостаточно: обе должны быть зафиксированы заранее. +BUN_VERSION=1.3.13 +BUN_LINUX_X64_SHA256=79c0771fa8b92c33aae41e15a0e0d307ea99d0e2f00317c71c6c53237a78e25a +BUN_LINUX_X64_BASELINE_SHA256=9d8a24292a7068090205daac0a5a223f5f69736f5287e37bf88d3b4031edc750 + +NODE_VERSION=20.19.0 +NODE_LINUX_X64_SHA256=b4e336584d62abefad31baecff7af167268be9bb7dd11f1297112e6eed3ca0d5 + +# pnpm ставится через corepack, отдельного архива с контрольной суммой нет. +PNPM_VERSION=9.15.9 + +# ------------------------------------------------ политика выбора Hysteria --- + +# stable — резолвить последний стабильный upstream-релиз на build-машине; +# pinned — использовать закоммиченный tools/build/hysteria-lock.env офлайн. +HYSTERIA_CHANNEL=stable