From 206cd45098978a43b61064568544c81d27c3f4c1 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 5 Nov 2026 18:43:45 +0800 Subject: [PATCH] Apply tracked schema migrations before deployment and test runtime grants --- .gitea/workflows/deploy.yml | 1 + deploy/check-schema.sh | 8 +++- deploy/migrate.py | 76 +++++++++++++++++++++++++++++++++++++ deploy/release.sh | 1 + 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 deploy/migrate.py diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 564745f..f2d5c54 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -8,6 +8,7 @@ on: - 'go.mod' - 'go.sum' - 'deploy/**' + - 'db/**' - '.gitea/workflows/deploy.yml' concurrency: diff --git a/deploy/check-schema.sh b/deploy/check-schema.sh index b97a56c..adec116 100644 --- a/deploy/check-schema.sh +++ b/deploy/check-schema.sh @@ -19,7 +19,11 @@ VALUES ('fixture','00000000-0000-0000-0000-000000000001','existing-google','exis INSERT INTO project_sessions VALUES ('existing-session','fixture','00000000-0000-0000-0000-000000000001',now()+interval '1 hour'); SQL -psql < db/002-platform.sql +psql <<'SQLROLE' +CREATE ROLE foundry_fixture LOGIN; +SQLROLE +python3 deploy/migrate.py --container "$container" --database postgres --role foundry_fixture +python3 deploy/migrate.py --container "$container" --database postgres --role foundry_fixture psql <<'SQL' DO $$ BEGIN @@ -60,5 +64,5 @@ CGO_ENABLED=0 go test -mod=readonly -c -o bin/platform-tests ./internal/platform docker run --rm --network "container:$container" --user 1000:1000 \ --read-only --cap-drop ALL --security-opt no-new-privileges \ --mount "type=bind,source=$(pwd)/bin/platform-tests,target=/opt/platform-tests,readonly" \ - -e PLATFORM_TEST_DATABASE_URL=postgres://postgres@127.0.0.1:5432/postgres?sslmode=disable \ + -e PLATFORM_TEST_DATABASE_URL=postgres://foundry_fixture@127.0.0.1:5432/postgres?sslmode=disable \ --entrypoint /opt/platform-tests postgres:17-alpine -test.v diff --git a/deploy/migrate.py b/deploy/migrate.py new file mode 100644 index 0000000..7cc46ff --- /dev/null +++ b/deploy/migrate.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Apply checked migrations atomically from the trusted deployment runner.""" +import argparse +import hashlib +from pathlib import Path +import re +import subprocess + +parser = argparse.ArgumentParser() +parser.add_argument("--container", default="endoleap-dev-postgres-1") +parser.add_argument("--database", default="foundry_auth") +parser.add_argument("--role", default="foundry") +args = parser.parse_args() +for value in (args.database, args.role): + if not re.fullmatch(r"[a-z_][a-z0-9_]{0,62}", value): + raise SystemExit("Invalid deployment database or role") +migrations = sorted((Path(__file__).resolve().parent.parent / "db").glob("[0-9]*.sql")) +if not migrations: + raise SystemExit("No migrations found") + +script = [ + "\\set ON_ERROR_STOP on", + "BEGIN;", + "SET LOCAL lock_timeout='30s';", + "SELECT pg_advisory_xact_lock(731920164);", + "CREATE TABLE IF NOT EXISTS foundry_schema_migrations " + "(name text PRIMARY KEY, checksum text NOT NULL, applied_at timestamptz NOT NULL DEFAULT now());", +] +tables = set() +for index, path in enumerate(migrations): + source = path.read_text() + name = path.name + if not re.fullmatch(r"[0-9]+-[a-z0-9-]+\.sql", name): + raise SystemExit("Invalid migration name") + checksum = hashlib.sha256(source.encode()).hexdigest() + tables.update(re.findall(r"CREATE TABLE ([a-z_]+)", source)) + # The first deployed version predates migration tracking. + if index == 0: + script.extend([ + "SELECT bool_and(to_regclass('public.' || name) IS NOT NULL) AS baseline FROM " + "unnest(ARRAY['project_accounts','oauth_attempts','login_grants','project_sessions']) name \\gset", + "\\if :baseline", + f"INSERT INTO foundry_schema_migrations(name,checksum) VALUES('{name}','{checksum}') ON CONFLICT DO NOTHING;", + "\\endif", + ]) + script.extend([ + f"SELECT EXISTS(SELECT 1 FROM foundry_schema_migrations WHERE name='{name}') AS applied, " + f"EXISTS(SELECT 1 FROM foundry_schema_migrations WHERE name='{name}' AND checksum='{checksum}') AS valid \\gset", + "\\if :applied", + "\\if :valid", + "\\else", + "\\echo Migration checksum mismatch", + "\\quit 1", + "\\endif", + "\\else", + ]) + source = source.strip() + if not source.startswith("BEGIN;") or not source.endswith("COMMIT;"): + raise SystemExit("Migration must have explicit transaction boundaries") + script.append(source[len("BEGIN;"):-len("COMMIT;")]) + script.extend([ + f"INSERT INTO foundry_schema_migrations(name,checksum) VALUES('{name}','{checksum}');", + "\\endif", + ]) +for table in sorted(tables): + script.append(f'GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE "{table}" TO "{args.role}";') +script.extend([ + f'GRANT USAGE,SELECT ON SEQUENCE link_clicks_id_seq TO "{args.role}";', + "COMMIT;", +]) +subprocess.run( + ["docker", "exec", "-i", args.container, "sh", "-c", + 'exec psql -X -U "${POSTGRES_USER:-postgres}" -d "$1" -v ON_ERROR_STOP=1', "sh", args.database], + input=("\n".join(script) + "\n").encode(), check=True, +) +print("Schema migrations and application grants verified.") diff --git a/deploy/release.sh b/deploy/release.sh index 3636858..d9ca918 100644 --- a/deploy/release.sh +++ b/deploy/release.sh @@ -40,6 +40,7 @@ export FOUNDRY_PROJECTS test -s "$foundry/database.env" docker network inspect "$network" >/dev/null +python3 "$(dirname "$0")/migrate.py" release=$(mktemp -d "$foundry/release.XXXXXXXX") install -m 755 "$1" "$release/foundry"