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 после
разнесения по каталогам совпадал бы ровно с одним файлом.
This commit is contained in:
+509
@@ -0,0 +1,509 @@
|
||||
# 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.
|
||||
|
||||
### Память build-хоста
|
||||
|
||||
Самый требовательный шаг сборки — не компиляция, а `govulncheck`: он строит
|
||||
граф достижимости по всему модулю вместе со stdlib.
|
||||
|
||||
На прогоне `v1.0.0-rc1` машина с ~1.9 GiB RAM и **нулевым swap** получила
|
||||
`govulncheck`, убитый Linux OOM killer. После подключения временного swap 4 GiB
|
||||
полный security gate прошёл.
|
||||
|
||||
4 GiB swap — не формально доказанный минимум, а подтверждённая рабочая
|
||||
конфигурация того прогона; см.
|
||||
[отчёт приёмки](../acceptance/2026-09-01-v1.0.0-rc1-host-acceptance.md).
|
||||
Практическое следствие: сборочная машина примерно с 2 GiB RAM без swap может
|
||||
оказаться недостаточной, и отказ выглядит как убитый процесс, а не как
|
||||
внятная ошибка инструмента.
|
||||
|
||||
Это требование к сборочной машине, а не к target-серверу: `govulncheck` в
|
||||
runtime-артефакт не попадает.
|
||||
|
||||
## Что хранится в репозитории проекта
|
||||
|
||||
Минимум:
|
||||
- исходники оркестратора на **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 — обязательный шаг релиза
|
||||
|
||||
```text
|
||||
pnpm run typecheck → vue-tsc --noEmit → ОБЯЗАН пройти
|
||||
pnpm run build:prod → vite build
|
||||
pnpm run verify → typecheck, затем build
|
||||
```
|
||||
|
||||
`bundle_ui()` запускает `typecheck` **до** сборки bundle: собирать production
|
||||
bundle из кода, который не проходит проверку типов, незачем. Порядок и сам факт
|
||||
наличия шага проверяются приёмкой.
|
||||
|
||||
До v1 этой гарантии не было. `build:prod` означал `vite build && vue-tsc
|
||||
--noEmit`, но `vue-tsc` был версии `0.35.0` (2022 год) и шаблоны Vue
|
||||
практически не типизировал: проверка проходила зелёной, не давая гарантии,
|
||||
которую обещает. Хуже того — на Vue 3.5 она ломается сама, потому что не знает
|
||||
`vue/jsx-runtime`, то есть пережить обновление Vue всё равно не могла.
|
||||
|
||||
Современный `vue-tsc 3.3` на том же коде дал **142 ошибки**: 141 × `TS18048`
|
||||
(«possibly undefined» при обращении к необязательным секциям конфига Hysteria в
|
||||
шаблоне) и одна `TS2322`, всё в двух файлах представления Hysteria. Ожидавшегося
|
||||
класса «`DefaultRow` несовместим с `PeerVo`» на Element Plus 2.3 не было вовсе —
|
||||
он появился позже, вместе с обновлением Element Plus до 2.14, где слоты таблицы
|
||||
типизированы строже.
|
||||
|
||||
Закрыто это не подавлением, а границей: `api/config/hysteriaViewModel.ts`
|
||||
превращает ответ сервера в модель, где присутствие каждой секции — свойство
|
||||
типа. Подробности — в [docs/04](../admin/04-admin-panel.md).
|
||||
|
||||
Контракт теперь читается так:
|
||||
|
||||
> проверка типов SFC-шаблонов проходит, и это доказывает сборка, а не намерение.
|
||||
|
||||
### Проверка зависимостей на уязвимости
|
||||
|
||||
`tools/build/lib/security.sh` — обязательный шаг сборки между тестами админки и
|
||||
записью metadata:
|
||||
|
||||
| Проверка | Что покрывает | Порог |
|
||||
| --- | --- | --- |
|
||||
| `govulncheck ./...` | Go-граф **и stdlib**, с анализом достижимости: уязвимость считается только при наличии пути вызова из нашего кода | любая вызываемая |
|
||||
| `pnpm audit` | **весь** lock-граф frontend, включая build tooling, без анализа достижимости | `PNPM_AUDIT_LEVEL` |
|
||||
|
||||
Про «весь граф» отдельно, потому что здесь стояло `--prod` с обоснованием
|
||||
«devDependencies в артефакт не попадают».
|
||||
|
||||
Для frontend build tooling это обоснование неверно по существу. `vite` и
|
||||
`rollup` действительно не копируются на production-сервер как `node_modules`.
|
||||
Но они **исполняются на build-машине, читают наши исходники и порождают тот
|
||||
самый production-бандл**, который уезжает в артефакт. Уязвимость в них — это
|
||||
уязвимость в том, что мы выпускаем.
|
||||
|
||||
Это не гипотеза: DOM clobbering в Rollup затрагивал именно генерируемый бандл, а
|
||||
проверка по одному production-подграфу его не показывала. По всему графу тот же
|
||||
прогон дал 33 предупреждения против нуля.
|
||||
|
||||
Версия `govulncheck` пиньтся в `versions.env`, а база уязвимостей подтягивается
|
||||
на каждом запуске: пин инструмента не должен превращаться в пин знаний о мире.
|
||||
|
||||
Аварийного выхода у шага **нет**, и это отличает его от `ALLOW_DIRTY_BUILD`.
|
||||
Результат уезжает в `metadata/package.env` полем `dependency_security_gate`,
|
||||
которое принимает единственное значение `true`: по готовому tarball видно, что
|
||||
он проверялся, потому что непроверенного tarball не бывает.
|
||||
|
||||
Две переменные обхода здесь существовали и были описаны как способ выпустить
|
||||
релиз, зная об уязвимости. Способом они не были: финальная приёмка архива
|
||||
требует буквально `dependency_security_gate=true`, поэтому сборка с любой из них
|
||||
доходила до конца — компиляция, бандл, тесты, метаданные, tar — и падала на
|
||||
последнем шаге. Продукт документировал операцию, которую сам же запрещал.
|
||||
Противоречие закрыто в пользу строгой политики; отсутствие обходов проверяется
|
||||
приёмкой, а не только описано здесь.
|
||||
|
||||
Контракт читается однозначно:
|
||||
|
||||
> релизный артефакт HY2XS невозможно собрать с непройденной проверкой
|
||||
> зависимостей.
|
||||
|
||||
Новое advisory чинится обновлением графа (`apps/go.sum`,
|
||||
`apps/frontend/pnpm-lock.yaml`) или версии toolchain в `versions.env`. Для
|
||||
локальной работы обходить нечего: `go test ./...`, `govulncheck ./...` и
|
||||
`pnpm audit` запускаются напрямую и tarball не создают.
|
||||
|
||||
### Тесты и типы
|
||||
|
||||
Та же политика и по той же причине. Аварийного выхода у этого шага **нет**:
|
||||
переменной, отключающей тесты, не существует.
|
||||
|
||||
Проверяется на трёх участках:
|
||||
|
||||
| Шаг сборки | Что запускается |
|
||||
| --- | --- |
|
||||
| `run_orchestrator_tests` | `bun x tsc --noEmit`, `bun test` |
|
||||
| `bundle_ui` | `pnpm run typecheck` (`vue-tsc --noEmit`) до сборки bundle |
|
||||
| `run_admin_tests` | `go vet ./...`, `go test ./...` |
|
||||
|
||||
Готовый пакет объявляет об этом полем `tests_gate=true` в
|
||||
`metadata/package.env` — так же, как `dependency_security_gate` и
|
||||
`hysteria_compat_gate`. Значение у поля ровно одно, потому что не бывает
|
||||
пакета, собранного с пропущенными тестами: обе функции прогона выставляют свой
|
||||
флаг **после** успешного завершения, а `write_metadata` отказывается писать
|
||||
метаданные, если хотя бы один из них не выставлен. То есть поле остаётся
|
||||
утверждением о результате, а не переключателем.
|
||||
|
||||
Здесь существовала переменная, описанная как «аварийное отключение тестов; для
|
||||
release-сборок недопустимо». Недопустимость держалась исключительно на этой
|
||||
фразе: ни metadata, ни финальная приёмка архива не проверяли, что тесты
|
||||
запускались, поэтому сборка с ней доходила до конца и выдавала внешне
|
||||
неотличимый production-tarball. Глушила она при этом не только тесты, но и
|
||||
`tsc --noEmit` с `go vet` — то есть проверку типов и статический анализ того
|
||||
самого кода, который уезжает в production. История — в `CHANGELOG.md`.
|
||||
|
||||
### Проверка, а не генерация
|
||||
|
||||
`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
|
||||
Reference in New Issue
Block a user