ADR-012: именованные сборки (zip) рядом с номерными — /sol/; ^~ для служебных путей; два дефекта сборки фронтендера

This commit is contained in:
Porfiry
2026-09-15 19:29:54 +03:00
parent d66c5f8893
commit b9799bf091
5 changed files with 175 additions and 17 deletions
+24
View File
@@ -226,3 +226,27 @@ curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' https://smart-updown
**TODO:** **TODO:**
- [ ] `[pump.meta]` Автоматизировать шаг 3 (сейчас ручная команда после CI). - [ ] `[pump.meta]` Автоматизировать шаг 3 (сейчас ручная команда после CI).
- [ ] `[pump.meta]` Ретеншен старых сборок на 152 (сейчас 27, 28, 29 = ~200 МБ; политика за владельцем). - [ ] `[pump.meta]` Ретеншен старых сборок на 152 (сейчас 27, 28, 29 = ~200 МБ; политика за владельцем).
## ⭐ 2026-09-15 (4) — именованная сборка `sol` из zip + два дефекта в ней
**Сделано:** фронтендер прислал `build_sol.zip` (вне релизного цикла, без номера). Выложил под именем
`sol` → **https://smart-updown.binom.pw/sol/** (см. ADR-012, `tools/proxmox-nginx/publish-zip.sh`).
Активная сборка не тронута — корень по-прежнему `/29/`. Адрес: `/sol/`.
**Правки в nginx (иначе не работало):**
- локейшн сборок расширен с чисел до имён → **обязательно вернуть `^~` служебным путям**
(`/api`, `/ws`, `/solana-rpc/`), иначе расширенный regex перехватывает `/api/...` и ломает бэк;
- `immutable` — только для номерных каталогов; именованные кэшировать нельзя (могут перезалить);
- в маску добавлен `unityweb` (сборки фронтендера на `.unityweb`-ассетах).
**⚠️ Два дефекта в присланной сборке (отдать фронтендеру):**
- [ ] **`[pump.front]` Двойной префикс API: игра зовёт `/api/api/state` → 404.**
`SmartUpdownConfig.ApiBase()` для WebGL = `Origin()+"/api"`, а `BackendClient.HttpGetAsync("/api/state")`
добавляет `/api/...` ещё раз. Лечится в одном месте: либо `ApiBase()` без `/api`, либо пути без `/api`.
- [ ] **`[pump.front]` `ReferenceError: getProvider is not defined`** — падение при подключении Phantom.
В `framework.js` есть вызовы `getProvider()`/`errMsg()`/`loadWeb3()`, но нет определений: в
`Assets/Plugins/WebGL/SolanaBridge.jslib` они объявлены внутри IIFE, а через `mergeInto` экспортированы
только `SolanaBridge*`-методы. Не регресс от nginx — воспроизводится и на сборке 29.
**Важное подтверждение:** билд `sol` в браузере **дошёл до игрового цикла** (`Phase -> Idle` →
`Phase -> Approach`) — раньше в headless ни одна сборка так далеко не шла. Статика с nginx отдаётся корректно.
+46
View File
@@ -293,3 +293,49 @@ no-store`; `/api/state`,`/api/price`,`/solana-rpc/` → 200; сквозная с
(`/Build/WebGL.wasm`, полученные до этого фикса), один раз может получить старый билд — URL теперь (`/Build/WebGL.wasm`, полученные до этого фикса), один раз может получить старый билд — URL теперь
`no-store`, так что после первой перезагрузки браузер сходит на сервер и получит свежее. Новые `no-store`, так что после первой перезагрузки браузер сходит на сервер и получит свежее. Новые
версии этой проблемы не имеют вовсе. версии этой проблемы не имеют вовсе.
## ADR-012 — Именованные сборки (zip от фронтендера) рядом с номерными: `/sol/` (2026-09-15)
**Контекст.** Фронтендер прислал готовый Unity-WebGL-билд **zip-архивом** — вне релизного цикла Gitea
(не через CI, без образа в реестре, без номера сборки). Владелец: «создай там сам папку и выложи».
**Решение.** Каталоги на nginx теперь двух видов, живут рядом:
- `/opt/game/releases/<N>/` — номерные релизы из CI/реестра (`1`, `2`, … `29`);
- `/opt/game/releases/<имя>/` — именованные сборки из zip (`sol`).
**Именованные сборки АКТИВНУЮ не трогают**: `current` остаётся на номере, корень по-прежнему
редиректит на `/29/`. Именованная доступна только явной ссылкой `/<имя>/`.
**Изменения в nginx** (`game.nginx.conf`):
- regex-локейшн сборок расширен с чисел до имён: `^/(?<build>[a-zA-Z0-9][a-zA-Z0-9_-]*)(?<subpath>/.*)?$`;
- ⚠️ служебные пути получили префикс **`^~`** (`location ^~ /api`, `^~ /ws`, `^~ /solana-rpc/`).
Без этого расширенный regex перехватывал бы `/api/...` как «сборку с именем api» — regex-локейшн
в nginx приоритетнее обычного префиксного. `^~` подавляет проверку regex для этого префикса.
**Это была реальная регрессия на первом применении: после расширения шаблона `/api/state` начал
уходить в файловый резолвер.**
- маска `immutable` осталась **только для номерных** каталогов (`~^/[0-9]+/…`): именованную папку
могут перезалить тем же именем, поэтому кэшировать её ассеты на 7 суток нельзя. Добавлено
расширение `unityweb` (сборки фронтендера используют `.unityweb`-ассеты).
- новый скрипт `tools/proxmox-nginx/publish-zip.sh <имя> <путь.zip>` — распаковка, подъём
единственной вложенной папки, раскладка, reload, проверка.
**Приёмка (живой домен):** `/sol/` и все его ассеты (`Build.loader.js`, `Build.wasm.unityweb`,
`Build.data.unityweb`, `Build.framework.js.unityweb`, `TemplateData/*`) → **200**; `/` → 302 на `/29/`;
`/27/`,`/28/`,`/29/` живы; `/api/state`,`/api/price`,`/solana-rpc/` → 200; `testfront` цел.
**⚠️ Что нашли в присланной сборке (отдать фронтендеру, не наш блокер):**
1. **Двойной префикс API.** Игра зовёт `GET https://smart-updown.binom.pw/api/api/state` → **404**.
Причина в исходниках: `SmartUpdownConfig.ApiBase()` для WebGL возвращает `Origin() + "/api"`,
а `BackendClient.HttpGetAsync("/api/state")` добавляет путь целиком → `…/api` + `/api/state`.
В сборке 29 тот же код, но она до этого места не доходила (в headless падала раньше).
2. **`ReferenceError: getProvider is not defined`** — падение на `_SolanaBridgeConnect` при подключении
кошелька. В собранном `framework.js` есть **вызовы** `getProvider()`, `errMsg()`, `loadWeb3()`,
но **нет их определений** (в `SolanaBridge.jslib` они объявлены как функции внутри IIFE, а в
библиотеку через `mergeInto(LibraryManager.library, {...})` экспортированы только
`SolanaBridge*`-методы). Ошибка воспроизводится и в сборке 29 — то есть это **не регресс от
переезда на nginx**, а дефект jslib-обвязки, который просто не был виден раньше.
Файл — `Assets/Plugins/WebGL/SolanaBridge.jslib` (коммит `090ebf1`).
⚠️ Что **подтверждено** этим билдом: игра в браузере **стартует и доходит до игрового цикла**
(`[BinaryRocket] Phase -> Idle` → `Phase -> Approach`), чего в headless-окружении не делала ни одна
предыдущая сборка. То есть статика с nginx отдаётся корректно.
+26
View File
@@ -81,3 +81,29 @@ supported on this GPU` и Unity-исключение из wasm. **Это арт
⚠️ `publish-release.sh` содержит креды реестра (`subochev:…`) в открытом виде — файл живёт только ⚠️ `publish-release.sh` содержит креды реестра (`subochev:…`) в открытом виде — файл живёт только
внутри LXC 152 как root (`/opt/game/tools/`), в git лежит шаблон из репы ops. Не тиражировать наружу. внутри LXC 152 как root (`/opt/game/tools/`), в git лежит шаблон из репы ops. Не тиражировать наружу.
## Именованные сборки (zip от фронтендера) — рядом с номерными
Кроме номерных релизов из CI (`/opt/game/releases/<N>/`) на nginx можно положить **именованную**
сборку — готовый zip от фронтендера, вне релизного цикла:
```bash
# zip сначала доставить на LXC 152 (через хост проксмокса):
scp build.zip root@192.168.76.66:/tmp/build.zip
ssh root@192.168.76.66 "pct push 152 /tmp/build.zip /tmp/build.zip"
# разложить под именем:
ssh root@192.168.76.66 "pct exec 152 -- /opt/game/tools/publish-zip.sh sol /tmp/build.zip"
```
Адрес: **`https://smart-updown.binom.pw/sol/`**. Активную сборку такая публикация **не трогает** —
корень по-прежнему редиректит на текущий номер.
**Питфоллы именованных сборок:**
- Служебные location обязаны иметь **`^~`** (`^~ /api`, `^~ /ws`, `^~ /solana-rpc/`). Regex-локейшн
сборок `^/(?<build>[a-zA-Z0-9][a-zA-Z0-9_-]*)…` иначе перехватит `/api/...` как «сборку с именем
api» — regex в nginx приоритетнее обычного префикса. Без `^~` API ломается (проверено).
- `immutable` — **только для номерных** каталогов (`~^/[0-9]+/…`). Именованную папку могут перезалить
тем же именем → её ассеты кэшировать нельзя.
- Сборки фронтендера бывают на `.unityweb`-ассетах (`Build.wasm.unityweb` и т.п.) — расширение
`unityweb` в маске кэша учтено.
- Имя папки не должно совпадать со служебным путём (`api`, `ws`, `solana-rpc`) — скрипт это проверяет.
+25 -17
View File
@@ -1,12 +1,14 @@
# nginx для раздачи Unity-сборок «Бинарной ракеты» — LXC 152 (game-web, 192.168.76.152) # nginx для раздачи Unity-сборок «Бинарной ракеты» — LXC 152 (game-web, 192.168.76.152)
# Файл: /etc/nginx/sites-available/game # Файл: /etc/nginx/sites-available/game
# #
# Модель версионирования: # Модель версионирования (два вида сборок, живут рядом):
# /opt/game/releases/<N>/ — файлы сборки N (Build/, TemplateData/, index.html) # /opt/game/releases/<N>/ — номерные релизы (1, 2, 3, … 29) из CI/реестра
# /opt/game/releases/<имя>/ — именованные сборки (например `sol`), присланные zip'ом
# /opt/game/current -> releases/<N> — активная сборка (симлинк, переключает publish-release.sh) # /opt/game/current -> releases/<N> — активная сборка (симлинк, переключает publish-release.sh)
# #
# https://smart-updown.binom.pw/ -> 302 на активную сборку (/29/) # https://smart-updown.binom.pw/ -> 302 на активную сборку (/29/)
# https://smart-updown.binom.pw/29/ -> конкретная сборка 29 # https://smart-updown.binom.pw/29/ -> номерная сборка 29
# https://smart-updown.binom.pw/sol/ -> именованная сборка `sol`
# https://smart-updown.binom.pw/29/Build/WebGL.data -> ассет сборки 29 # https://smart-updown.binom.pw/29/Build/WebGL.data -> ассет сборки 29
# #
# ⚠️ ПОЧЕМУ КОРЕНЬ РЕДИРЕКТИТ, А НЕ ОТДАЁТ ФАЙЛЫ: # ⚠️ ПОЧЕМУ КОРЕНЬ РЕДИРЕКТИТ, А НЕ ОТДАЁТ ФАЙЛЫ:
@@ -18,16 +20,20 @@
# просто перестаёт использоваться, и `immutable` становится корректным. # просто перестаёт использоваться, и `immutable` становится корректным.
# Номер активной сборки пишет publish-release.sh в отдельный include-файл /etc/nginx/game-active.inc. # Номер активной сборки пишет publish-release.sh в отдельный include-файл /etc/nginx/game-active.inc.
# #
# ⚠️ ПОРЯДОК LOCATION. Служебные пути (/api, /ws, /solana-rpc) объявлены с префиксом `^~`,
# потому что regex-локейшн сборок `^/(?<build>…)` иначе перехватил бы их (regex в nginx
# приоритетнее обычного префикса). `^~` подавляет проверку regex для этого префикса.
#
# API и WebSocket nginx НЕ обслуживает: их забирает Traefik на VDS # API и WebSocket nginx НЕ обслуживает: их забирает Traefik на VDS
# (router smart-updown-api: PathPrefix /api,/ws -> куб 192.168.76.120). # (router smart-updown-api: PathPrefix /api,/ws -> куб 192.168.76.120).
# Локальные location /api, /ws, /solana-rpc — страховка для запросов, # Локальные location /api, /ws, /solana-rpc — страховка для запросов,
# которые всё же дойдут до nginx (прямой заход по IP, старые сборки). # которые всё же дойдут до nginx (прямой заход по IP, старые сборки).
# Ассеты Unity внутри каталога версии неизменяемы -> immutable. # Ассеты Unity внутри НОМЕРНОГО каталога неизменяемы -> immutable.
# Всё, что НЕ под номером сборки, и index.html -> no-store. # Всё остальное (включая именованные сборки: их папку могут перезалить) -> no-store.
map $uri $game_cache_control { map $uri $game_cache_control {
default "no-store"; default "no-store";
~^/[0-9]+/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?)$ "public, max-age=604800, immutable"; ~^/[0-9]+/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?|unityweb)$ "public, max-age=604800, immutable";
} }
server { server {
@@ -51,18 +57,11 @@ server {
# Файл переписывает publish-release.sh при --activate. # Файл переписывает publish-release.sh при --activate.
include /etc/nginx/game-active.inc; include /etc/nginx/game-active.inc;
# --- версионированные сборки: /<число>/... ------------------------------- # --- страховка: API (^~ — приоритетнее regex-локейшна сборок) -------------
# /29/index.html -> /opt/game/releases/29/index.html
location ~ ^/(?<build>[0-9]+)(?<subpath>/.*)?$ {
root /opt/game/releases;
try_files /$build$subpath /$build$subpath/ =404;
}
# --- страховка: API -------------------------------------------------------
# Нормальный /api идёт мимо nginx (Traefik VDS -> куб). Если запрос всё же # Нормальный /api идёт мимо nginx (Traefik VDS -> куб). Если запрос всё же
# пришёл сюда — уходим в куб на k3s-Traefik с правильным Host, дальше # пришёл сюда — уходим в куб на k3s-Traefik с правильным Host, дальше
# ingress smart-updown-api ведёт в шлюз :8895. # ingress smart-updown-api ведёт в шлюз :8895.
location /api { location ^~ /api {
proxy_pass http://192.168.76.120; proxy_pass http://192.168.76.120;
proxy_set_header Host smart-updown.binom.pw; proxy_set_header Host smart-updown.binom.pw;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -76,7 +75,7 @@ server {
} }
# --- страховка: WebSocket ------------------------------------------------- # --- страховка: WebSocket -------------------------------------------------
location /ws { location ^~ /ws {
proxy_pass http://192.168.76.120; proxy_pass http://192.168.76.120;
proxy_set_header Host smart-updown.binom.pw; proxy_set_header Host smart-updown.binom.pw;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -92,10 +91,19 @@ server {
# --- страховка: Solana RPC ------------------------------------------------- # --- страховка: Solana RPC -------------------------------------------------
# /solana-rpc/ НЕ ловится PathPrefix /api на VDS, поэтому уходит в корневой # /solana-rpc/ НЕ ловится PathPrefix /api на VDS, поэтому уходит в корневой
# router, т.е. сюда. Старые сборки игры стучатся именно в этот путь. # router, т.е. сюда. Старые сборки игры стучатся именно в этот путь.
location /solana-rpc/ { location ^~ /solana-rpc/ {
proxy_pass http://192.168.76.181:8899/; proxy_pass http://192.168.76.181:8899/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_read_timeout 300s; proxy_read_timeout 300s;
} }
# --- сборки: /<N>/... и /<имя>/... ---------------------------------------
# /29/index.html -> /opt/game/releases/29/index.html
# /sol/index.html -> /opt/game/releases/sol/index.html
# /29/Build/WebGL.data -> /opt/game/releases/29/Build/WebGL.data
location ~ ^/(?<build>[a-zA-Z0-9][a-zA-Z0-9_-]*)(?<subpath>/.*)?$ {
root /opt/game/releases;
try_files /$build$subpath /$build$subpath/ =404;
}
} }
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# Публикация готового zip-билда (например, присланного фронтендером) в ОТДЕЛЬНУЮ папку nginx.
#
# publish-zip.sh <имя-папки> <путь-к-zip>
#
# Имя папки может быть НЕ числовым (например `sol`) — такие сборки живут рядом с
# номерными релизами (/opt/game/releases/<N>/) и АКТИВНУЮ сборку НЕ трогают:
# корень продолжает редиректить на текущий номер, а эта сборка доступна по /<имя>/.
set -euo pipefail
NAME="${1:?использование: publish-zip.sh <имя-папки> <путь-к-zip>}"
ZIP="${2:?использование: publish-zip.sh <имя-папки> <путь-к-zip>}"
DEST="/opt/game/releases/${NAME}"
TMP="/tmp/unzip-${NAME}"
case "$NAME" in
*[!a-zA-Z0-9_-]*|'') echo "имя папки: только буквы/цифры/дефис/подчёркивание, получено: $NAME" >&2; exit 1 ;;
api|ws|solana-rpc) echo "имя '$NAME' занято служебным location — выбери другое" >&2; exit 1 ;;
esac
[ -f "$ZIP" ] || { echo "нет файла $ZIP" >&2; exit 1; }
command -v unzip >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unzip >/dev/null
echo "=== 1/3 распаковываю ${ZIP} ==="
rm -rf "$TMP"; mkdir -p "$TMP"
unzip -q "$ZIP" -d "$TMP"
# если в архиве одна вложенная папка — поднимаем содержимое наверх
CNT=$(find "$TMP" -mindepth 1 -maxdepth 1 | wc -l)
if [ "$CNT" = 1 ]; then
INNER="$(find "$TMP" -mindepth 1 -maxdepth 1)"
if [ -d "$INNER" ]; then
mv "$INNER"/* "$TMP"/ 2>/dev/null || true
rmdir "$INNER" 2>/dev/null || true
fi
fi
echo "=== 2/3 раскладываю в ${DEST} ==="
rm -rf "$DEST"; mkdir -p "$DEST"
cp -a "$TMP"/. "$DEST"/
chmod -R a+rX /opt/game/releases
rm -rf "$TMP"
[ -f "$DEST/index.html" ] || { echo "ВНИМАНИЕ: в сборке нет index.html — проверь архив" >&2; exit 1; }
echo "=== 3/3 проверка ==="
nginx -t >/dev/null && systemctl reload nginx
for u in "/${NAME}/" "/${NAME}/index.html"; do
printf ' %-46s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1${u}")"
done
echo "готово: https://smart-updown.binom.pw/${NAME}/"
du -sh "$DEST"
ls -la "$DEST"