From cfde033c2f3c67abb53e25d28cdb6e5dc5b35a9b Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 6 Aug 2026 22:36:15 +0500 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=D0=B6=D0=B4=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D0=BE=D1=8F=D1=89=D0=B8=D0=B9=20Postgr?= =?UTF-8?q?es,=20=D0=B0=20=D0=BD=D0=B5=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=20=D1=84=D0=B0=D0=B7=D1=8B=20initdb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прогон 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 слиты в ОДИН шаг — между шагами контейнер успевал исчезнуть. --- .forgejo/workflows/ci-tradein.yml | 68 ++++++++++++++++++------------- .forgejo/workflows/ci.yml | 32 +++++++++++---- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/.forgejo/workflows/ci-tradein.yml b/.forgejo/workflows/ci-tradein.yml index 07e94059..5b228267 100644 --- a/.forgejo/workflows/ci-tradein.yml +++ b/.forgejo/workflows/ci-tradein.yml @@ -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'") таблиц" diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index ef62f676..66d5fd7f 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -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"