594525dd73
Два гейта сборки проверяли не то, что обещали.
1. pnpm audit проверял production-подграф вместо всего lock-графа.
Гейт запускался с --prod под обоснованием «devDependencies в артефакт не
попадают». Для frontend build tooling это неверно по существу: vite и
rollup действительно не копируются на production-сервер как node_modules,
но они ИСПОЛНЯЮТСЯ на build-машине, читают наши исходники и порождают тот
самый production-бандл, который уезжает в артефакт.
Это не гипотеза: DOM clobbering в Rollup затрагивал именно генерируемый
бандл, и `pnpm audit --prod` его не показывал — по всему графу тот же
прогон дал 33 предупреждения против нуля. Критерий приёмки №47 в docs/11
формулировал «по всему графу» правильно ещё до того, как это стало правдой
в коде.
На текущем lock-файле полный граф на пороге high чист.
2. SKIP_TESTS позволял собрать production-артефакт без тестов.
Переменная была описана как «аварийное отключение тестов; для
release-сборок недопустимо». Недопустимость держалась исключительно на этой
фразе: ни metadata, ни финальная приёмка архива не проверяли, что тесты
запускались. То есть
SKIP_TESTS=true ./tools/build/build.sh
доходила до конца и выдавала обычный tarball с build_profile=production и
dependency_security_gate=true — артефакт, по которому невозможно отличить
проверенную сборку от непроверенной.
Глушила она при этом не только тесты: под тем же флагом пропускались
`tsc --noEmit` для оркестратора и `go vet` для админки, то есть проверка
типов и статический анализ того самого кода, который уезжает в production.
Выбран тот же строгий вариант, что уже принят для проверки зависимостей:
обхода нет. Готовый пакет объявляет tests_gate=true в metadata, и это
утверждение опирается на результат — обе функции прогона выставляют свой
флаг только после успешного завершения, а write_metadata отказывается
писать метаданные, если хотя бы один не подтверждён.
Приёмка закрепляет оба инварианта: --prod не может вернуться в гейт, SKIP_TESTS
не может вернуться ни в один модуль сборки и ни в README/docs, tests_gate=true
обязателен в metadata, а утверждение о прогоне обязано следовать за прогоном.
377 lines
18 KiB
Markdown
377 lines
18 KiB
Markdown
# HY2XS production builder
|
||
|
||
Этот каталог содержит production builder для HY2XS.
|
||
|
||
Builder собирает один переносимый install-archive:
|
||
|
||
```text
|
||
dist/hy2xs-install-<version>.tar.gz
|
||
```
|
||
|
||
Этот архив переносится на production server, распаковывается и устанавливается через [`install.sh`](../../package/install.sh).
|
||
|
||
На production server не должно быть сборки из исходников: без `go build`, без `bun install`, без `pnpm install`, без frontend build и без TypeScript transpilation.
|
||
|
||
## Что где лежит
|
||
|
||
Основные части проекта:
|
||
|
||
- [`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) — проверка структуры репозитория и итогового архива.
|
||
- [`tools/build/lib/acceptance.sh`](lib/acceptance.sh) — acceptance-проверки production-контракта.
|
||
- [`tools/build/lib/hysteria.sh`](lib/hysteria.sh) — разрешение upstream-версии Hysteria и compatibility gate.
|
||
- [`tools/build/hysteria-lock.env`](hysteria-lock.env) — fallback-значения для офлайн-сборки (`HYSTERIA_CHANNEL=pinned`).
|
||
- [`tools/test/e2e-hysteria.sh`](../test/e2e-hysteria.sh) — end-to-end проверка с реальным клиентом Hysteria.
|
||
- [`orchestrator`](../../orchestrator) — TypeScript/Bun install-only orchestrator.
|
||
- [`apps`](../../apps) — HY2XS admin: Go backend и Vue frontend.
|
||
- [`package`](../../package) — skeleton будущего install package: `install.sh`, templates, systemd units, default config.
|
||
- [`dist`](../../dist) — итоговые архивы. Создаётся builder'ом, в git обычно не хранится.
|
||
- [`.toolchain`](../../.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`](../../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
|
||
|
||
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: сумма фиксирует то, что пришло, каким бы оно ни было.
|
||
|
||
Формат ассета:
|
||
|
||
```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:
|
||
|
||
```text
|
||
скачать бинарник
|
||
↓
|
||
сверить SHA-256 и `hysteria version`
|
||
↓
|
||
отрендерить канонический конфиг HY2XS тем же кодом, что и на target
|
||
↓
|
||
запустить настоящий Hysteria с этим конфигом (gecko и salamander)
|
||
↓
|
||
только после этого собирать release package
|
||
```
|
||
|
||
При несовместимости сборка останавливается:
|
||
|
||
```text
|
||
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:
|
||
|
||
```bash
|
||
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`.
|
||
|
||
По умолчанию:
|
||
|
||
```bash
|
||
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
|
||
BUN_FLAVOR=x64 ./tools/build/build.sh
|
||
```
|
||
|
||
или:
|
||
|
||
```bash
|
||
BUN_FLAVOR=x64-baseline ./tools/build/build.sh
|
||
```
|
||
|
||
## Запуск
|
||
|
||
Из корня репозитория:
|
||
|
||
```bash
|
||
./tools/build/build.sh
|
||
```
|
||
|
||
Версия пакета берётся из `versions.env`, поэтому обычно нужен только build id:
|
||
|
||
```bash
|
||
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh
|
||
```
|
||
|
||
## Проверка результата
|
||
|
||
Проверка архива:
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
Проверка обязательных файлов:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env
|
||
```
|
||
|
||
## Когда обновлять orchestrator/bun.lock
|
||
|
||
Не перегенерируйте [`orchestrator/bun.lock`](../../orchestrator/bun.lock) во время production-сборки.
|
||
|
||
Обновлять и коммитить [`orchestrator/bun.lock`](../../orchestrator/bun.lock) следует только когда:
|
||
|
||
- изменился [`orchestrator/package.json`](../../orchestrator/package.json);
|
||
- изменился `BUN_VERSION` в [`versions.env`](../../versions.env);
|
||
- зависимости оркестратора обновляются осознанно.
|
||
|
||
При смене `BUN_VERSION` нужно обновить и `packageManager` в
|
||
`orchestrator/package.json`, и обе контрольные суммы Bun: иначе
|
||
`verify_versions_contract` остановит сборку.
|
||
|
||
Production builder всегда выполняет:
|
||
|
||
```bash
|
||
bun install --frozen-lockfile
|
||
```
|
||
|
||
Если команда падает, исправьте и закоммитьте lockfile в системе контроля версий. Не убирайте `--frozen-lockfile`.
|
||
|
||
## Дисциплина lockfile для frontend
|
||
|
||
Не перегенерируйте frontend lock data во время обычной production-сборки.
|
||
|
||
Правила управления пакетами frontend:
|
||
|
||
- [`apps/frontend/package.json`](../../apps/frontend/package.json) объявляет `"packageManager": "pnpm@9.15.9"`;
|
||
- builder использует закреплённый pnpm из `PNPM_VERSION` в [`versions.env`](../../versions.env), и `verify_versions_contract` сверяет эти два значения;
|
||
- production-путь установки frontend всегда:
|
||
|
||
```bash
|
||
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()`](lib/package.sh):
|
||
|
||
```bash
|
||
--max-old-space-size=2048
|
||
```
|
||
|
||
Способы переопределения:
|
||
|
||
- изменить значение по умолчанию для этой политики:
|
||
|
||
```bash
|
||
FRONTEND_NODE_OLD_SPACE_SIZE=3072 ./tools/build/build.sh
|
||
```
|
||
|
||
- или передать полный набор собственных Node-опций (если `--max-old-space-size` там уже задан, builder не добавит второй):
|
||
|
||
```bash
|
||
NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh
|
||
```
|
||
|
||
## Диагностика
|
||
|
||
Проверить AVX2:
|
||
|
||
```bash
|
||
grep -m1 '^flags' /proc/cpuinfo | grep -qw avx2 && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'
|
||
```
|
||
|
||
Проверить Bun:
|
||
|
||
```bash
|
||
.toolchain/bun/bin/bun --version
|
||
echo "bun_exit=$?"
|
||
```
|
||
|
||
Если есть `Illegal instruction`, удалите старый Bun и пересоберите:
|
||
|
||
```bash
|
||
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
|
||
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
|
||
```
|