From a07da68a3736e01fdc1422cead303925a5ad1e85 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 6 Aug 2026 22:23:46 +0500 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=B9=20Postgres=20=E2=80=94=20=D1=81=D0=B2=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80,=20?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20`services:`=20=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D1=83=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Первая версия (#2745, прогон 6853) упала, и упала полезно: `services:` + `ports: 5432:5432` на этом раннере означает не то, что кажется. Раннер запускает и job, и сервис-контейнеры с `--network host` (в логе: `docker create image=... network="host"`), а на 5432 того же хоста слушает ПРОДОВЫЙ Postgres. Сервис-контейнер порт занять не смог, а psql из job'а ушёл в прод и получил `password authentication failed for user "tradein"`. То есть `localhost:5432` из CI-job'а на vps-runner — это боевая база. Тест, чьи креды случайно подошли бы, писал бы в прод. Поэтому здесь нельзя публиковать порты вообще. Теперь контейнер поднимается явным `docker run` в дефолтной bridge-сети, БЕЗ `-p`, а DSN собирается из его собственного IP и уезжает в $GITHUB_ENV. Плюсы помимо безопасности: имя контейнера содержит github.run_id (параллельные PR не дерутся), psql берётся из самого контейнера (ушёл apt-get postgresql-client), снос контейнера в шаге с if: always(). --- .forgejo/workflows/ci-tradein.yml | 83 ++++++++++++++++++++----------- .forgejo/workflows/ci.yml | 60 ++++++++++++++-------- 2 files changed, 92 insertions(+), 51 deletions(-) diff --git a/.forgejo/workflows/ci-tradein.yml b/.forgejo/workflows/ci-tradein.yml index e9d38c2b..07e94059 100644 --- a/.forgejo/workflows/ci-tradein.yml +++ b/.forgejo/workflows/ci-tradein.yml @@ -75,48 +75,64 @@ jobs: # Замер перед включением: полный сьют на mock-лэйне 122с / 3858 passed / 10 skipped, # тот же сьют против живой БД — 106с / 3867 passed / 1 skipped. Живая БД не # медленнее, поэтому НЕ добавляем второй job, а чиним этот: один прогон, на - # девять реальных проверок больше. Накладные — только подъём сервиса и bootstrap - # схемы (219 файлов, ~20с). - services: - postgres: - # postgis, не plain postgres: tests/tasks/test_cadastral_geo_match.py - # проверяет KNN по geometry (PostGIS_Version() в connectivity-probe). - image: postgis/postgis:16-3.4 - env: - # CI-локальная одноразовая БД, не секрет (тот же паттерн, что в - # .github/workflows/ci.yml). Имя БД ОБЯЗАНО отличаться от `test`: - # `_live_session()` считает DSN с `localhost:5432/test` заглушкой и - # возвращает None — с ним сервис поднялся бы, а тесты всё равно скипались. - POSTGRES_DB: tradein - POSTGRES_USER: tradein - POSTGRES_PASSWORD: tradein - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U tradein" - --health-interval 5s - --health-timeout 5s - --health-retries 10 + # девять реальных проверок больше. Накладные — только подъём контейнера и + # bootstrap схемы (219 файлов, ~20с). defaults: run: working-directory: ./tradein-mvp/backend env: - DATABASE_URL: postgresql+psycopg://tradein:tradein@localhost:5432/tradein + # Имя контейнера уникально на прогон: параллельные PR не дерутся за него. + CI_PG: ci-pg-tradein-${{ github.run_id }} steps: - uses: actions/checkout@v4 + - name: Поднять Postgres для тестов + working-directory: . + # ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА. + # Раннер запускает и job, и сервис-контейнеры с `--network host` (видно в + # логе прогона: `docker create image=... network="host"`), а на этом же + # хосте на 5432 уже слушает ПРОДОВЫЙ Postgres. Первая попытка через + # `services:` + `ports: 5432:5432` (прогон #6853) кончилась тем, что + # сервис-контейнер не смог занять порт, а psql из job'а ушёл В ПРОД и + # получил `password authentication failed for user "tradein"`. То есть на + # этом раннере `localhost:5432` — это боевая база, а не тестовая. + # + # Поэтому: контейнер поднимаем сами, в дефолтной bridge-сети, БЕЗ + # публикации порта, и ходим по его собственному IP. Ноль шансов задеть + # прод, ноль конфликтов между параллельными прогонами и никакого + # postgresql-client на раннере — psql берём из самого контейнера. + # + # postgis, не plain postgres: tests/tasks/test_cadastral_geo_match.py + # проверяет KNN по geometry (PostGIS_Version() в connectivity-probe). + # + # Имя БД ОБЯЗАНО отличаться от `test`: `_live_session()` считает DSN с + # `localhost:5432/test` заглушкой и возвращает None — иначе контейнер + # поднялся бы, а тесты всё равно скипались. Пароль — одноразовый, + # CI-локальный, контейнер живёт минуты и наружу не смотрит. + run: | + docker rm -f "$CI_PG" >/dev/null 2>&1 || true + docker run -d --name "$CI_PG" \ + -e POSTGRES_DB=tradein -e POSTGRES_USER=tradein -e POSTGRES_PASSWORD=tradein \ + postgis/postgis:16-3.4 + for _ in $(seq 1 30); do + docker exec "$CI_PG" pg_isready -U tradein -q && break + sleep 2 + done + docker exec "$CI_PG" pg_isready -U tradein \ + || { echo "::error::Postgres не поднялся за 60с"; docker logs "$CI_PG"; exit 1; } + ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CI_PG") + [ -n "$ip" ] || { echo "::error::не удалось узнать IP контейнера $CI_PG"; exit 1; } + echo "DATABASE_URL=postgresql+psycopg://tradein:tradein@${ip}:5432/tradein" >> "$GITHUB_ENV" + echo "✓ Postgres на ${ip}:5432 (контейнер $CI_PG)" + - name: Bootstrap схемы tradein (backend/data/sql/*.sql) working-directory: ./tradein-mvp - env: - PGPASSWORD: tradein # Тот же порядок и тот же строгий режим, что в deploy-tradein.yml: # `ls | sort` + ON_ERROR_STOP=on, падение любой миграции → job RED. # Никаких «применилось как получилось»: схема в CI либо та же, что на # проде, либо гейта нет. run: | - sudo apt-get update - sudo apt-get install -y postgresql-client - psql -h localhost -U tradein -d tradein -v ON_ERROR_STOP=on -q -c \ + docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q -c \ "CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE ROLE gendesign_reader;" @@ -130,10 +146,10 @@ jobs: echo "⚠ пропускаю $fname — postgres_fdw к БД gendesign, которой в CI нет" continue fi - psql -h localhost -U tradein -d tradein -v ON_ERROR_STOP=on -q -f "$sql_file" \ + docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q < "$sql_file" \ || { echo "::error::миграция $fname не применилась"; exit 1; } done - echo "✓ схема собрана: $(psql -h localhost -U tradein -d tradein -tAc \ + echo "✓ схема собрана: $(docker exec "$CI_PG" psql -U tradein -d tradein -tAc \ "SELECT count(*) FROM information_schema.tables WHERE table_schema='public'") таблиц" - name: Install uv @@ -188,6 +204,13 @@ jobs: # называет себя вслух, со временем перестаёт быть верным. run: uv run pytest -q -rs + - name: Снести тестовый Postgres + # if: always() — контейнер уходит и когда сьют красный, и когда прогон + # отменён concurrency-группой. Иначе на раннере копятся мёртвые контейнеры. + if: always() + working-directory: . + run: docker rm -f "$CI_PG" >/dev/null 2>&1 || true + # Тесты браузерного сайдкара (#2722). До этого job'а они не бежали НИГДЕ: # ci-tradein гейтил только backend/frontend, deploy-tradein — тоже, а каталог # вне uv-воркспейса, так что и `uv run pytest` из backend их не собирал. Итог: diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 008a9015..ef62f676 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -81,25 +81,8 @@ jobs: # # TEST_DATABASE_URL НАМЕРЕННО НЕ задаётся: на него завязан tests/integration/ # (phantom-column gate), которому нужна КОПИЯ ПРОДОВОЙ схемы через pg_dump по - # SSH-туннелю. Пустой сервис-контейнер дал бы там красноту на пустом месте, - # поэтому integration остаётся честно пропущенным — с причиной в логе (`-rs`). - services: - postgres: - image: postgres:16 - env: - # CI-локальная одноразовая БД, не секрет (тот же паттерн, что в - # .github/workflows/ci.yml). Имя БД ≠ `test`, чтобы DSN не выглядел - # заглушкой для probe-ов в тестах. - POSTGRES_DB: gendesign_ci - POSTGRES_USER: gendesign - POSTGRES_PASSWORD: gendesign - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U gendesign" - --health-interval 5s - --health-timeout 5s - --health-retries 10 + # SSH-туннелю. Пустой контейнер дал бы там красноту на пустом месте, поэтому + # integration остаётся честно пропущенным — с причиной в логе (`-rs`). defaults: run: working-directory: backend @@ -107,11 +90,39 @@ jobs: # TESTING=1 активирует RBAC-bypass (app/main.py rbac_guard пропускает # запросы при settings.testing=True) — иначе 401 на всём /api/v1. TESTING: "1" - DATABASE_URL: postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign_ci REDIS_URL: redis://localhost:6379/0 + # Имя контейнера уникально на прогон: параллельные PR не дерутся за него. + CI_PG: ci-pg-backend-${{ github.run_id }} steps: - uses: actions/checkout@v4 + - name: Поднять Postgres для тестов + working-directory: . + # ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА — см. подробный + # разбор в ci-tradein.yml (тот же раннер). Кратко: job и сервис-контейнеры + # запускаются с `--network host`, а на 5432 этого хоста слушает ПРОДОВЫЙ + # Postgres, так что `localhost:5432` из job'а — боевая база. Поднимаем + # контейнер сами, в bridge-сети, без публикации порта, ходим по его IP. + # + # plain postgres:16, БЕЗ PostGIS: тесты tests/sql/ строят себе временные + # таблицы и не трогают ни geometry, ни реальную схему — bootstrap схемы + # здесь не нужен вовсе, в отличие от tradein-лэйна. + run: | + docker rm -f "$CI_PG" >/dev/null 2>&1 || true + docker run -d --name "$CI_PG" \ + -e POSTGRES_DB=gendesign_ci -e POSTGRES_USER=gendesign -e POSTGRES_PASSWORD=gendesign \ + postgres:16 + for _ in $(seq 1 30); do + docker exec "$CI_PG" pg_isready -U gendesign -q && break + sleep 2 + done + docker exec "$CI_PG" pg_isready -U gendesign \ + || { echo "::error::Postgres не поднялся за 60с"; docker logs "$CI_PG"; exit 1; } + ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CI_PG") + [ -n "$ip" ] || { echo "::error::не удалось узнать IP контейнера $CI_PG"; exit 1; } + echo "DATABASE_URL=postgresql+psycopg://gendesign:gendesign@${ip}:5432/gendesign_ci" >> "$GITHUB_ENV" + echo "✓ Postgres на ${ip}:5432 (контейнер $CI_PG)" + - name: Set up Python uses: actions/setup-python@v5 with: @@ -164,7 +175,7 @@ jobs: # но --ignore — belt-and-suspenders на случай сбора фикстур). # tests/integration self-skip'ается через requires_test_db (skipif на # TEST_DATABASE_URL, который тут не задан) → НЕ игнорим, оно чисто skip'ается. - # tests/sql/ теперь РЕАЛЬНО ИДУТ — postgres-сервис выше (#2745). + # tests/sql/ теперь РЕАЛЬНО ИДУТ — postgres-контейнер выше (#2745). # PDF-тесты ИДУТ (libpango выше). Target: 0 failed, skips OK. # # `-rs` (#2745): каждый оставшийся пропуск печатает причину. Под `-q` без @@ -200,6 +211,13 @@ jobs: echo "$report" fi + - name: Снести тестовый Postgres + # if: always() — контейнер уходит и когда сьют красный, и когда прогон + # отменён concurrency-группой. Иначе на раннере копятся мёртвые контейнеры. + if: always() + working-directory: . + run: docker rm -f "$CI_PG" >/dev/null 2>&1 || true + frontend-tests: runs-on: ubuntu-latest needs: changes