Files
HY2XS_flamy/docs/02-build-layer-and-package.md
T
founder e30fdaa004 docs: зафиксировать известное ограничение проверки типов frontend
vue-tsc 0.35.0 (2022) шаблоны Vue практически не типизирует: build:prod
проходит зелёной, не давая гарантии, которую обещает. Замер записан — на паре
typescript@5.9 + vue-tsc@2.2 тот же код даёт около 155 ошибок в четырёх файлах.

Обновление typechecker'а тянет vue 3.2 -> 3.5, а за ним element-plus, pinia и
vue-router, поэтому вынесено в отдельную работу. На безопасность не влияет:
уязвимые пакеты обновляются движением lockfile внутри объявленных диапазонов.
2026-08-29 21:38:40 +05:00

422 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build layer and package
## Цель документа
Зафиксировать локальный слой сборки и формат итогового install package.
## Базовое решение
В baseline builder остаётся **shell-first** для packaging-слоя.
То есть:
- основной packaging pipeline — **sh/bash**
- оркестратор при этом пишется на **Bun + TypeScript**
- builder локально компилирует оркестратор в готовый install-артефакт
- target machine не должна сама собирать или доустанавливать JS/TS toolchain
Причина простая: packaging можно держать простым, а оркестратор — typed и модульным.
## Где работает builder
Production builder работает на отдельном build host:
- Debian 13
- amd64 / x86_64
- bash
- доступ к интернету для apt и скачивания toolchain
В текущей production-модели сборка выполняется **на Debian 13 amd64**, а не на Windows/macOS dev-машине.
Builder не является частью target install flow: на target server приезжает уже готовый install package, без JS/TS/Go build step.
## Что хранится в репозитории проекта
Минимум:
- исходники оркестратора на **Bun + TypeScript**
- shell packaging scripts
- шаблоны конфигов
- systemd unit templates
- docs
- исходный код **HY2XS admin**
- шаблоны для `post-install.env`
- package metadata
## Контракт версий: `versions.env`
Корневой `versions.env`**единственный источник истины** для контракта
«продукт / платформа / toolchain».
Что в нём есть:
```bash
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
GO_VERSION=1.26.7
GO_LINUX_AMD64_SHA256=<sha256>
BUN_VERSION=1.3.13
BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>
NODE_VERSION=24.20.0
NODE_LINUX_X64_SHA256=<sha256>
PNPM_VERSION=9.15.9
HYSTERIA_CHANNEL=stable
GOVULNCHECK_VERSION=v1.7.0
PNPM_AUDIT_LEVEL=high
```
Чего в нём **нет** и быть не должно:
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`. Пин версии здесь вернул бы ручное
обновление, от которого мы ушли.
Два Bun-артефакта зафиксированы отдельно намеренно: `select_bun_artifact()`
выбирает `bun-linux-x64` или `bun-linux-x64-baseline` по наличию AVX2, поэтому
одной контрольной суммы архитектурно недостаточно.
### Почему версии toolchain — это вопрос безопасности, а не удобства
Go здесь не просто сборщик: им компилируется `hy2xs-admin`, и его stdlib целиком
попадает в production-бинарь. Поэтому версия выбирается по политике поддержки Go
(major поддерживается, пока не вышли две более новые), а не по тому, на чём
собиралось раньше.
Цифры, ради которых это записано. На `GO_VERSION=1.21.13` — линия, давно вне
поддержки — `govulncheck ./...` находил **21 вызываемую уязвимость**, из них 17 в
одной только stdlib. После перехода на 1.26.7 и обновления графа зависимостей —
**ноль**.
Node живёт только на build-хосте и в артефакт не попадает, но 20.x достигла EOL,
то есть перестала получать security-обновления, а собирает она код, который
уезжает в production. Отсюда LTS-линия 24.
Bun обновляется отдельно от остальных: оркестратор собирается через
`bun build --compile`, то есть Bun runtime физически входит в исполняемый файл.
Смена его minor-версии — это смена рантайма внутри артефакта, и она требует
полного прохода `bun test → tsc → compile → приёмка на Debian`, а не строки в
общем патче.
### Известное ограничение: проверка типов frontend почти ничего не проверяет
`pnpm run build:prod` выполняет `vite build && vue-tsc --noEmit`, но `vue-tsc`
здесь версии `0.35.0` (2022 год) и шаблоны Vue практически не типизирует.
Проверка проходит зелёной, не давая гарантии, которую обещает.
Замер сделан: на паре `typescript@5.9` + `vue-tsc@2.2` тот же исходный код даёт
**около 155 ошибок типов** в четырёх файлах — почти все одного вида
(`possibly 'undefined'` при обращении к необязательным полям модели конфига
Hysteria в шаблоне) плюс несовместимость `DefaultRow` с `PeerVo` в слотах
таблицы пиров.
Это не дефект безопасности и не блокер релиза: ошибки существуют в коде уже
сейчас и ни на что в рантайме не влияют. Но обновление typechecker'а тянет за
собой обновление `vue` (3.2 → 3.5, иначе `vue-tsc` 2.x не разбирает
`JSX.IntrinsicElements`), а за ним — `element-plus`, `pinia` и `vue-router`.
То есть это отдельная работа с собственной проверкой на живой панели, а не
строка в security-патче.
Ограничение на безопасность не влияет: уязвимые пакеты frontend обновляются
независимо от версии typechecker'а, движением lockfile внутри уже объявленных
диапазонов (см. `pnpm.overrides` в `apps/frontend/package.json`).
### Проверка зависимостей на уязвимости
`tools/build/lib/security.sh` — обязательный шаг сборки между тестами админки и
записью metadata:
| Проверка | Что покрывает | Порог |
| --- | --- | --- |
| `govulncheck ./...` | Go-граф **и stdlib**, с анализом достижимости: уязвимость считается только при наличии пути вызова из нашего кода | любая вызываемая |
| `pnpm audit --prod` | production-зависимости frontend, без анализа достижимости | `PNPM_AUDIT_LEVEL` |
Версия `govulncheck` пиньтся в `versions.env`, а база уязвимостей подтягивается
на каждом запуске: пин инструмента не должен превращаться в пин знаний о мире.
Аварийный выход — `ALLOW_VULNERABLE_DEPENDENCIES=true`, по той же логике, что и
`ALLOW_DIRTY_BUILD`: выпустить релиз, зная об уязвимости, можно, но это решение
человека, а не поведение по умолчанию. Результат шага уезжает в
`metadata/package.env` полем `dependency_security_gate`, так что по готовому
tarball видно, проверялся он или собран с пропущенной проверкой.
### Проверка, а не генерация
`profile.ts`, `package/config/hy2xs.env` и `packageManager` в двух `package.json`
остаются обычными файлами. Сборка их **не генерирует**, а сверяет шагом
`verify_versions_contract`.
Причина: генерируемые исходники ломают чистый чекаут — `bun test`, `tsc` и
`go test` должны работать до запуска сборки. Проверка даёт тот же инвариант
дешевле.
`verify_versions_contract` сверяет:
| Что | С чем |
| --- | --- |
| `PACKAGE_VERSION` | `HY2XS_VERSION` |
| `orchestrator/package.json``packageManager` | `bun@$BUN_VERSION` |
| `apps/frontend/package.json``packageManager` | `pnpm@$PNPM_VERSION` |
| `package/config/hy2xs.env` → схема | `HY2XS_CONFIG_SCHEMA_VERSION` |
| константы, **скомпилированные** в оркестратор | схема, release line, целевая платформа, API namespace |
| `constant.AdminAPIBase` / `constant.HysteriaMachineAuthPath` (Go) | константы оркестратора |
| `API_BASE` фронтенда | `constant.AdminAPIBase` |
| шаблоны Hysteria и `post-install.env` | `HYSTERIA_MACHINE_AUTH_PATH` |
| `apps/go.mod` → директива `go` | `GO_VERSION` |
| `metadata/package.env` | версия, release line, схема, target |
| `hy2xs-admin version` (готовый бинарь) | `v$HY2XS_VERSION` |
Контракт оркестратора сверяется не grep'ом по исходникам, а выводом
`orchestrator/tools/print-contract.ts`: это доказывает, что в бинарь попало то
же значение.
API namespace попал в этот список не для красоты. Путь machine-auth
записывается в `/etc/hysteria/config.yaml` и в `post-install.env`, то есть по
нему Hysteria обращается к админке. Пока строка была продублирована в шаблонах,
smoke, тестах, приёмке и e2e, расхождение обнаруживалось только на живом
сервере.
### Версии в `package.json` — не версия продукта
`orchestrator/package.json` объявляет `version: 0.1.0`, а
`apps/frontend/package.json``version: 0.0.0`. Это placeholder'ы приватных
пакетов, которые никуда не публикуются; единственная версия продукта живёт в
`versions.env` (`HY2XS_VERSION`) и оттуда доезжает до `metadata/package.env`,
install-state и бинарника админки. Ни одно из этих двух чисел не участвует в
контракте версий и не должно восприниматься как release version.
Версия админки приходит в бинарь через ldflags:
```bash
-ldflags "-s -w -X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}'"
```
Собственной константы версии в Go-коде больше нет: она уже успела разъехаться с
версией пакета.
## Что делает builder
1. Проверяет структуру проекта.
2. Загружает и проверяет контракт `versions.env`.
3. Прогоняет тесты и типы оркестратора.
4. Разрешает upstream-версию Hysteria и проходит compatibility gate.
5. Компилирует оркестратор из Bun/TypeScript в install-артефакт.
6. Собирает / подготавливает HY2XS admin и сверяет его версию с контрактом.
7. Прогоняет тесты HY2XS admin (после сборки frontend: `go:embed all:dist` требует готовых ассетов).
8. Копирует артефакты UI в package staging directory.
9. Кладёт entrypoint, templates, docs и service files.
10. Формирует итоговый install package.
11. Считает manifest/checksum.
12. Проверяет архив и прогоняет acceptance-проверки.
13. Выдаёт один переносимый результат для target machine.
## Что builder не делает
- не ставит Hysteria2 на локальной машине «для продакшена»
- не превращается в CI/CD платформу
- не генерирует update pipeline
- не делает uninstall manifests
- не готовит миграции между старыми инсталляциями
## Рекомендуемая структура
```text
project/
├── tools/
│ └── build/
│ ├── build.sh
│ ├── README.md
│ └── lib/
├── orchestrator/
│ ├── package.json
│ ├── bun.lock
│ ├── tsconfig.json
│ └── src/
├── package/
│ ├── install.sh
│ ├── orchestrator/
│ ├── templates/
│ └── systemd/
├── ui/
│ └── hy2xs-admin/
├── docs/
└── dist/
```
## Формат итогового пакета
Итоговый пакет должен содержать:
- install-only orchestrator artifact
- bundled HY2XS admin
- unit templates
- config templates
- docs / examples
- manifest версии проекта
Итоговый пакет **не должен** содержать:
- builder scripts
- исходную локальную build-среду
- временные каталоги сборки
- мусор CI
- target-side dependency install step для оркестратора
## Production builder bootstrap
`tools/build/build.sh` должен быть самодостаточным для Debian 13 amd64:
1. Проверяет ОС и архитектуру по `versions.env` (`HY2XS_BUILD_*`).
2. Проверяет структуру репозитория и lock-файлы.
3. Доставляет отсутствующие системные build-зависимости через `apt-get`.
4. Проверяет версии Go, Bun, Node.js и pnpm по `versions.env`.
5. При несовпадении версий скачивает управляемый локальный toolchain в `.toolchain/`
и **сверяет каждый архив с контрольной суммой из `versions.env`**.
6. Собирает только Linux amd64 артефакты.
7. Записывает версии toolchain в metadata пакета.
Собственных значений по умолчанию у `tools/build/lib/deps.sh` больше нет: без
загруженного контракта сборка падает сразу, а не собирает пакет на неизвестном
toolchain.
## Отношение к Hysteria2
Сам бинарь Hysteria2 **не вендорится** в install package как baseline-правило.
Причина:
- ядро Hysteria рассматривается как stable upstream component;
- целевая установка скачивает его с official upstream, но **строго по замороженным координатам**.
### Разрешение версии на сборке
```text
SOURCE
resolve latest stable (HyNetworks/hysteria, только теги app/vX.Y.Z)
resolve exact release asset (hysteria-linux-amd64 + hashes.txt)
download hashes.txt → ожидаемый SHA-256 от upstream
download artifact + сверка с ожидаемым SHA-256
compatibility gate (реальный бинарник принимает канонический конфиг HY2XS)
PACKAGE METADATA
version = vX.Y.Z
exact_url = <immutable release asset>
sha256 = <...>
resolution = latest-stable | pinned | override
TARGET SERVER
скачивает уже конкретный неизменяемый артефакт
```
Так одновременно выполняются оба требования: «по умолчанию брать последнюю стабильную» и «production-установка должна быть детерминированной и проверяемой».
Переменные builder:
| Переменная | Значение по умолчанию | Назначение |
| --- | --- | --- |
| `HYSTERIA_CHANNEL` | `stable` | `stable` — разрешить последнюю стабильную через upstream API; `pinned` — взять `tools/build/hysteria-lock.env` без сети |
| `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` |
| `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен |
| `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `tools/build/hysteria-lock.env` |
| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break-glass |
| `GITHUB_TOKEN` | пусто | Опционально, чтобы не упереться в anonymous rate limit |
### Проверка происхождения артефакта
Раньше SHA-256 считался локально от уже скачанного файла. Это защищает target от
последующей подмены, но не доказывает, что builder скачал именно ожидаемый
upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было
(trust-on-first-use).
Upstream публикует контрольные суммы релиза отдельным ассетом `hashes.txt`:
```text
6493dfff…f94 build/hysteria-linux-amd64
f24f63be…189 build/hysteria-linux-amd64-avx
```
Поэтому порядок теперь такой:
```text
download hysteria-linux-amd64
download hashes.txt
ожидаемый SHA-256 из upstream
сверка скачанного бинарника
и только после этого — запись SHA-256 в HY2XS lock и metadata
```
Сопоставление идёт по базовому имени и строго на равенство: `build/` — часть
пути, а `hysteria-linux-amd64-avx` — другой артефакт, который не должен совпасть
по префиксу. Разбор вынесен в `parseUpstreamHashes` и покрыт тестами.
Источник ожидаемой суммы фиксируется в `metadata/package.env`
(`hysteria_sha_source=upstream-hashes | hy2xs-lock | local-download`).
Дополнительно:
- версия, URL и SHA256 фиксируются в metadata install package (`metadata/hysteria.version`, `metadata/hysteria.url`, `metadata/hysteria.sha256`);
- способ выбора версии фиксируется в `metadata/hysteria.resolution` и `metadata/package.env`;
- runtime `reconfigure` не обновляет и не откатывает бинарник Hysteria2;
- install flow валидирует SHA256 и фактическую версию установленного бинарника;
- install-time код не обращается к upstream API и не использует moving `latest` — это проверяется тестами и acceptance-шагом сборки.
### Compatibility gate
Gate защищает от ситуации, когда upstream меняет схему конфигурации, а builder молча собирает неработающий HY2XS.
Порядок:
1. скачать артефакт и сверить SHA-256 с upstream `hashes.txt`;
2. сверить `hysteria version` с разрешённой версией;
3. отрендерить канонический конфиг HY2XS тем же кодом, что работает на target (`orchestrator/tools/render-canonical-config.ts`);
4. запустить реальный бинарник Hysteria с этим конфигом — для Gecko и для Salamander;
5. только после этого собирать release package.
При несовместимости ломается сборка:
```text
BUILD FAILED: unsupported Hysteria stable v2.13.0
```
Это осознанно: ошибка должна проявиться на build machine, а не на сервере оператора.
## Инварианты
Система считается правильной, если:
1. builder запускается на Debian 13 amd64 build host, не как target-side build step
2. пакет можно перенести на чистый Debian 13
3. на сервере нет отдельного build step
4. bundled UI уже находится внутри пакета
5. оркестратор authored as Bun/TypeScript, но на target приходит как готовый install-артефакт
6. Hysteria2 подтягивается install layer'ом с upstream по замороженным координатам, а не собирается на target из исходников
7. выход новой версии Hysteria после сборки не меняет содержимое уже собранного пакета
8. несовместимый upstream ломает сборку, а не установку у пользователя
9. контрольная сумма Hysteria подтверждена upstream-ассетом `hashes.txt`, а не только локальным пересчётом
10. версии продукта, платформы и toolchain объявлены в одном месте, а рассинхрон роняет сборку до создания tarball