fix(ci): ждать настоящий Postgres, а не временный сервер фазы initdb
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Successful in 52s
CI Trade-In / frontend-checks (pull_request) Successful in 1m38s
CI / frontend-tests (pull_request) Successful in 1m44s
CI / openapi-codegen-check (pull_request) Successful in 2m20s
CI Trade-In / backend-tests (pull_request) Successful in 4m31s
CI / backend-tests (pull_request) Successful in 15m35s

Прогон 6859: контейнер поднялся, `pg_isready` сказал «accepting connections»,
через 1.4с bootstrap упал на `container is not running`. Причина — проба шла
через unix-сокет, а по сокету отвечает ВРЕМЕННЫЙ сервер, который образ
postgres поднимает на время initdb с listen_addresses=''. Снаружи БД в этот
момент ещё не существует, и впереди рестарт: мы поймали окно и приняли его за
готовность. Классическая ложная зелень — проверка сказала «готово» про не то.

Теперь проба `pg_isready -h 127.0.0.1` (TCP) — зеленеет только на настоящем
сервере, том самом, к которому пойдут тесты. Плюс: цикл ждёт до 90с и
прерывается, если контейнер вышел; при неудаче печатается статус, код выхода
и `docker logs`. Подъём и bootstrap слиты в ОДИН шаг — между шагами контейнер
успевал исчезнуть.
This commit is contained in:
bot-backend 2026-08-06 22:36:15 +05:00
parent a07da68a37
commit cfde033c2f
2 changed files with 63 additions and 37 deletions

View file

@ -86,57 +86,69 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Поднять Postgres для тестов
- name: Поднять Postgres и собрать схему tradein
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` — это боевая база, а не тестовая.
# логе прогона: `docker create image=... network="host"`), а на 5432 того
# же хоста слушает ПРОДОВЫЙ Postgres. Попытка через `services:` +
# `ports: 5432:5432` кончилась тем, что сервис-контейнер не смог занять
# порт, а psql из job'а ушёл В ПРОД и получил
# `password authentication failed for user "tradein"`. То есть
# `localhost:5432` из job'а на этом раннере — боевая база, а не тестовая.
# Поэтому контейнер поднимаем сами, в bridge-сети, БЕЗ публикации порта,
# и ходим по его собственному IP: прод недостижим в принципе, параллельные
# прогоны не конфликтуют, psql берём из самого контейнера.
#
# Поэтому: контейнер поднимаем сами, в дефолтной bridge-сети, БЕЗ
# публикации порта, и ходим по его собственному IP. Ноль шансов задеть
# прод, ноль конфликтов между параллельными прогонами и никакого
# postgresql-client на раннере — psql берём из самого контейнера.
# ОДИН шаг, а не два: между шагами контейнер успевал исчезнуть, и
# bootstrap падал на `container is not running`.
#
# `pg_isready -h 127.0.0.1`, а НЕ через unix-сокет: на время initdb образ
# поднимает ВРЕМЕННЫЙ сервер с listen_addresses='' — по сокету он уже
# отвечает «accepting connections», хотя снаружи БД ещё не существует, а
# впереди рестарт. Проба по TCP зеленеет только на настоящем сервере —
# том самом, к которому пойдут тесты.
#
# 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-локальный, контейнер живёт минуты и наружу не смотрит.
# `localhost:5432/test` заглушкой и вернул бы None — контейнер поднялся
# бы, а тесты всё равно скипались.
run: |
set -u
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
ready=""
for _ in $(seq 1 45); do
if docker exec "$CI_PG" pg_isready -h 127.0.0.1 -U tradein -q 2>/dev/null; then
ready=1; break
fi
[ "$(docker inspect -f '{{.State.Status}}' "$CI_PG" 2>/dev/null)" = "running" ] || break
sleep 2
done
docker exec "$CI_PG" pg_isready -U tradein \
|| { echo "::error::Postgres не поднялся за 60с"; docker logs "$CI_PG"; exit 1; }
if [ -z "$ready" ]; then
echo "::error::Postgres не поднялся; статус=$(docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' "$CI_PG" 2>&1)"
docker logs --tail 50 "$CI_PG" 2>&1 || true
exit 1
fi
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
# Тот же порядок и тот же строгий режим, что в deploy-tradein.yml:
# `ls | sort` + ON_ERROR_STOP=on, падение любой миграции → job RED.
# Никаких «применилось как получилось»: схема в CI либо та же, что на
# проде, либо гейта нет.
run: |
# Тот же порядок и тот же строгий режим, что в deploy-tradein.yml:
# `ls | sort` + ON_ERROR_STOP=on, падение любой миграции → job RED.
# Никаких «применилось как получилось»: схема в CI либо та же, что на
# проде, либо гейта нет.
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;"
for sql_file in $(ls -1 backend/data/sql/*.sql | sort); do
for sql_file in $(ls -1 tradein-mvp/backend/data/sql/*.sql | sort); do
fname=$(basename "$sql_file")
# ЕДИНСТВЕННОЕ исключение, и оно названо вслух: 077 — не DDL, а
# backfill, читающий foreign table gendesign_rosreestr_deals из БД
@ -147,7 +159,7 @@ jobs:
continue
fi
docker exec -i "$CI_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -q < "$sql_file" \
|| { echo "::error::миграция $fname не применилась"; exit 1; }
|| { echo "::error::миграция $fname не применилась"; docker logs --tail 20 "$CI_PG" 2>&1 || true; exit 1; }
done
echo "✓ схема собрана: $(docker exec "$CI_PG" psql -U tradein -d tradein -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public'") таблиц"

View file

@ -98,26 +98,40 @@ jobs:
- name: Поднять Postgres для тестов
working-directory: .
# ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА — см. подробный
# разбор в ci-tradein.yml (тот же раннер). Кратко: job и сервис-контейнеры
# запускаются с `--network host`, а на 5432 этого хоста слушает ПРОДОВЫЙ
# Postgres, так что `localhost:5432` из job'а — боевая база. Поднимаем
# контейнер сами, в bridge-сети, без публикации порта, ходим по его IP.
# ПОЧЕМУ НЕ `services:` И ПОЧЕМУ БЕЗ ПУБЛИКАЦИИ ПОРТА — подробный разбор в
# ci-tradein.yml (тот же раннер). Кратко: job и сервис-контейнеры идут с
# `--network host`, а на 5432 этого хоста слушает ПРОДОВЫЙ Postgres, то
# есть `localhost:5432` из job'а — боевая база. Поднимаем контейнер сами,
# в bridge-сети, без публикации порта, ходим по его IP.
#
# `pg_isready -h 127.0.0.1`, а не через unix-сокет: по сокету отвечает
# ВРЕМЕННЫЙ сервер фазы initdb (listen_addresses=''), после которой БД
# ещё перезапускается. Проба по TCP зеленеет только на настоящем сервере.
#
# plain postgres:16, БЕЗ PostGIS: тесты tests/sql/ строят себе временные
# таблицы и не трогают ни geometry, ни реальную схему — bootstrap схемы
# здесь не нужен вовсе, в отличие от tradein-лэйна.
run: |
set -u
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
ready=""
for _ in $(seq 1 45); do
if docker exec "$CI_PG" pg_isready -h 127.0.0.1 -U gendesign -q 2>/dev/null; then
ready=1; break
fi
[ "$(docker inspect -f '{{.State.Status}}' "$CI_PG" 2>/dev/null)" = "running" ] || break
sleep 2
done
docker exec "$CI_PG" pg_isready -U gendesign \
|| { echo "::error::Postgres не поднялся за 60с"; docker logs "$CI_PG"; exit 1; }
if [ -z "$ready" ]; then
echo "::error::Postgres не поднялся; статус=$(docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' "$CI_PG" 2>&1)"
docker logs --tail 50 "$CI_PG" 2>&1 || true
exit 1
fi
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"