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:
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user