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:
@@ -30,5 +30,15 @@ var (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
TokenType = "Bearer"
|
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"
|
||||||
|
|||||||
@@ -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`);
|
||||||
+11
-2
@@ -6,6 +6,11 @@ BUILD_DIR="$ROOT_DIR/tools/build"
|
|||||||
|
|
||||||
# shellcheck source=tools/build/lib/common.sh
|
# shellcheck source=tools/build/lib/common.sh
|
||||||
. "$BUILD_DIR/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
|
# shellcheck source=tools/build/lib/deps.sh
|
||||||
. "$BUILD_DIR/lib/deps.sh"
|
. "$BUILD_DIR/lib/deps.sh"
|
||||||
# shellcheck source=tools/build/lib/verify.sh
|
# shellcheck source=tools/build/lib/verify.sh
|
||||||
@@ -20,15 +25,19 @@ BUILD_DIR="$ROOT_DIR/tools/build"
|
|||||||
main() {
|
main() {
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
require_linux_debian13_amd64
|
require_build_host_platform
|
||||||
require_repo_layout
|
require_repo_layout
|
||||||
verify_source_tree_policy
|
verify_source_tree_policy
|
||||||
ensure_build_dependencies
|
ensure_build_dependencies
|
||||||
ensure_toolchain
|
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)}"
|
BUILD_ID="${BUILD_ID:-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||||
|
|
||||||
|
verify_versions_contract
|
||||||
|
|
||||||
log_step "Checking orchestrator contracts"
|
log_step "Checking orchestrator contracts"
|
||||||
run_orchestrator_tests
|
run_orchestrator_tests
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -1,15 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
GO_REQUIRED="${GO_REQUIRED:-1.21.13}"
|
# Версии и контрольные суммы toolchain приходят из versions.env через
|
||||||
BUN_REQUIRED="${BUN_REQUIRED:-1.3.13}"
|
# load_versions_contract (tools/build/lib/versions.sh). Собственных значений
|
||||||
NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}"
|
# по умолчанию здесь нет намеренно: второй источник истины неизбежно
|
||||||
PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}"
|
# разъезжается с первым.
|
||||||
|
[ -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}"
|
TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}"
|
||||||
VERIFY_TOOLCHAIN_CHECKSUMS="${VERIFY_TOOLCHAIN_CHECKSUMS:-true}"
|
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}"
|
BUN_FLAVOR="${BUN_FLAVOR:-auto}"
|
||||||
|
|
||||||
verify_archive_sha256() {
|
verify_archive_sha256() {
|
||||||
@@ -21,24 +23,36 @@ verify_archive_sha256() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
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
|
local actual
|
||||||
actual="$(sha256sum "$archive" | awk '{print $1}')"
|
actual="$(sha256sum "$archive" | awk '{print $1}')"
|
||||||
[ "$actual" = "$expected" ] || fail "$label sha256 mismatch: expected $expected, got $actual"
|
[ "$actual" = "$expected" ] || fail "$label sha256 mismatch: expected $expected, got $actual"
|
||||||
}
|
}
|
||||||
|
|
||||||
require_linux_debian13_amd64() {
|
require_build_host_platform() {
|
||||||
[ "$(uname -s)" = "Linux" ] || fail "production builder supports only Linux Debian 13 amd64"
|
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
|
local arch
|
||||||
arch="$(uname -m)"
|
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"
|
[ -f /etc/os-release ] || fail "missing /etc/os-release"
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
[ "${ID:-}" = "debian" ] || fail "production builder supports only Debian 13, got: ${ID:-unknown}"
|
[ "${ID:-}" = "$want_os" ] \
|
||||||
[ "${VERSION_ID:-}" = "13" ] || fail "production builder supports only Debian 13, got version: ${VERSION_ID:-unknown}"
|
|| 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() {
|
apt_install_missing() {
|
||||||
@@ -190,7 +204,9 @@ ensure_bun() {
|
|||||||
|
|
||||||
local archive="$TOOLCHAIN_DIR/downloads/${BUN_ARTIFACT}-${BUN_REQUIRED}.zip"
|
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"
|
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"
|
rm -rf "$TOOLCHAIN_DIR/bun-tmp" "$TOOLCHAIN_DIR/bun"
|
||||||
mkdir -p "$TOOLCHAIN_DIR/bun-tmp"
|
mkdir -p "$TOOLCHAIN_DIR/bun-tmp"
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ prepare_stage() {
|
|||||||
|
|
||||||
printf '%s\n' "$version" >"$STAGE_DIR/metadata/package.version"
|
printf '%s\n' "$version" >"$STAGE_DIR/metadata/package.version"
|
||||||
printf '%s\n' "$build_id" >"$STAGE_DIR/metadata/package.build_id"
|
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() {
|
run_orchestrator_tests() {
|
||||||
@@ -95,12 +97,19 @@ bundle_ui() {
|
|||||||
"$PNPM_BIN" run build:prod
|
"$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"
|
cd "$ui_src"
|
||||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local "$GO_BIN" mod download
|
GOOS="$HY2XS_TARGET_GOOS" GOARCH="$HY2XS_TARGET_ARCH" 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" 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"
|
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
|
if [ -f "$ui_src/docs/sql/h_ui_db.sql" ]; then
|
||||||
mkdir -p "$STAGE_DIR/ui/hy2xs-admin/docs/sql"
|
mkdir -p "$STAGE_DIR/ui/hy2xs-admin/docs/sql"
|
||||||
@@ -131,13 +140,15 @@ write_metadata() {
|
|||||||
printf 'name=HY2XS\n'
|
printf 'name=HY2XS\n'
|
||||||
printf 'license=AGPL-3.0-only\n'
|
printf 'license=AGPL-3.0-only\n'
|
||||||
printf 'version=%s\n' "$version"
|
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_id=%s\n' "$build_id"
|
||||||
printf 'build_host_os=debian13\n'
|
printf 'build_host_os=%s%s\n' "$HY2XS_BUILD_OS" "$HY2XS_BUILD_OS_VERSION"
|
||||||
printf 'build_host_arch=amd64\n'
|
printf 'build_host_arch=%s\n' "$HY2XS_BUILD_ARCH"
|
||||||
printf 'target_os=linux\n'
|
printf 'target_os=%s\n' "$HY2XS_TARGET_GOOS"
|
||||||
printf 'target_arch=amd64\n'
|
printf 'target_arch=%s\n' "$HY2XS_TARGET_ARCH"
|
||||||
printf 'target_distro=debian\n'
|
printf 'target_distro=%s\n' "$HY2XS_TARGET_OS"
|
||||||
printf 'target_version=13\n'
|
printf 'target_version=%s\n' "$HY2XS_TARGET_OS_VERSION"
|
||||||
printf 'source_git_commit=%s\n' "$source_git_commit"
|
printf 'source_git_commit=%s\n' "$source_git_commit"
|
||||||
printf 'dirty_tree=%s\n' "$dirty_tree"
|
printf 'dirty_tree=%s\n' "$dirty_tree"
|
||||||
printf 'build_profile=production\n'
|
printf 'build_profile=production\n'
|
||||||
@@ -152,6 +163,7 @@ write_metadata() {
|
|||||||
printf 'hysteria_version=%s\n' "$HYSTERIA_VERSION"
|
printf 'hysteria_version=%s\n' "$HYSTERIA_VERSION"
|
||||||
printf 'hysteria_artifact_url=%s\n' "$HYSTERIA_ARTIFACT_URL"
|
printf 'hysteria_artifact_url=%s\n' "$HYSTERIA_ARTIFACT_URL"
|
||||||
printf 'hysteria_artifact_sha256=%s\n' "$HYSTERIA_ARTIFACT_SHA256"
|
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_channel=%s\n' "${HYSTERIA_CHANNEL:-unknown}"
|
||||||
printf 'hysteria_resolution=%s\n' "${HYSTERIA_RESOLUTION:-unknown}"
|
printf 'hysteria_resolution=%s\n' "${HYSTERIA_RESOLUTION:-unknown}"
|
||||||
printf 'hysteria_resolved_at=%s\n' "${HYSTERIA_RESOLVED_AT:-unknown}"
|
printf 'hysteria_resolved_at=%s\n' "${HYSTERIA_RESOLVED_AT:-unknown}"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
require_repo_layout() {
|
require_repo_layout() {
|
||||||
[ -f "LICENSE" ] || fail "missing LICENSE"
|
[ -f "LICENSE" ] || fail "missing LICENSE"
|
||||||
|
[ -f "versions.env" ] || fail "missing versions.env (product/platform/toolchain contract)"
|
||||||
[ -d "orchestrator/src" ] || fail "missing orchestrator/src"
|
[ -d "orchestrator/src" ] || fail "missing orchestrator/src"
|
||||||
[ -f "orchestrator/package.json" ] || fail "missing orchestrator/package.json"
|
[ -f "orchestrator/package.json" ] || fail "missing orchestrator/package.json"
|
||||||
[ -f "orchestrator/bun.lock" ] || fail "missing orchestrator/bun.lock"
|
[ -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.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.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/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
|
local license_text
|
||||||
license_text="$(tar -xOzf "$archive" hy2xs-install/LICENSE)"
|
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 '^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"
|
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
|
if declare -F run_fix20_acceptance_subset >/dev/null 2>&1; then
|
||||||
run_fix20_acceptance_subset "$tmp/hy2xs-install"
|
run_fix20_acceptance_subset "$tmp/hy2xs-install"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user