From 2eebdfe16a88716d93b89ac14b7288aa3769f0d0 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Wed, 8 Apr 2026 23:43:05 +0800 Subject: [PATCH] ci: add migration test workflow for SQLite and PostgreSQL Tests alembic upgrade on both databases: - Stamp baseline on existing schema - Upgrade to head - Idempotent re-upgrade - Fresh DB upgrade from scratch --- .github/workflows/test-migrations.yml | 171 ++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 .github/workflows/test-migrations.yml diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml new file mode 100644 index 00000000..fa2d30ae --- /dev/null +++ b/.github/workflows/test-migrations.yml @@ -0,0 +1,171 @@ +name: Test Migrations + +on: + push: + branches: + - main + - master + - dev + paths: + - 'src/langbot/pkg/persistence/**' + - 'src/langbot/pkg/entity/persistence/**' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'src/langbot/pkg/persistence/**' + - 'src/langbot/pkg/entity/persistence/**' + +jobs: + test-migrations-sqlite: + name: Migrations (SQLite) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies + run: uv sync --dev + + - name: Test Alembic upgrade (SQLite) + run: | + uv run python -c " + import asyncio + from sqlalchemy.ext.asyncio import create_async_engine + from langbot.pkg.entity.persistence.base import Base + from langbot.pkg.persistence.alembic_runner import run_alembic_upgrade, run_alembic_stamp, get_alembic_current + + async def main(): + engine = create_async_engine('sqlite+aiosqlite:///test_migrations.db') + + # Create all tables (simulates existing DB) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + # Stamp baseline + await run_alembic_stamp(engine, '0001_baseline') + rev = await get_alembic_current(engine) + assert rev == '0001_baseline', f'Expected 0001_baseline, got {rev}' + print(f'Stamped: {rev}') + + # Upgrade to head + await run_alembic_upgrade(engine, 'head') + rev = await get_alembic_current(engine) + print(f'After upgrade: {rev}') + assert rev is not None, 'Expected a revision after upgrade' + + # Verify idempotent + await run_alembic_upgrade(engine, 'head') + rev2 = await get_alembic_current(engine) + assert rev2 == rev, f'Expected {rev}, got {rev2}' + print(f'Idempotent check passed: {rev2}') + + # Fresh DB: upgrade from scratch + engine2 = create_async_engine('sqlite+aiosqlite:///test_migrations_fresh.db') + async with engine2.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await run_alembic_upgrade(engine2, 'head') + rev3 = await get_alembic_current(engine2) + print(f'Fresh DB upgrade: {rev3}') + assert rev3 is not None + + print('All SQLite migration tests passed!') + + asyncio.run(main()) + " + + test-migrations-postgres: + name: Migrations (PostgreSQL) + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: langbot + POSTGRES_PASSWORD: langbot + POSTGRES_DB: langbot_test + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U langbot" + --health-interval=5s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies + run: uv sync --dev + + - name: Test Alembic upgrade (PostgreSQL) + run: | + uv run python -c " + import asyncio + from sqlalchemy.ext.asyncio import create_async_engine + from langbot.pkg.entity.persistence.base import Base + from langbot.pkg.persistence.alembic_runner import run_alembic_upgrade, run_alembic_stamp, get_alembic_current + + DB_URL = 'postgresql+asyncpg://langbot:langbot@localhost:5432/langbot_test' + + async def main(): + engine = create_async_engine(DB_URL) + + # Create all tables + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + # Stamp baseline + await run_alembic_stamp(engine, '0001_baseline') + rev = await get_alembic_current(engine) + assert rev == '0001_baseline', f'Expected 0001_baseline, got {rev}' + print(f'Stamped: {rev}') + + # Upgrade to head + await run_alembic_upgrade(engine, 'head') + rev = await get_alembic_current(engine) + print(f'After upgrade: {rev}') + assert rev is not None + + # Verify idempotent + await run_alembic_upgrade(engine, 'head') + rev2 = await get_alembic_current(engine) + assert rev2 == rev, f'Expected {rev}, got {rev2}' + print(f'Idempotent check passed: {rev2}') + + # Fresh DB: drop all and upgrade from scratch + engine2 = create_async_engine(DB_URL.replace('langbot_test', 'langbot_fresh')) + + # Create fresh database + from sqlalchemy import text + async with engine.connect() as conn: + await conn.execute(text('COMMIT')) + await conn.execute(text('CREATE DATABASE langbot_fresh')) + + async with engine2.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + await run_alembic_upgrade(engine2, 'head') + rev3 = await get_alembic_current(engine2) + print(f'Fresh DB upgrade: {rev3}') + assert rev3 is not None + + print('All PostgreSQL migration tests passed!') + + asyncio.run(main()) + "