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
+11 -2
View File
@@ -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
+30 -14
View File
@@ -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"
+20 -8
View File
@@ -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}"
+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
+232
View File
@@ -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:-<unset>}"
;;
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"
}