diff --git a/.forgejo/workflows/docker-public.yml b/.forgejo/workflows/docker-public.yml index ebc2929..47fd5cb 100644 --- a/.forgejo/workflows/docker-public.yml +++ b/.forgejo/workflows/docker-public.yml @@ -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 < /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 без деплоя. diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 73af0f9..ff1e996 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -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-` | `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` + `:` | `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`. + ## Бэкап и восстановление ### Расписание diff --git a/docs/public-test-url-progress.md b/docs/public-test-url-progress.md new file mode 100644 index 0000000..b210033 --- /dev/null +++ b/docs/public-test-url-progress.md @@ -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` не получила ни одного запроса +от публичного сайта. diff --git a/src/food-market.public/.env.example b/src/food-market.public/.env.example index ed8a18b..cb8a52d 100644 --- a/src/food-market.public/.env.example +++ b/src/food-market.public/.env.example @@ -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 diff --git a/src/food-market.public/deploy/nginx.conf b/src/food-market.public/deploy/nginx.conf index 70a9245..838a030 100644 --- a/src/food-market.public/deploy/nginx.conf +++ b/src/food-market.public/deploy/nginx.conf @@ -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;