From 69276ed1fac18f9d1a45c8bfbcc76aed2b0b7921 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 20:31:42 +0800 Subject: [PATCH] fix(cd): skip converged MCP receipt DDL --- .gitea/workflows/cd.yaml | 80 ++++++++++++------- .../test_mcp_version_lifecycle_service.py | 3 + 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 0d1283445..2a4d5a808 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -2351,44 +2351,68 @@ jobs: return value.replace("postgresql+asyncpg://", "postgresql://", 1) + async def read_schema_state(connection): + rows = await connection.fetch( + """ + SELECT relname, relrowsecurity, relforcerowsecurity + FROM pg_class + JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace + WHERE pg_namespace.nspname = 'public' + AND relname = ANY($1::text[]) + """, + sorted(REQUIRED_TABLES), + ) + table_state = { + str(row["relname"]): ( + bool(row["relrowsecurity"]), + bool(row["relforcerowsecurity"]), + ) + for row in rows + } + policy_count = await connection.fetchval( + """ + SELECT COUNT(*)::int + FROM pg_policies + WHERE tablename = ANY($1::text[]) + """, + sorted(REQUIRED_TABLES), + ) + schema_verified = ( + set(table_state) == REQUIRED_TABLES + and all(state == (True, True) for state in table_state.values()) + and int(policy_count or 0) == 2 + ) + return schema_verified, int(policy_count or 0) + + async def apply_and_verify_schema() -> None: connection = await asyncpg.connect( normalize_url(os.environ["MIGRATION_DATABASE_URL"]), timeout=10, ) try: - await connection.execute(MIGRATION) - rows = await connection.fetch( - """ - SELECT relname, relrowsecurity, relforcerowsecurity - FROM pg_class - WHERE relname = ANY($1::text[]) - """, - sorted(REQUIRED_TABLES), - ) - table_state = { - str(row["relname"]): ( - bool(row["relrowsecurity"]), - bool(row["relforcerowsecurity"]), + # Verified schema is already converged; avoid needless + # ACCESS EXCLUSIVE policy DDL during a production pg_dump. + schema_verified, policy_count = await read_schema_state(connection) + migration_applied = False + if not schema_verified: + await connection.execute("SET lock_timeout = '10s'") + await connection.execute("SET statement_timeout = '120s'") + await connection.execute(MIGRATION) + migration_applied = True + schema_verified, policy_count = await read_schema_state( + connection ) - for row in rows - } - policy_count = await connection.fetchval( - """ - SELECT COUNT(*)::int - FROM pg_policies - WHERE tablename = ANY($1::text[]) - """, - sorted(REQUIRED_TABLES), + print( + "mcp_version_lifecycle_migration_applied=" + f"{str(migration_applied).lower()}" ) - schema_verified = ( - set(table_state) == REQUIRED_TABLES - and all(state == (True, True) for state in table_state.values()) - and int(policy_count or 0) == 2 + print( + "mcp_version_lifecycle_migration_skipped_existing_verified=" + f"{str(not migration_applied and schema_verified).lower()}" ) - print("mcp_version_lifecycle_migration_applied=true") print(f"mcp_version_lifecycle_schema_verified={str(schema_verified).lower()}") - print(f"mcp_version_lifecycle_rls_policy_count={int(policy_count or 0)}") + print(f"mcp_version_lifecycle_rls_policy_count={policy_count}") if not schema_verified: raise RuntimeError("mcp_version_lifecycle_schema_verifier_failed") finally: diff --git a/apps/api/tests/test_mcp_version_lifecycle_service.py b/apps/api/tests/test_mcp_version_lifecycle_service.py index 15fc35bd4..0ddb49567 100644 --- a/apps/api/tests/test_mcp_version_lifecycle_service.py +++ b/apps/api/tests/test_mcp_version_lifecycle_service.py @@ -261,3 +261,6 @@ def test_migration_is_additive_rls_and_does_not_store_raw_content() -> None: assert "Apply MCP Version Lifecycle Receipt Schema" in workflow assert "mcp_version_lifecycle_schema_verified" in workflow assert "mcp_version_lifecycle_runtime_role_read_verified" in workflow + assert "mcp_version_lifecycle_migration_skipped_existing_verified" in workflow + assert "SET lock_timeout = '10s'" in workflow + assert "SET statement_timeout = '120s'" in workflow