diff --git a/.forgejo/workflows/deploy-tradein.yml b/.forgejo/workflows/deploy-tradein.yml index 11ea64d6..ecac8eb3 100644 --- a/.forgejo/workflows/deploy-tradein.yml +++ b/.forgejo/workflows/deploy-tradein.yml @@ -156,18 +156,66 @@ jobs: docker compose -p gendesign-tradein -f docker-compose.prod.yml pull docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d - # Применяем SQL миграции (если есть data/sql/*.sql) + # Применяем SQL миграции (если есть backend/data/sql/*.sql) # Postgres init load *.sql из /docker-entrypoint-initdb.d ТОЛЬКО при первом # старте volume. Здесь — для повторных миграций после первого запуска. + # Tracking через _schema_migrations (порт паттерна из deploy.yml): + # каждый .sql применяется РОВНО один раз, failed migration → exit 1 + # (никаких swallowed errors). cwd = /opt/gendesign/tradein-mvp. sleep 5 + + # Pre-existence detection ДО CREATE TABLE: если таблицы ещё нет, это + # первый deploy после внедрения tracking на уже-наполненной prod-БД + # (миграции 001-076 живут в схеме). 7 из них (002/003/052/053/054/063/072) + # содержат INSERT-seed БЕЗ ON CONFLICT — повторный прогон под строгим + # ON_ERROR_STOP упал бы на PK violation. Поэтому: baseline (см. ниже). + migrations_table_existed=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \ + "SELECT to_regclass('public._schema_migrations') IS NOT NULL;" | tr -d '[:space:]') + + docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c " + CREATE TABLE IF NOT EXISTS _schema_migrations ( + filename TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + " + + if [ "$migrations_table_existed" != "t" ]; then + # BASELINE: таблицы не было → seed ВСЕ текущие миграции как applied + # БЕЗ их прогона. prod уже работает на этой схеме; помечаем её + # текущим состоянием, чтобы под строгий gate попадали только НОВЫЕ + # (077+) миграции. INSERT ... ON CONFLICT DO NOTHING — идемпотентно. + echo "→ _schema_migrations отсутствовала — baseline существующих миграций (без прогона)" + for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do + fname=$(basename "$sql_file") + echo " baseline: $fname" + docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c \ + "INSERT INTO _schema_migrations (filename) VALUES ('$fname') ON CONFLICT DO NOTHING;" + done + echo "Baseline complete — existing schema marked as applied." + fi + for sql_file in $(ls -1 backend/data/sql/*.sql 2>/dev/null | sort); do fname=$(basename "$sql_file") - echo "→ Migration: $fname" - docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ - psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein \ - -v ON_ERROR_STOP=on < "$sql_file" \ - || echo " (skipped — already applied or error: $fname)" + applied=$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \ + "SELECT COUNT(*) FROM _schema_migrations WHERE filename='$fname'" | tr -d '[:space:]') + if [ "$applied" = "0" ]; then + echo "→ Applying migration: $fname" + docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on \ + < "$sql_file" \ + || { echo "FAILED on migration: $fname"; exit 1; } + docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \ + psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -c \ + "INSERT INTO _schema_migrations (filename) VALUES ('$fname') ON CONFLICT DO NOTHING;" + else + echo "✓ Already applied: $fname" + fi done + echo "All migrations applied." # Retry backend lifespan hook AFTER migrations applied. # tradein-backend startup runs ensure_fdw_user_mapping which needs