feat(public): :test ↔ test.admin, :latest ↔ admin; HTTPS-safe redirects
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Workflow (.forgejo/workflows/docker-public.yml): - Собирает ДВА тега: :test с PUBLIC_APP_URL=https://test.admin.food-market.kz (для test.food-market.kz) и :latest с PUBLIC_APP_URL=https://admin.food- market.kz (для будущего food-market.kz). deploy_prod отключён намеренно: прод-public закрыт владельцем. - deploy_test: SSH на prod-vm, идемпотентно устанавливает PUBLIC_TAG=test в ~/food-market/deploy/.env, pull + up -d public. Smoke проверяет что /signup/ ссылается на test.admin и НЕ содержит admin.food-market.kz. nginx.conf (src/food-market.public/deploy/): - absolute_redirect off; port_in_redirect off; Было: curl -sI https://test.food-market.kz/signup → location: http://... (schema терялась на 301 /signup → /signup/). Стало: location: /signup/ (относительный, браузер держит https). Env-пример (.env.example) — комментарий, что для test-билда ставить test.admin и прод в коде оставить дефолтом в source-коде (SignupForm/ Header), но в билде всегда задавать явно через PUBLIC_APP_URL build-arg. RUNBOOK — новый раздел «Публичный сайт: какой образ ↔ какой домен» с таблицей и smoke-командой. Smoke проверил: - https://test.food-market.kz/signup → 301 на /signup/ (без схемы, https сохраняется). - https://test.food-market.kz/signup/ → ссылается на test.admin.food- market.kz; ни одного вхождения admin.food-market.kz (без test.). - POST /api/auth/signup с Origin test.food-market.kz → user создан в food-market-stage-postgres-1 (STAGE), не в food-market-postgres (прод- adjacent). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
eccdd06a4c
commit
4296236206
|
|
@ -11,72 +11,92 @@ on:
|
|||
|
||||
env:
|
||||
LOCAL_REGISTRY: 127.0.0.1:5001
|
||||
# Текущие production-домены (миграция со stage zat.kz, см. коммит 79406e3).
|
||||
# Публичный сайт = test.food-market.kz, админка/API = admin.food-market.kz.
|
||||
# Без актуальных значений CI собирал бандл с zat.kz и каждый push
|
||||
# перетирал латест-image, ломая prod (см. коммит 2a026c5).
|
||||
PUBLIC_SITE_URL: https://test.food-market.kz
|
||||
PUBLIC_APP_URL: https://admin.food-market.kz
|
||||
# Два целевых домена:
|
||||
# test.food-market.kz → test.admin.food-market.kz (стенд-админка) → tag :test
|
||||
# food-market.kz → admin.food-market.kz (прод-админка) → tag :latest
|
||||
# Публичный сайт — Astro static; PUBLIC_APP_URL инжектится в бандл при
|
||||
# pnpm build и используется в SignupForm/Header для ссылок на login+signup.
|
||||
# Прод-деплой пока ОТКЛЮЧЁН (food-market.kz закрыт владельцем до окончания
|
||||
# приёмки стенда). Собираем оба тега, но деплоим ТОЛЬКО :test.
|
||||
STAGE_SITE_URL: https://test.food-market.kz
|
||||
STAGE_APP_URL: https://test.admin.food-market.kz
|
||||
PROD_SITE_URL: https://food-market.kz
|
||||
PROD_APP_URL: https://admin.food-market.kz
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build + push Public
|
||||
name: Build + push Public (test + latest)
|
||||
runs-on: [self-hosted, linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build + push
|
||||
- name: Build + push :test (test.food-market.kz)
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
DOCKER_BUILDKIT: '1'
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg PUBLIC_SITE_URL=$PUBLIC_SITE_URL \
|
||||
--build-arg PUBLIC_APP_URL=$PUBLIC_APP_URL \
|
||||
--build-arg PUBLIC_SITE_URL=$STAGE_SITE_URL \
|
||||
--build-arg PUBLIC_APP_URL=$STAGE_APP_URL \
|
||||
-f src/food-market.public/Dockerfile \
|
||||
-t $LOCAL_REGISTRY/food-market-public:$SHA \
|
||||
-t $LOCAL_REGISTRY/food-market-public:latest \
|
||||
-t $LOCAL_REGISTRY/food-market-public:test \
|
||||
-t $LOCAL_REGISTRY/food-market-public:test-$SHA \
|
||||
src/food-market.public
|
||||
docker push $LOCAL_REGISTRY/food-market-public:$SHA
|
||||
docker push $LOCAL_REGISTRY/food-market-public:latest
|
||||
docker push $LOCAL_REGISTRY/food-market-public:test
|
||||
docker push $LOCAL_REGISTRY/food-market-public:test-$SHA
|
||||
|
||||
deploy:
|
||||
name: Deploy Public on stage
|
||||
- name: Build + push :latest (food-market.kz, пока не деплоится)
|
||||
env:
|
||||
SHA: ${{ github.sha }}
|
||||
DOCKER_BUILDKIT: '1'
|
||||
run: |
|
||||
docker build \
|
||||
--build-arg PUBLIC_SITE_URL=$PROD_SITE_URL \
|
||||
--build-arg PUBLIC_APP_URL=$PROD_APP_URL \
|
||||
-f src/food-market.public/Dockerfile \
|
||||
-t $LOCAL_REGISTRY/food-market-public:latest \
|
||||
-t $LOCAL_REGISTRY/food-market-public:$SHA \
|
||||
src/food-market.public
|
||||
docker push $LOCAL_REGISTRY/food-market-public:latest
|
||||
docker push $LOCAL_REGISTRY/food-market-public:$SHA
|
||||
|
||||
deploy_test:
|
||||
name: Deploy :test → test.food-market.kz
|
||||
needs: build
|
||||
runs-on: [self-hosted, linux]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update compose + .env
|
||||
# Compose + .env на prod-vm живёт в ~/food-market/deploy/. Контейнер
|
||||
# называется `food-market-public` (без -stage суффикса — исторически).
|
||||
# Пути прописаны прямо в SSH-команде, чтобы step не зависел от
|
||||
# workdir-настроек раннера.
|
||||
- name: Pull + recreate public (prod-vm)
|
||||
env:
|
||||
PGPASS: ${{ secrets.STAGE_POSTGRES_PASSWORD }}
|
||||
SSH: nns@192.168.1.190
|
||||
run: |
|
||||
cat > /home/nns/food-market-stage/deploy/.env <<ENV
|
||||
REGISTRY=127.0.0.1:5001
|
||||
API_TAG=latest
|
||||
WEB_TAG=latest
|
||||
PUBLIC_TAG=latest
|
||||
POSTGRES_PASSWORD=$PGPASS
|
||||
ENV
|
||||
cp deploy/docker-compose.yml /home/nns/food-market-stage/deploy/docker-compose.yml
|
||||
ssh "$SSH" "cd ~/food-market/deploy \
|
||||
&& grep -q '^PUBLIC_TAG=test$' .env || sed -i 's/^PUBLIC_TAG=.*/PUBLIC_TAG=test/' .env \
|
||||
&& docker compose pull public \
|
||||
&& docker compose up -d --no-deps --force-recreate public"
|
||||
|
||||
- name: Pull + recreate public only
|
||||
working-directory: /home/nns/food-market-stage/deploy
|
||||
run: |
|
||||
docker compose pull public
|
||||
docker compose up -d --no-deps public
|
||||
|
||||
- name: Smoke
|
||||
- name: Smoke — внешний домен отдаёт нужные ссылки
|
||||
run: |
|
||||
set -e
|
||||
for i in 1 2 3 4 5 6; do
|
||||
sleep 4
|
||||
if curl -fsS http://127.0.0.1:8082/ -o /dev/null; then
|
||||
echo "Public OK"
|
||||
exit 0
|
||||
if curl -fsSI https://test.food-market.kz/ -o /dev/null; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo "Public smoke failed"
|
||||
exit 1
|
||||
# Главная проверка: /signup и главная содержат ссылку на
|
||||
# test.admin.food-market.kz, а НЕ на admin.food-market.kz.
|
||||
body=$(curl -fsS https://test.food-market.kz/signup/)
|
||||
echo "$body" | grep -q 'test\.admin\.food-market\.kz/login' \
|
||||
|| { echo "signup не ссылается на test.admin — PUBLIC_APP_URL не прошёл в билд"; exit 1; }
|
||||
! echo "$body" | grep -qE 'href="https://admin\.food-market\.kz' \
|
||||
|| { echo "signup содержит ссылку на PROD admin — утечка"; exit 1; }
|
||||
echo "Public (test) OK — ссылки ведут на test.admin.food-market.kz"
|
||||
|
||||
- name: Notify Telegram on success
|
||||
if: success()
|
||||
|
|
@ -87,7 +107,7 @@ jobs:
|
|||
run: |
|
||||
curl -sS -X POST "https://api.telegram.org/bot$BOT/sendMessage" \
|
||||
--data-urlencode "chat_id=$CHAT" \
|
||||
--data-urlencode "text=✅ stage public deployed — ${SHA:0:7} → https://test.food-market.kz" \
|
||||
--data-urlencode "text=✅ public:test deployed — ${SHA:0:7} → https://test.food-market.kz (links → test.admin)" \
|
||||
> /dev/null
|
||||
|
||||
- name: Notify Telegram on failure
|
||||
|
|
@ -99,5 +119,13 @@ jobs:
|
|||
run: |
|
||||
curl -sS -X POST "https://api.telegram.org/bot$BOT/sendMessage" \
|
||||
--data-urlencode "chat_id=$CHAT" \
|
||||
--data-urlencode "text=❌ stage public deploy FAILED — ${SHA:0:7}" \
|
||||
--data-urlencode "text=❌ public:test deploy FAILED — ${SHA:0:7}" \
|
||||
> /dev/null
|
||||
|
||||
# Прод-деплой :latest на food-market.kz — ОТКЛЮЧЁН намеренно.
|
||||
# Включать, когда владелец разрешит открыть прод-public:
|
||||
# - добавить job deploy_prod (аналогичный deploy_test), SSH на prod,
|
||||
# PUBLIC_TAG=latest в прод-compose;
|
||||
# - проверить, что ссылки ведут на admin.food-market.kz;
|
||||
# - смотреть DNS: food-market.kz должен указывать на prod-vm.
|
||||
# До тех пор build:latest есть, но висит в registry без деплоя.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,35 @@
|
|||
**Важно**: проект `docker compose` называется `food-market-stage`
|
||||
(флаг `-p food-market-stage`). См. инцидент ниже про project name.
|
||||
|
||||
## Публичный сайт: какой образ ↔ какой домен
|
||||
|
||||
Публичный Astro-сайт собирается в **двух** вариантах — build-arg
|
||||
`PUBLIC_APP_URL` (куда уводит кнопка «Войти» и форма регистрации) инжектит
|
||||
Astro в бандл при `pnpm build`, поэтому один образ работает для ровно
|
||||
одного `admin`-хоста. Нельзя переиспользовать прод-образ для стенда и
|
||||
наоборот.
|
||||
|
||||
| Образ (`food-market-public`) | `PUBLIC_SITE_URL` | `PUBLIC_APP_URL` | Хост-назначение | Деплой |
|
||||
|---|---|---|---|---|
|
||||
| `:test` + `:test-<sha>` | `https://test.food-market.kz` | `https://test.admin.food-market.kz` | **test.food-market.kz** (стенд публики) | **да** — в CI `.forgejo/workflows/docker-public.yml` job `deploy_test` обновляет контейнер `food-market-public` на prod-vm через `~/food-market/deploy/.env` c `PUBLIC_TAG=test` |
|
||||
| `:latest` + `:<sha>` | `https://food-market.kz` | `https://admin.food-market.kz` | **food-market.kz** (прод публики) | **нет** — собирается, пушится, но на хост не деплоится; food-market.kz закрыт по решению владельца. Включать job `deploy_prod` когда разрешат. |
|
||||
|
||||
Workflow строит оба тега при каждом push в main, затрагивающем
|
||||
`src/food-market.public/**`. Выбор образа на хосте через `PUBLIC_TAG` в
|
||||
`~/food-market/deploy/.env` на prod-vm. Если `.env` отредактировали
|
||||
руками (например, координатор сменил `PUBLIC_TAG=latest → test`), в
|
||||
следующем CI-прогоне workflow проверит и не трогает, если уже `test`.
|
||||
|
||||
Smoke-проверка (jobs `deploy_test` → step «Smoke»):
|
||||
`curl https://test.food-market.kz/signup/ | grep test.admin` должен
|
||||
находить ссылки на `test.admin.food-market.kz`, и НЕ должен находить
|
||||
`admin.food-market.kz`. Если ссылки не те — PUBLIC_APP_URL не прошёл в
|
||||
билд, образ пересобрать.
|
||||
|
||||
Nginx внутри контейнера сделан с `absolute_redirect off; port_in_redirect
|
||||
off;` — иначе 301 с `/signup` на `/signup/` строился через HTTP-schema и
|
||||
браузер уходил с https → http. См. `src/food-market.public/deploy/nginx.conf`.
|
||||
|
||||
## Бэкап и восстановление
|
||||
|
||||
### Расписание
|
||||
|
|
|
|||
84
docs/public-test-url-progress.md
Normal file
84
docs/public-test-url-progress.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Public test URL — прогресс (5/5 ✓)
|
||||
|
||||
Фикс: `https://test.food-market.kz` (публичный сайт) должен вести на
|
||||
`https://test.admin.food-market.kz` (стенд-админку), а не на прод
|
||||
`admin.food-market.kz`.
|
||||
|
||||
Координатор уже вручную пересобрал образ `food-market-public:test` с
|
||||
правильным `PUBLIC_APP_URL` и переключил контейнер (`PUBLIC_TAG=test` в
|
||||
`~/food-market/deploy/.env` на prod-vm). Теперь это закреплено в репе,
|
||||
автосборка CI не откатит.
|
||||
|
||||
## Контекст (prod-vm 192.168.1.190)
|
||||
|
||||
- `~/food-market/deploy/` — публичный сайт **test.food-market.kz** раздаёт
|
||||
контейнер `food-market-public:test`. API/web тоже тут, но на отдельных
|
||||
портах; prod-API 2 месяца `Exited`.
|
||||
- `~/food-market-stage/deploy/` — stage-стек (admin API/web) с тегом
|
||||
`:stage`.
|
||||
|
||||
## Чек-лист
|
||||
|
||||
- [x] 1. `.forgejo/workflows/docker-public.yml`: сборка `:test`-тега с
|
||||
`PUBLIC_APP_URL=https://test.admin.food-market.kz`, деплой апдейтит именно
|
||||
его. Прод-сборка `:latest` → `admin.food-market.kz` собирается и пушится,
|
||||
но job `deploy_prod` ОТСУТСТВУЕТ (food-market.kz закрыт решением
|
||||
владельца; включать когда разрешат).
|
||||
Smoke в CI: `curl .../signup/ | grep test.admin` должен найти,
|
||||
`grep -E 'href="https://admin\.food-market\.kz'` НЕ должен найти.
|
||||
- [x] 2. Нет захардкоженных `admin.food-market.kz` в `src/food-market.public`
|
||||
кроме env-fallback'ов в `Header.astro` и `SignupForm.tsx`
|
||||
(`?? 'https://admin.food-market.kz'`). Fallback активен только когда
|
||||
env-переменная не задана — для автосборки через workflow она ЕСТЬ
|
||||
всегда. `.env.example` обновлён с явным комментарием про test vs prod.
|
||||
- [x] 3. Smoke выполнил:
|
||||
- `curl https://test.food-market.kz/` → ссылка «Войти» ведёт на
|
||||
`https://test.admin.food-market.kz/login`.
|
||||
- `curl https://test.food-market.kz/signup/` → все `href`/`action`
|
||||
указывают на `test.admin.food-market.kz`. Нет ни одного `admin.food-
|
||||
market.kz` (без `test.` префикса).
|
||||
- `POST /api/auth/signup` c `Origin: https://test.food-market.kz`:
|
||||
юзер `publicsmoke-1791116018@food-market.local` создан в
|
||||
**food-market-stage-postgres-1** (STAGE-БД). В
|
||||
`food-market-postgres` (прод-adjacent) этого юзера НЕТ.
|
||||
- [x] 4. `docs/RUNBOOK.md` — добавлен раздел «Публичный сайт: какой
|
||||
образ ↔ какой домен» с таблицей `:test` ↔ `test.food-market.kz` /
|
||||
`test.admin.food-market.kz` и `:latest` ↔ `food-market.kz` / `admin.
|
||||
food-market.kz`, и note'ом про `absolute_redirect off`.
|
||||
- [x] 5. `src/food-market.public/deploy/nginx.conf`:
|
||||
- `absolute_redirect off;` — Location в 301 становится относительным
|
||||
(`/signup/` вместо `http://test.food-market.kz/signup/`); браузер
|
||||
использует текущий scheme → https сохраняется.
|
||||
- `port_in_redirect off;` — на случай cross-port сценариев.
|
||||
Проверено: `curl -sI https://test.food-market.kz/signup` → теперь
|
||||
`location: /signup/` без схемы. Отбились от скатывания на http.
|
||||
|
||||
## Что в коде
|
||||
|
||||
| Что | Где |
|
||||
|---|---|
|
||||
| Workflow (два build + один deploy) | `.forgejo/workflows/docker-public.yml` |
|
||||
| nginx-config публичного образа | `src/food-market.public/deploy/nginx.conf` |
|
||||
| Dockerfile с ARG | `src/food-market.public/Dockerfile` |
|
||||
| Env-fallback'и к admin-URL | `src/food-market.public/src/components/{Header.astro,SignupForm.tsx}` (без изменений — работают через PUBLIC_APP_URL) |
|
||||
| Env-пример | `src/food-market.public/.env.example` (комментарий про test vs prod) |
|
||||
| RUNBOOK раздел | `docs/RUNBOOK.md` «Публичный сайт: какой образ ↔ какой домен» |
|
||||
|
||||
## Проверка после деплоя
|
||||
|
||||
```bash
|
||||
curl -sI https://test.food-market.kz/signup | grep -i location
|
||||
# location: /signup/ ← без http://; значит absolute_redirect off работает
|
||||
|
||||
curl -s https://test.food-market.kz/signup/ | grep -oE 'href="[^"]*login[^"]*"' | sort -u
|
||||
# href="https://test.admin.food-market.kz/login"
|
||||
# href="/signup"
|
||||
|
||||
curl -s https://test.food-market.kz/signup/ | grep -oE 'admin\.food-market\.kz'
|
||||
# (пусто — ни одного вхождения прод-admin)
|
||||
```
|
||||
|
||||
Прод (`admin.food-market.kz`) и `food-market.kz` НЕ ТРОНУТЫ. Прод-API
|
||||
по-прежнему остановлен ~2 месяца (это пре-существующий outage, отдельная
|
||||
задача). Prod-БД `food-market-postgres` не получила ни одного запроса
|
||||
от публичного сайта.
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
# Публичный URL самого сайта (canonical, OG, sitemap)
|
||||
PUBLIC_SITE_URL=https://test.food-market.kz
|
||||
|
||||
# Админка / API — куда уходит юзер после signup, и куда шлются POST /api/auth/signup
|
||||
PUBLIC_APP_URL=https://food-market.zat.kz
|
||||
# Админка / API — куда уходит юзер после signup, и куда шлются POST /api/auth/signup.
|
||||
# ⚠ Для test-сборки (образ food-market-public:test) → https://test.admin.food-market.kz
|
||||
# Для прод-сборки (:latest) → https://admin.food-market.kz
|
||||
# Workflow docker-public.yml собирает оба тега с соответствующими значениями.
|
||||
PUBLIC_APP_URL=https://test.admin.food-market.kz
|
||||
|
|
|
|||
|
|
@ -4,6 +4,20 @@ server {
|
|||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Контейнер стоит ЗА внешним nginx на prod-vm (host-уровень терминирует
|
||||
# https). Внутри контейнера мы получаем plain HTTP, но автогенеренные
|
||||
# редиректы вроде /signup → /signup/ попадают в body `Location:` с
|
||||
# scheme http:// — браузер уходит на http://test.food-market.kz/signup/
|
||||
# и теряет https. absolute_redirect off; отключает ВСЕ абсолютные URL в
|
||||
# 301/302 — Location становится относительным ('/signup/'), а значит
|
||||
# браузер сам поставит текущий scheme (https). Решение описано в
|
||||
# https://nginx.org/r/absolute_redirect .
|
||||
absolute_redirect off;
|
||||
# Чтобы cm:X-Forwarded-Proto из апстримного nginx учитывался, если
|
||||
# когда-нибудь понадобится явный scheme — но дефолт absolute_redirect off
|
||||
# этого достаточно.
|
||||
port_in_redirect off;
|
||||
|
||||
# Astro static — gzip + длинный кэш для assets с хешами в имени.
|
||||
gzip on;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml text/plain;
|
||||
|
|
|
|||
Loading…
Reference in a new issue