diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index cb55d07..f52e985 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -258,3 +258,38 @@ пересборку образа на каждый билд игры и не решает вопрос кэша; (б) поднять в LXC не nginx, а podman-контейнер с nginx — лишний слой на 10 строках конфига; (в) отдавать статику прямо с k3s-Traefik по PVC — не то, о чём просил владелец (он хотел nginx на проксмоксе). + +## ADR-011-доп — корень редиректит на активную сборку (закрыта кэш-мина ADR-010) (2026-09-15) + +**Что нашли на сборке 29 (проверка живьём, не по документам).** `index.html` Unity ссылается на +ассеты **относительным** путём (`var buildUrl = "Build"`). Пока корень отдавал файлы напрямую из +`/opt/game/current`, игрок получал ассеты по URL **без номера сборки** — `/Build/WebGL.wasm`, +`/Build/WebGL.data` и т.д. Эти URL между релизами **не меняются**, а в конфиге они попадали под +маску `immutable` (7 суток). Итог: у игравшего игрока браузер неделю держал бы ассеты старой сборки +и молча игнорировал новый билд. То есть моя собственная маска кэша воспроизводила ровно ту мину, +которую ADR-011 объявлял закрытой. Версионирование каталогами **само по себе** её не лечило — +лечило только то, чтобы URL ассетов реально содержали номер сборки. + +**Решение.** Корень больше не раздаёт файлы — он **302-редиректит на активную сборку**: +- `publish-release.sh --activate` пишет `/etc/nginx/game-active.inc`: + `location = / { return 302 //; }` + то же для `/index.html`; +- `include /etc/nginx/game-active.inc;` подключён в server-блок; +- после редиректа `location.pathname` = `/29/`, относительный `buildUrl = "Build"` резолвится + в `/29/Build/…` — версионированный URL, старый URL перестаёт использоваться вовсе; +- маска `immutable` сужена до путей **с номером сборки** (`~^/[0-9]+/…`); всё остальное, + включая неверсионированные ассеты, теперь `no-store` (страховка от отравления кэша). + +**Проверено живьём (сборка 29):** `/` → **302 → `/29/`**; браузер приземляется на `/29/` и тянет +`/29/Build/WebGL.wasm`, `/29/Build/WebGL.data`, `/29/Build/WebGL.framework.js` — все 200, ни одной +ошибки в консоли; `/28/`,`/27/` живы; неверсионированный `/Build/WebGL.wasm` → 200 с `cache-control: +no-store`; `/api/state`,`/api/price`,`/solana-rpc/` → 200; сквозная ставка `autoclose-check` — **9/9**; +`testfront` не задет (200). + +**Почему не «отдавать корень без редиректа».** Это работало бы (неверсионированные ассеты получили бы +`no-store`), но теряется весь смысл `immutable`-кэширования: ассеты в 52 МБ + 18 МБ перекачивались бы +при каждом заходе. Редирект даёт и корректность, и кэш. + +**Остаточный риск.** Игрок, у которого в кэше уже лежат ассеты по **старому** неверсионированному URL +(`/Build/WebGL.wasm`, полученные до этого фикса), один раз может получить старый билд — URL теперь +`no-store`, так что после первой перезагрузки браузер сходит на сервер и получит свежее. Новые +версии этой проблемы не имеют вовсе. diff --git a/tools/proxmox-nginx/README.md b/tools/proxmox-nginx/README.md index 4f39f27..0a0c792 100644 --- a/tools/proxmox-nginx/README.md +++ b/tools/proxmox-nginx/README.md @@ -49,8 +49,16 @@ cd ~/WORK/games/pump-game-ops && BASE=https://smart-updown.binom.pw/api \ относительным путём (`Build/WebGL.loader.js`), поэтому без слэша `Build/...` уедет в корень → 404. - **`skopeo ... dir:` пишет OCI-манифест**, где `layers` — словари. `unpack-release.sh` берёт `digest` каждого слоя и распаковывает по имени файла `` (не `sha256:...`). -- **Кэш больше не прилипает между релизами**: URL содержит номер сборки → `immutable` корректен. - Внутри версии каталог не перезаписывается, поэтому ассеты действительно неизменяемы. +- **Корень — 302 на активную сборку** (`location = / { return 302 //; }` в + `/etc/nginx/game-active.inc`, пишет `publish-release.sh --activate`). Нужен потому, что + `index.html` Unity зовёт ассеты **относительным** путём (`buildUrl = "Build"`): без редиректа они + запрашивались бы как `/Build/WebGL.wasm` — без номера сборки, а такой URL между релизами НЕ + меняется → `immutable` заставлял браузер неделю отдавать старый билд. С редиректом путь становится + `/29/Build/…` и кэш корректен. +- **`immutable` только для путей с номером сборки** (`~^/[0-9]+/…`). Всё остальное, включая + неверсионированные ассеты, — `no-store` (страховка от отравления кэша). +- ⚠️ Если правишь `game.nginx.conf` — не забудь `include /etc/nginx/game-active.inc;`, иначе корень + снова начнёт раздавать файлы напрямую и мина вернётся. - **Сборки копятся** (~68 МБ на версию) — ретеншен пока ручной, см. `TASKS.md`. - **Откат схемы целиком**: вернуть `/opt/traefik/static/smart-updown.yaml` из `/root/bak-smart-updown-nginx-2026-09-15/` на VDS — домен снова пойдёт целиком в куб. diff --git a/tools/proxmox-nginx/game.nginx.conf b/tools/proxmox-nginx/game.nginx.conf index edc4f88..9cb18c4 100644 --- a/tools/proxmox-nginx/game.nginx.conf +++ b/tools/proxmox-nginx/game.nginx.conf @@ -5,9 +5,18 @@ # /opt/game/releases// — файлы сборки N (Build/, TemplateData/, index.html) # /opt/game/current -> releases/ — активная сборка (симлинк, переключает publish-release.sh) # -# https://smart-updown.binom.pw/ -> активная сборка (current) -# https://smart-updown.binom.pw/28/ -> конкретная сборка 28 -# https://smart-updown.binom.pw/28/Build/WebGL.data -> ассет сборки 28 +# https://smart-updown.binom.pw/ -> 302 на активную сборку (/29/) +# https://smart-updown.binom.pw/29/ -> конкретная сборка 29 +# https://smart-updown.binom.pw/29/Build/WebGL.data -> ассет сборки 29 +# +# ⚠️ ПОЧЕМУ КОРЕНЬ РЕДИРЕКТИТ, А НЕ ОТДАЁТ ФАЙЛЫ: +# index.html сборки ссылается на ассеты ОТНОСИТЕЛЬНЫМ путём (`buildUrl = "Build"`). +# Если отдавать его прямо с корня, ассеты запрашиваются как /Build/WebGL.wasm — БЕЗ номера +# сборки. Такой URL между релизами НЕ меняется, а мы отдаём его `immutable` на 7 суток → +# браузер игрока неделю держит ассеты старой сборки и игнорирует новый билд (мина из ADR-010). +# При редиректе на // все ассеты получают версионированный URL (/29/Build/...), старый URL +# просто перестаёт использоваться, и `immutable` становится корректным. +# Номер активной сборки пишет publish-release.sh в отдельный include-файл /etc/nginx/game-active.inc. # # API и WebSocket nginx НЕ обслуживает: их забирает Traefik на VDS # (router smart-updown-api: PathPrefix /api,/ws -> куб 192.168.76.120). @@ -15,11 +24,10 @@ # которые всё же дойдут до nginx (прямой заход по IP, старые сборки). # Ассеты Unity внутри каталога версии неизменяемы -> immutable. -# index.html и всё остальное -> no-store. +# Всё, что НЕ под номером сборки, и index.html -> 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"; - ~^/.+\.(js|wasm|data|mem|symbols|css|png|jpg|jpeg|gif|svg|woff2?)$ "public, max-age=604800, immutable"; } server { @@ -39,60 +47,55 @@ server { add_header Cache-Control $game_cache_control; + # --- корень: 302 на активную сборку --------------------------------------- + # Файл переписывает publish-release.sh при --activate. + include /etc/nginx/game-active.inc; + # --- версионированные сборки: /<число>/... ------------------------------- - # /28/index.html -> /opt/game/releases/28/index.html + # /29/index.html -> /opt/game/releases/29/index.html location ~ ^/(?[0-9]+)(?/.*)?$ { root /opt/game/releases; try_files /$build$subpath /$build$subpath/ =404; } - # --- ассеты активной сборки ---------------------------------------------- - location ~* \.(js|wasm|data|mem|symbols|json|css|png|jpg|jpeg|gif|svg|woff2?)$ { - root /opt/game/current; - try_files $uri =404; - } - # --- страховка: API ------------------------------------------------------- # Нормальный /api идёт мимо nginx (Traefik VDS -> куб). Если запрос всё же # пришёл сюда — уходим в куб на k3s-Traefik с правильным Host, дальше # ingress smart-updown-api ведёт в шлюз :8895. location /api { proxy_pass http://192.168.76.120; - proxy_http_version 1.1; proxy_set_header Host smart-updown.binom.pw; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_connect_timeout 10s; - proxy_read_timeout 90s; - } - - # страховка: WebSocket шлюза - location /ws { - proxy_pass http://192.168.76.120; + proxy_set_header X-Forwarded-Proto https; proxy_http_version 1.1; - proxy_set_header Host smart-updown.binom.pw; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; + proxy_buffering off; } - # страховка: тонкий CORS-прокси до Solana RPC (старые сборки игры) + # --- страховка: WebSocket ------------------------------------------------- + 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; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + proxy_buffering off; + } + + # --- страховка: Solana RPC ------------------------------------------------- + # /solana-rpc/ НЕ ловится PathPrefix /api на VDS, поэтому уходит в корневой + # router, т.е. сюда. Старые сборки игры стучатся именно в этот путь. location /solana-rpc/ { proxy_pass http://192.168.76.181:8899/; + proxy_set_header Host $host; proxy_http_version 1.1; - proxy_set_header Host 192.168.76.181:8899; - proxy_connect_timeout 5s; - proxy_read_timeout 30s; - add_header Access-Control-Allow-Origin "*" always; - add_header Access-Control-Allow-Headers "content-type" always; - add_header Access-Control-Allow-Methods "POST, OPTIONS" always; - if ($request_method = OPTIONS) { return 204; } - } - - # --- корень = активная сборка -------------------------------------------- - location / { - try_files $uri $uri/ /index.html; + proxy_read_timeout 300s; } } diff --git a/tools/proxmox-nginx/publish-release.sh b/tools/proxmox-nginx/publish-release.sh index 9125604..648d72c 100644 --- a/tools/proxmox-nginx/publish-release.sh +++ b/tools/proxmox-nginx/publish-release.sh @@ -7,10 +7,11 @@ # 1. тянет образ images.binom.pw/fun/game-front2:<тег> из реестра (skopeo copy в dir); # 2. распаковывает статику в /opt/game/releases/<тег>/; # 3. без --activate оставляет активной текущую сборку (корень не меняется); -# с --activate переключает симлинк /opt/game/current на новую версию; +# с --activate переключает симлинк /opt/game/current на новую версию +# И пишет /etc/nginx/game-active.inc (редирект корня на /<тег>/); # 4. печатает проверку. # -# Итог: https://smart-updown.binom.pw/ -> активная сборка +# Итог: https://smart-updown.binom.pw/ -> 302 на активную сборку # https://smart-updown.binom.pw/<тег>/ -> конкретная сборка set -euo pipefail @@ -19,6 +20,11 @@ ACTIVATE="${2:-}" REG="images.binom.pw/fun/game-front2" CREDS="subochev:Drovosek319" IMG="/tmp/gameimg-${TAG}" +ACTIVE_INC="/etc/nginx/game-active.inc" + +case "$TAG" in + ''|*[!0-9]*) echo "тег должен быть числом (номер сборки), получено: $TAG" >&2; exit 1 ;; +esac echo "=== 1/4 тяну образ ${REG}:${TAG} ===" rm -rf "$IMG" @@ -34,14 +40,28 @@ if [ "$ACTIVATE" = "--activate" ]; then PREV="$(readlink -f /opt/game/current || echo none)" rm -f /opt/game/current ln -s "/opt/game/releases/${TAG}" /opt/game/current + + # корень редиректим на /<тег>/: тогда ассеты получают версионированный URL + # и immutable-кэш перестаёт «прилипать» между релизами (мина из ADR-010). + cat > "$ACTIVE_INC" < /${TAG}/ (${ACTIVE_INC})" else echo "=== 3/4 активную сборку НЕ трогаю (нет --activate) ===" echo "активна: $(readlink -f /opt/game/current)" fi echo "=== 4/4 проверка ===" +nginx -t systemctl reload nginx for u in "/" "/${TAG}/" "/${TAG}/index.html" "/${TAG}/Build/WebGL.framework.js"; do printf ' %-42s %s\n' "$u" "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1${u}")" diff --git a/tools/wait-ci-run.sh b/tools/wait-ci-run.sh new file mode 100755 index 0000000..336cd2b --- /dev/null +++ b/tools/wait-ci-run.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Ждём завершения последнего прогона CI fun-game-front2 и печатаем итог + УРЛ образа. +AUTH="-u subochev:Drovosek319" +BASE="https://git.binom.pw/api/v1" +REPO="subochev/fun-game-front2" + +for i in $(seq 1 120); do + OUT=$(curl -s $AUTH "$BASE/repos/$REPO/actions/runs?limit=1") + ST=$(echo "$OUT" | python3 -c "import sys,json;r=json.load(sys.stdin)['workflow_runs'][0];print(r['id'],r['run_number'],r['status'],r.get('conclusion'))") + set -- $ST + RUN_ID=$1; NUM=$2; STATUS=$3; CONC=$4 + echo "$(date +%H:%M:%S) run #$NUM статус=$STATUS вывод=$CONC" + if [ "$STATUS" = "completed" ]; then + echo "=== ИТОГ run #$NUM: $CONC ===" + if [ "$CONC" != "success" ]; then + echo "--- хвост лога провалившихся шагов ---" + curl -s $AUTH "$BASE/repos/$REPO/actions/runs/$RUN_ID/jobs" | python3 -c " +import sys,json +for j in json.load(sys.stdin).get('jobs',[]): + print('job:', j['name'], '|', j.get('conclusion')) + for s in j.get('steps',[]): + if s.get('conclusion') not in ('success','skipped',None): + print(' FAILED step:', s['name'], s.get('conclusion')) +" + fi + exit 0 + fi + sleep 30 +done +echo "=== ТАЙМАУТ ожидания (60 мин) ===" +exit 1