fix(ci): тестовый Postgres — свой контейнер, а не services: на порту прода
Some checks failed
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Failing after 20s
CI Trade-In / browser-tests (pull_request) Successful in 57s
CI / frontend-tests (pull_request) Successful in 1m39s
CI Trade-In / frontend-checks (pull_request) Successful in 1m37s
CI / openapi-codegen-check (pull_request) Successful in 2m37s
CI / backend-tests (pull_request) Successful in 16m7s

Первая версия (#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().
This commit is contained in:
bot-backend 2026-08-06 22:23:46 +05:00
parent 1fa3cb6949
commit a07da68a37
2 changed files with 92 additions and 51 deletions

View file

@ -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 их не собирал. Итог:

View file

@ -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