docs: clean-install-only, versions.env и очистка предыдущего поколения

Новый docs/14-legacy-cleanup.md: как выглядит отказ установщика, полный
список маркеров чужой установки, что сохранить перед очисткой, работа
purge-v0.sh, ручная процедура и отдельно - случай незавершённой
установки текущего поколения, где нужен repair, а не очистка.

Обновлено под фактическое поведение:

- README и package/docs: установка описана как две фазы, PHASE 0 ничего
  не меняет; добавлен troubleshooting по отказу clean-host; версии
  toolchain больше не передаются через окружение;
- 02-build-layer: раздел про versions.env (что в нём есть и чего нет и
  почему), verify_versions_contract, проверка происхождения артефакта
  по upstream hashes.txt;
- 08-orchestrator-spec: двухфазный контракт, read-only guard,
  идентификация поколения в install-state, ownership-aware rollback,
  расширенная семантическая проверка конфига, структурная редакция;
- 04-admin-panel: таблица удалённых маршрутов и почему они удалены, а
  не оставлены заглушками; сужена формулировка гарантии санитайза;
- 11-testing: новые unit-наборы, полный список инвариантов конфига,
  раздел про одну реализацию URI вместо двух, сценарий проверки
  границы установки на живом сервере;
- 12-operations и 13-runbook: диагностика отказов по поколению,
  поведение diagnostics-бандла;
- tools/build/README: контракт версий, обе суммы Bun, hashes.txt.

CHANGELOG: раздел Unreleased с разбором каждого исправленного дефекта.
This commit is contained in:
2026-08-27 12:16:38 +05:00
parent 42db78c6a0
commit 3a4ce9c751
12 changed files with 1117 additions and 99 deletions
+100 -33
View File
@@ -16,7 +16,9 @@ dist/hy2xs-install-<version>.tar.gz
Основные части проекта:
- [`versions.env`](../../versions.env) — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.
- [`tools/build/build.sh`](build.sh) — главный entrypoint сборки.
- [`tools/build/lib/versions.sh`](lib/versions.sh) — загрузка `versions.env` и `verify_versions_contract`.
- [`tools/build/lib/deps.sh`](lib/deps.sh) — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.
- [`tools/build/lib/package.sh`](lib/package.sh) — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.
- [`tools/build/lib/verify.sh`](lib/verify.sh) — проверка структуры репозитория и итогового архива.
@@ -53,24 +55,49 @@ Windows и macOS можно использовать для редактиров
При запуске builder:
1. Проверяет, что host — Debian 13 amd64.
2. Проверяет структуру репозитория.
3. Устанавливает недостающие системные build dependencies через `apt-get`.
4. Проверяет или скачивает локальные версии:
- Go `1.21.13`;
- Bun `1.3.13`;
- Node.js `20.19.0`;
- pnpm `9.15.9`.
5. Прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`).
6. Разрешает upstream-версию Hysteria, скачивает артефакт и считает SHA-256.
7. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS.
8. Копирует package skeleton.
9. Собирает install-only orchestrator в standalone binary.
10. Собирает frontend и backend HY2XS admin в Linux amd64 binary.
11. Прогоняет `go vet` и `go test` для HY2XS admin (после сборки frontend, потому что `go:embed all:dist` требует готовых ассетов).
12. Записывает metadata и checksums.
13. Создаёт `dist/hy2xs-install-<version>.tar.gz`.
14. Проверяет архив и прогоняет acceptance-проверки.
1. Загружает и валидирует [`versions.env`](../../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`](../../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`](../../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-lock.env)).
## Версия Hysteria: разрешение и compatibility gate
@@ -81,11 +108,32 @@ Builder не хранит версию Hysteria вручную. По умолч
1. канонический upstream — `HyNetworks/hysteria`;
2. принимаются только стабильные релизы, без draft и prerelease;
3. тег должен иметь вид `app/vX.Y.Z`;
4. берётся ровно один артефакт `hysteria-linux-amd64`;
4. берётся ровно один артефакт `hysteria-linux-amd64` и ровно один `hashes.txt`;
5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
6. SHA-256 считается локально от скачанного файла;
6. ожидаемый SHA-256 берётся из upstream `hashes.txt`, и скачанный бинарник сверяется с ним;
7. разрешённые значения попадают в metadata пакета.
### Почему hashes.txt, а не локальный пересчёт
Раньше SHA-256 считался от уже скачанного файла. Это защищает target от
последующей подмены, но не доказывает, что builder скачал именно ожидаемый
upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было.
Формат ассета:
```text
6493dfff…f94 build/hysteria-linux-amd64
f24f63be…189 build/hysteria-linux-amd64-avx
```
Сопоставление идёт по базовому имени и строго на равенство: `build/` — часть
пути, а `hysteria-linux-amd64-avx` — другой артефакт, который не должен
совпасть по префиксу. Разбор вынесен в `parseUpstreamHashes`
([`hysteriaRelease.ts`](../../orchestrator/src/build/hysteriaRelease.ts)) и
покрыт юнит-тестами.
Источник ожидаемой суммы фиксируется в metadata как `hysteria_sha_source`.
Сравнение версий числовое, поэтому `v2.9.10` считается новее `v2.9.2`.
После разрешения обязателен compatibility gate:
@@ -114,9 +162,10 @@ BUILD FAILED: unsupported Hysteria stable v2.13.0
| Переменная | По умолчанию | Назначение |
| --- | --- | --- |
| `HYSTERIA_CHANNEL` | `stable` | `stable` — разрешить последнюю стабильную через upstream API; `pinned` — офлайн-сборка по `hysteria-lock.env` |
| `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 |
@@ -151,6 +200,16 @@ BUN_FLAVOR=auto
- если CPU поддерживает AVX2, используется `bun-linux-x64`;
- если CPU не поддерживает AVX2, используется `bun-linux-x64-baseline`.
Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В
[`versions.env`](../../versions.env) зафиксированы обе:
```bash
BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>
```
Ожидаемый digest выбирается уже **после** `select_bun_artifact()`.
Можно принудительно задать flavor:
```bash
@@ -171,12 +230,10 @@ BUN_FLAVOR=x64-baseline ./tools/build/build.sh
./tools/build/build.sh
```
С явной версией и build id:
Версия пакета берётся из `versions.env`, поэтому обычно нужен только build id:
```bash
PACKAGE_VERSION=1.0.0 \
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) \
./tools/build/build.sh
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh
```
## Проверка результата
@@ -207,9 +264,13 @@ tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env
Обновлять и коммитить [`orchestrator/bun.lock`](../../orchestrator/bun.lock) следует только когда:
- изменился [`orchestrator/package.json`](../../orchestrator/package.json);
- изменился `BUN_REQUIRED` в [`tools/build/lib/deps.sh`](lib/deps.sh);
- изменился `BUN_VERSION` в [`versions.env`](../../versions.env);
- зависимости оркестратора обновляются осознанно.
При смене `BUN_VERSION` нужно обновить и `packageManager` в
`orchestrator/package.json`, и обе контрольные суммы Bun: иначе
`verify_versions_contract` остановит сборку.
Production builder всегда выполняет:
```bash
@@ -225,7 +286,7 @@ bun install --frozen-lockfile
Правила управления пакетами frontend:
- [`apps/frontend/package.json`](../../apps/frontend/package.json) объявляет `"packageManager": "pnpm@9.15.9"`;
- builder использует закреплённый pnpm `9.15.9` из [`PNPM_REQUIRED`](lib/deps.sh);
- builder использует закреплённый pnpm из `PNPM_VERSION` в [`versions.env`](../../versions.env), и `verify_versions_contract` сверяет эти два значения;
- production-путь установки frontend всегда:
```bash
@@ -236,18 +297,21 @@ pnpm install --frozen-lockfile
## Полезные переменные
- `PACKAGE_VERSION=1.0.0`
- `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`
По умолчанию `VERIFY_TOOLCHAIN_CHECKSUMS=true` в [`tools/build/lib/deps.sh`](lib/deps.sh), поэтому для production-сборки обязательно передавать контрольные суммы:
`PACKAGE_VERSION` берётся из `versions.env` (`HY2XS_VERSION`); переопределять
его вручную нужно только для отладочных сборок, и `verify_versions_contract`
такую сборку отклонит.
- `GO_ARCHIVE_SHA256=<sha256>`
- `NODE_ARCHIVE_SHA256=<sha256>`
- `BUN_ARCHIVE_SHA256=<sha256>`
Контрольные суммы toolchain больше **не передаются через окружение**: они
объявлены в `versions.env`. Раньше воспроизводимая сборка в чистой Debian-среде
требовала предварительного знания четырёх SHA-256 и не запускалась одной
командой.
## Политика памяти при сборке frontend
@@ -292,7 +356,7 @@ echo "bun_exit=$?"
rm -rf .toolchain/bun .toolchain/bun-tmp
rm -f .toolchain/downloads/bun-linux-x64-*.zip
rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip
PACKAGE_VERSION=1.0.0 ./tools/build/build.sh
./tools/build/build.sh
```
Проверить shell syntax:
@@ -300,7 +364,10 @@ PACKAGE_VERSION=1.0.0 ./tools/build/build.sh
```bash
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
```