From b9799bf091c6817dcfd11b6209a1e1044b49c789 Mon Sep 17 00:00:00 2001 From: Porfiry Date: Tue, 15 Sep 2026 19:29:54 +0300 Subject: [PATCH] =?UTF-8?q?ADR-012:=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20(zip)=20=D1=80=D1=8F=D0=B4=D0=BE=D0=BC=20=D1=81=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D0=B5=D1=80=D0=BD=D1=8B=D0=BC=D0=B8=20?= =?UTF-8?q?=E2=80=94=20/sol/;=20^~=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BB?= =?UTF-8?q?=D1=83=D0=B6=D0=B5=D0=B1=D0=BD=D1=8B=D1=85=20=D0=BF=D1=83=D1=82?= =?UTF-8?q?=D0=B5=D0=B9;=20=D0=B4=D0=B2=D0=B0=20=D0=B4=D0=B5=D1=84=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B0=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=84=D1=80=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TASKS.md | 24 +++++++++++++ docs/DECISIONS.md | 46 ++++++++++++++++++++++++ tools/proxmox-nginx/README.md | 26 ++++++++++++++ tools/proxmox-nginx/game.nginx.conf | 42 +++++++++++++--------- tools/proxmox-nginx/publish-zip.sh | 54 +++++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 tools/proxmox-nginx/publish-zip.sh diff --git a/TASKS.md b/TASKS.md index 0df43a1..3a1eda7 100644 --- a/TASKS.md +++ b/TASKS.md @@ -226,3 +226,27 @@ curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' https://smart-updown **TODO:** - [ ] `[pump.meta]` Автоматизировать шаг 3 (сейчас ручная команда после CI). - [ ] `[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 отдаётся корректно. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index f52e985..8dd2b74 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -293,3 +293,49 @@ no-store`; `/api/state`,`/api/price`,`/solana-rpc/` → 200; сквозная с (`/Build/WebGL.wasm`, полученные до этого фикса), один раз может получить старый билд — URL теперь `no-store`, так что после первой перезагрузки браузер сходит на сервер и получит свежее. Новые версии этой проблемы не имеют вовсе. + +## ADR-012 — Именованные сборки (zip от фронтендера) рядом с номерными: `/sol/` (2026-09-15) + +**Контекст.** Фронтендер прислал готовый Unity-WebGL-билд **zip-архивом** — вне релизного цикла Gitea +(не через CI, без образа в реестре, без номера сборки). Владелец: «создай там сам папку и выложи». + +**Решение.** Каталоги на nginx теперь двух видов, живут рядом: +- `/opt/game/releases//` — номерные релизы из CI/реестра (`1`, `2`, … `29`); +- `/opt/game/releases/<имя>/` — именованные сборки из zip (`sol`). + +**Именованные сборки АКТИВНУЮ не трогают**: `current` остаётся на номере, корень по-прежнему +редиректит на `/29/`. Именованная доступна только явной ссылкой `/<имя>/`. + +**Изменения в nginx** (`game.nginx.conf`): +- regex-локейшн сборок расширен с чисел до имён: `^/(?[a-zA-Z0-9][a-zA-Z0-9_-]*)(?/.*)?$`; +- ⚠️ служебные пути получили префикс **`^~`** (`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 отдаётся корректно. diff --git a/tools/proxmox-nginx/README.md b/tools/proxmox-nginx/README.md index 0a0c792..d3c9f75 100644 --- a/tools/proxmox-nginx/README.md +++ b/tools/proxmox-nginx/README.md @@ -81,3 +81,29 @@ supported on this GPU` и Unity-исключение из wasm. **Это арт ⚠️ `publish-release.sh` содержит креды реестра (`subochev:…`) в открытом виде — файл живёт только внутри LXC 152 как root (`/opt/game/tools/`), в git лежит шаблон из репы ops. Не тиражировать наружу. + +## Именованные сборки (zip от фронтендера) — рядом с номерными + +Кроме номерных релизов из CI (`/opt/game/releases//`) на 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-локейшн + сборок `^/(?[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`) — скрипт это проверяет. diff --git a/tools/proxmox-nginx/game.nginx.conf b/tools/proxmox-nginx/game.nginx.conf index 9cb18c4..f2758fe 100644 --- a/tools/proxmox-nginx/game.nginx.conf +++ b/tools/proxmox-nginx/game.nginx.conf @@ -1,12 +1,14 @@ # nginx для раздачи Unity-сборок «Бинарной ракеты» — LXC 152 (game-web, 192.168.76.152) # Файл: /etc/nginx/sites-available/game # -# Модель версионирования: -# /opt/game/releases// — файлы сборки N (Build/, TemplateData/, index.html) +# Модель версионирования (два вида сборок, живут рядом): +# /opt/game/releases// — номерные релизы (1, 2, 3, … 29) из CI/реестра +# /opt/game/releases/<имя>/ — именованные сборки (например `sol`), присланные zip'ом # /opt/game/current -> releases/ — активная сборка (симлинк, переключает publish-release.sh) # # 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 # # ⚠️ ПОЧЕМУ КОРЕНЬ РЕДИРЕКТИТ, А НЕ ОТДАЁТ ФАЙЛЫ: @@ -18,16 +20,20 @@ # просто перестаёт использоваться, и `immutable` становится корректным. # Номер активной сборки пишет publish-release.sh в отдельный include-файл /etc/nginx/game-active.inc. # +# ⚠️ ПОРЯДОК LOCATION. Служебные пути (/api, /ws, /solana-rpc) объявлены с префиксом `^~`, +# потому что regex-локейшн сборок `^/(?…)` иначе перехватил бы их (regex в nginx +# приоритетнее обычного префикса). `^~` подавляет проверку regex для этого префикса. +# # API и WebSocket nginx НЕ обслуживает: их забирает Traefik на VDS # (router smart-updown-api: PathPrefix /api,/ws -> куб 192.168.76.120). # Локальные location /api, /ws, /solana-rpc — страховка для запросов, # которые всё же дойдут до nginx (прямой заход по IP, старые сборки). -# Ассеты Unity внутри каталога версии неизменяемы -> immutable. -# Всё, что НЕ под номером сборки, и index.html -> no-store. +# Ассеты Unity внутри НОМЕРНОГО каталога неизменяемы -> immutable. +# Всё остальное (включая именованные сборки: их папку могут перезалить) -> no-store. map $uri $game_cache_control { 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 { @@ -51,18 +57,11 @@ server { # Файл переписывает publish-release.sh при --activate. include /etc/nginx/game-active.inc; - # --- версионированные сборки: /<число>/... ------------------------------- - # /29/index.html -> /opt/game/releases/29/index.html - location ~ ^/(?[0-9]+)(?/.*)?$ { - root /opt/game/releases; - try_files /$build$subpath /$build$subpath/ =404; - } - - # --- страховка: API ------------------------------------------------------- + # --- страховка: API (^~ — приоритетнее regex-локейшна сборок) ------------- # Нормальный /api идёт мимо nginx (Traefik VDS -> куб). Если запрос всё же # пришёл сюда — уходим в куб на k3s-Traefik с правильным Host, дальше # ingress smart-updown-api ведёт в шлюз :8895. - location /api { + location ^~ /api { proxy_pass http://192.168.76.120; proxy_set_header Host smart-updown.binom.pw; proxy_set_header X-Real-IP $remote_addr; @@ -76,7 +75,7 @@ server { } # --- страховка: WebSocket ------------------------------------------------- - location /ws { + location ^~ /ws { proxy_pass http://192.168.76.120; proxy_set_header Host smart-updown.binom.pw; proxy_set_header X-Real-IP $remote_addr; @@ -92,10 +91,19 @@ server { # --- страховка: Solana RPC ------------------------------------------------- # /solana-rpc/ НЕ ловится PathPrefix /api на VDS, поэтому уходит в корневой # router, т.е. сюда. Старые сборки игры стучатся именно в этот путь. - location /solana-rpc/ { + location ^~ /solana-rpc/ { proxy_pass http://192.168.76.181:8899/; proxy_set_header Host $host; proxy_http_version 1.1; proxy_read_timeout 300s; } + + # --- сборки: //... и /<имя>/... --------------------------------------- + # /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 ~ ^/(?[a-zA-Z0-9][a-zA-Z0-9_-]*)(?/.*)?$ { + root /opt/game/releases; + try_files /$build$subpath /$build$subpath/ =404; + } } diff --git a/tools/proxmox-nginx/publish-zip.sh b/tools/proxmox-nginx/publish-zip.sh new file mode 100644 index 0000000..aaecd6a --- /dev/null +++ b/tools/proxmox-nginx/publish-zip.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Публикация готового zip-билда (например, присланного фронтендером) в ОТДЕЛЬНУЮ папку nginx. +# +# publish-zip.sh <имя-папки> <путь-к-zip> +# +# Имя папки может быть НЕ числовым (например `sol`) — такие сборки живут рядом с +# номерными релизами (/opt/game/releases//) и АКТИВНУЮ сборку НЕ трогают: +# корень продолжает редиректить на текущий номер, а эта сборка доступна по /<имя>/. +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"