Files
HY2XS_flamy/tools/build
founder c0a43ae915 fix(admin): закрыть обещания панели, которые продукт не выполнял
Девятый проход, по итогам приёмки v1.0.0-rc1 на живом Debian 13. Общая тема:
интерфейс обещал оператору то, что продукт умел, но до чего не доходило
управление.

Секрет пира. Подпись под полем предлагала оставить его пустым, сервер умел его
сгенерировать, и генерация была недостижима: в go-playground/validator тег
omitempty НЕ пропускает правило, если поле объявлено указателем и указатель не
nil — hasValue считает указатель на пустую строку «значением». Правило min=6
применялось к пустой строке и отказывало. Ловушка закрыта общим шагом
нормализации DTO, а не тегом на одном поле: та же ловушка ломала фильтр списка
пиров, где очищенный крестиком el-input отправляет `?name=`. Граница проходит по
каждому полю отдельно — у remark пустая строка означает «убрать пометку», у
disabled ноль означает «включён».

Отказы. Любая ошибка любого поля превращалась в слово `invalid`, а слой vo
определял код ответа СРАВНЕНИЕМ текста сообщения — тот же антипаттерн, который
запрещён панели, только на сервере. Ответ несёт errors[{code, field, message,
params}]; панель выбирает фразу по коду и подставляет причины под поля.

Сессия. Ветка «войдите заново» была недостижима дважды: сервер отвечает HTTP 200
на любой отказ, поэтому обработчик ошибок axios не вызывался, а условие в нём
проверяло code === "A0230" и поле msg, которых в этом API никогда не было.
Истёкший токен вдобавок уезжал с кодом системной ошибки.

Иконки. Контракт currentColor был объявлен в двух местах и не действовал: восемь
ассетов несли литеральный fill="#000000" на <path>, а атрибут представления
перебивает унаследованное CSS-свойство. Под это попадали все семь иконок
бокового меню на фоне #181818.

Имя пира. Два правила на одном поле противоречили друг другу (min=1 против
6-32), а копия набора символов в слое контроллеров несла неэкранированный дефис
и впускала `, - . / : ; <` — через панель проходило имя peer/name, которое
импорт того же пира отклонял. Набор символов ЛОГИНА сознательно не сужен и
закреплён тестом: он приходит из HY2XS_ADMIN_USER и оркестратором не
ограничивается.

Добавлены подпись «Разработано во Flamy» с адресом, принадлежащим приложению, и
контрактные тесты панели как обязательный шаг сборки. Их исполняет Bun, а не
vitest: jsdom не вычисляет currentColor и визуальной корректности не доказал бы,
зато vitest привёл бы в граф pnpm audit сотню транзитивных зависимостей.

docs/ разложена по слоям, 11-testing-and-acceptance.md (117 КБ) разбит на пять
частей, добавлен docs/acceptance/ с отчётом о прогоне rc1 и перечнем дефектов.
Обход документации в приёмке стал рекурсивным: плоский docs/*.md после
разнесения по каталогам совпадал бы ровно с одним файлом.
2026-09-01 07:27:15 +05:00
..

HY2XS production builder

Этот каталог содержит production builder для HY2XS.

Builder собирает один переносимый install-archive:

dist/hy2xs-install-<version>.tar.gz

Этот архив переносится на production server, распаковывается и устанавливается через install.sh.

На production server не должно быть сборки из исходников: без go build, без bun install, без pnpm install, без frontend build и без TypeScript transpilation.

Что где лежит

Основные части проекта:

  • versions.env — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.
  • tools/build/build.sh — главный entrypoint сборки.
  • tools/build/lib/versions.sh — загрузка versions.env и verify_versions_contract.
  • tools/build/lib/deps.sh — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.
  • tools/build/lib/package.sh — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.
  • tools/build/lib/verify.sh — проверка структуры репозитория и итогового архива.
  • tools/build/lib/acceptance.sh — acceptance-проверки production-контракта.
  • tools/build/lib/hysteria.sh — разрешение upstream-версии Hysteria и compatibility gate.
  • tools/build/hysteria-lock.env — fallback-значения для офлайн-сборки (HYSTERIA_CHANNEL=pinned).
  • tools/test/e2e-hysteria.sh — end-to-end проверка с реальным клиентом Hysteria.
  • orchestrator — TypeScript/Bun install-only orchestrator.
  • apps — HY2XS admin: Go backend и Vue frontend.
  • package — skeleton будущего install package: install.sh, templates, systemd units, default config.
  • dist — итоговые архивы. Создаётся builder'ом, в git обычно не хранится.
  • .toolchain — локальный toolchain builder'а. Создаётся автоматически, в git не хранится.

Требования к build machine

Production build поддерживается только на:

  • Debian 13;
  • amd64 / x86_64;
  • root или пользователь с sudo;
  • доступ в интернет.

Нужен outbound HTTPS/DNS до:

  • Debian apt repositories;
  • go.dev;
  • github.com;
  • nodejs.org;
  • npm registry.

Windows и macOS можно использовать для редактирования исходников, но финальную production-сборку надо делать на Debian 13 amd64.

Что builder делает сам

При запуске builder:

  1. Загружает и валидирует versions.env.
  2. Проверяет, что host соответствует HY2XS_BUILD_* (по умолчанию Debian 13 amd64).
  3. Проверяет структуру репозитория.
  4. Устанавливает недостающие системные build dependencies через apt-get.
  5. Проверяет или скачивает локальные версии Go/Bun/Node.js/pnpm из контракта и сверяет каждый архив с контрольной суммой из versions.env.
  6. Выполняет verify_versions_contract: рассинхрон версий роняет сборку до создания tarball.
  7. Прогоняет тесты и типы оркестратора (bun test, tsc --noEmit).
  8. Разрешает upstream-версию Hysteria, берёт ожидаемый SHA-256 из upstream hashes.txt и сверяет с ним скачанный артефакт.
  9. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS.
  10. Копирует package skeleton.
  11. Собирает install-only orchestrator в standalone binary.
  12. Собирает frontend и backend HY2XS admin в Linux amd64 binary, проставляя версию админки через ldflags.
  13. Прогоняет go vet и go test для HY2XS admin (после сборки frontend, потому что go:embed all:dist требует готовых ассетов).
  14. Записывает metadata и checksums.
  15. Создаёт dist/hy2xs-install-<version>.tar.gz.
  16. Проверяет архив и прогоняет acceptance-проверки.

Контракт версий

Версии продукта, платформы и toolchain объявлены в корневом versions.env. Собственных значений по умолчанию у deps.sh больше нет: без загруженного контракта сборка падает сразу.

Подход — проверка, а не генерация. profile.ts, package/config/hy2xs.env и packageManager в обоих package.json остаются обычными файлами, чтобы bun test, tsc и go test работали из чистого чекаута до запуска сборки. verify_versions_contract сверяет их с контрактом и роняет сборку при расхождении.

Контракт оркестратора сверяется не grep'ом по исходникам, а выводом orchestrator/tools/print-contract.ts: это доказывает, что в бинарь попало то же значение.

Версия админки приезжает в бинарь через ldflags (-X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}') и проверяется запуском собранного hy2xs-admin version. Захардкоженной константы версии в Go-коде больше нет: она уже успела разъехаться с версией пакета.

Чего в versions.env нет намеренно: прикладных зависимостей (для них есть pnpm-lock.yaml, bun.lock, go.sum) и конкретной версии Hysteria (здесь только политика HYSTERIA_CHANNEL, результат резолва — в hysteria-lock.env).

Версия Hysteria: разрешение и compatibility gate

Builder не хранит версию Hysteria вручную. По умолчанию он определяет последнюю стабильную версию сам и замораживает её в пакете.

Правила разрешения:

  1. канонический upstream — HyNetworks/hysteria;
  2. принимаются только стабильные релизы, без draft и prerelease;
  3. тег должен иметь вид app/vX.Y.Z;
  4. берётся ровно один артефакт hysteria-linux-amd64 и ровно один hashes.txt;
  5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
  6. ожидаемый SHA-256 берётся из upstream hashes.txt, и скачанный бинарник сверяется с ним;
  7. разрешённые значения попадают в metadata пакета.

Почему hashes.txt, а не локальный пересчёт

Раньше SHA-256 считался от уже скачанного файла. Это защищает target от последующей подмены, но не доказывает, что builder скачал именно ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было.

Формат ассета:

6493dfff…f94  build/hysteria-linux-amd64
f24f63be…189  build/hysteria-linux-amd64-avx

Сопоставление идёт по базовому имени и строго на равенство: build/ — часть пути, а hysteria-linux-amd64-avx — другой артефакт, который не должен совпасть по префиксу. Разбор вынесен в parseUpstreamHashes (hysteriaRelease.ts) и покрыт юнит-тестами.

Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.

Сравнение версий числовое, поэтому v2.9.10 считается новее v2.9.2.

После разрешения обязателен compatibility gate:

скачать бинарник
      ↓
сверить SHA-256 и `hysteria version`
      ↓
отрендерить канонический конфиг HY2XS тем же кодом, что и на target
      ↓
запустить настоящий Hysteria с этим конфигом (gecko и salamander)
      ↓
только после этого собирать release package

При несовместимости сборка останавливается:

BUILD FAILED: unsupported Hysteria stable v2.13.0

Это осознанное решение: ошибка должна проявиться на build machine, а не на production-сервере.

Переменные:

Переменная По умолчанию Назначение
HYSTERIA_CHANNEL из versions.env (stable) stable — разрешить последнюю стабильную через upstream API; pinned — офлайн-сборка по hysteria-lock.env
HYSTERIA_VERSION_OVERRIDE пусто Закрепить конкретную версию vX.Y.Z
HYSTERIA_COMPAT_GATE true Compatibility gate; для release-сборок обязателен
HYSTERIA_VERIFY_UPSTREAM_HASHES true Сверять артефакт с upstream hashes.txt; отключение — только break-glass
HYSTERIA_WRITE_LOCK false Записать разрешённые значения обратно в hysteria-lock.env
HYSTERIA_GATE_PORT 34443 UDP-порт для временного запуска Hysteria в gate
HYSTERIA_GATE_STATS_PORT 34712 TCP-порт trafficStats в gate
GITHUB_TOKEN пусто Опционально: снимает anonymous rate limit GitHub API

Тесты, проверка типов и проверка зависимостей переменными не управляются: у них нет аварийного выхода. Готовый пакет объявляет об этом полями tests_gate=true и dependency_security_gate=true в metadata/package.env.

Обновить lock-файл под текущий upstream:

HYSTERIA_WRITE_LOCK=true ./tools/build/build.sh

Важное про Bun и старые CPU

Bun имеет два Linux x64 artifact'а:

  • bun-linux-x64 — обычный build;
  • bun-linux-x64-baseline — build для CPU без AVX2.

Если CPU не поддерживает AVX2, обычный Bun может падать с Illegal instruction.

Builder автоматически проверяет /proc/cpuinfo.

По умолчанию:

BUN_FLAVOR=auto

Логика:

  • если CPU поддерживает AVX2, используется bun-linux-x64;
  • если CPU не поддерживает AVX2, используется bun-linux-x64-baseline.

Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В versions.env зафиксированы обе:

BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>

Ожидаемый digest выбирается уже после select_bun_artifact().

Можно принудительно задать flavor:

BUN_FLAVOR=x64 ./tools/build/build.sh

или:

BUN_FLAVOR=x64-baseline ./tools/build/build.sh

Запуск

Из корня репозитория:

./tools/build/build.sh

Версия пакета берётся из versions.env, поэтому обычно нужен только build id:

BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh

Проверка результата

Проверка архива:

ls -lh dist/hy2xs-install-1.0.0.tar.gz
sha256sum dist/hy2xs-install-1.0.0.tar.gz | tee dist/hy2xs-install-1.0.0.tar.gz.sha256

Проверка обязательных файлов:

tar -tzf dist/hy2xs-install-1.0.0.tar.gz | grep -E '^(hy2xs-install/install.sh|hy2xs-install/orchestrator/hy2xs-orchestrator|hy2xs-install/ui/hy2xs-admin/hy2xs-admin|hy2xs-install/metadata/checksums.txt)$'

Проверка metadata:

tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env

Когда обновлять orchestrator/bun.lock

Не перегенерируйте orchestrator/bun.lock во время production-сборки.

Обновлять и коммитить orchestrator/bun.lock следует только когда:

  • изменился orchestrator/package.json;
  • изменился BUN_VERSION в versions.env;
  • зависимости оркестратора обновляются осознанно.

При смене BUN_VERSION нужно обновить и packageManager в orchestrator/package.json, и обе контрольные суммы Bun: иначе verify_versions_contract остановит сборку.

Production builder всегда выполняет:

bun install --frozen-lockfile

Если команда падает, исправьте и закоммитьте lockfile в системе контроля версий. Не убирайте --frozen-lockfile.

Дисциплина lockfile для frontend

Не перегенерируйте frontend lock data во время обычной production-сборки.

Правила управления пакетами frontend:

  • apps/frontend/package.json объявляет "packageManager": "pnpm@9.15.9";
  • builder использует закреплённый pnpm из PNPM_VERSION в versions.env, и verify_versions_contract сверяет эти два значения;
  • production-путь установки frontend всегда:
pnpm install --frozen-lockfile

Если frozen install падает, обновите зависимости осознанно в системе контроля версий и закоммитьте изменения lockfile. Не убирайте --frozen-lockfile из сборочного потока.

Полезные переменные

  • BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)
  • BUN_FLAVOR=auto|x64|x64-baseline
  • FRONTEND_NODE_OLD_SPACE_SIZE=2048 (default memory limit for frontend build step)
  • TOOLCHAIN_DIR=/custom/path/.toolchain
  • VERIFY_TOOLCHAIN_CHECKSUMS=true
  • VERSIONS_ENV_FILE=/custom/path/versions.env

PACKAGE_VERSION берётся из versions.env (HY2XS_VERSION); переопределять его вручную нужно только для отладочных сборок, и verify_versions_contract такую сборку отклонит.

Контрольные суммы toolchain больше не передаются через окружение: они объявлены в versions.env. Раньше воспроизводимая сборка в чистой Debian-среде требовала предварительного знания четырёх SHA-256 и не запускалась одной командой.

Политика памяти при сборке frontend

Builder задаёт безопасный лимит heap для Node.js внутри bundle_ui():

--max-old-space-size=2048

Способы переопределения:

  • изменить значение по умолчанию для этой политики:
FRONTEND_NODE_OLD_SPACE_SIZE=3072 ./tools/build/build.sh
  • или передать полный набор собственных Node-опций (если --max-old-space-size там уже задан, builder не добавит второй):
NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh

Диагностика

Проверить AVX2:

grep -qw avx2 <<<"$(grep -m1 '^flags' /proc/cpuinfo)" && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'

Проверить Bun:

.toolchain/bun/bin/bun --version
echo "bun_exit=$?"

Если есть Illegal instruction, удалите старый Bun и пересоберите:

rm -rf .toolchain/bun .toolchain/bun-tmp
rm -f .toolchain/downloads/bun-linux-x64-*.zip
rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip
./tools/build/build.sh

Проверить shell syntax:

bash -n tools/build/build.sh
bash -n tools/build/lib/common.sh
bash -n tools/build/lib/versions.sh
bash -n tools/build/lib/deps.sh
bash -n tools/build/lib/hysteria.sh
bash -n tools/build/lib/package.sh
bash -n tools/build/lib/verify.sh
bash -n tools/build/lib/acceptance.sh