#!/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" }