coulomb-social/scripts/rehearse-storage-migration.py
tegwick 789e69902d
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 7m5s
Harden original-date migration and verify Coulomb recovery admission
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
2026-10-27 23:51:27 +01:00

168 lines
5.1 KiB
Python

"""Rehearse migration 0003 on disposable internal-network PostgreSQL.
Uses cached images and read-only source/test dependencies; no published ports,
production credentials, live data, or production migration. Requires Docker,
postgres:16, the deployed dd83c90 app image and the locked local dev environment.
"""
import argparse
import hashlib
import json
import shutil
import subprocess
import sysconfig
import tempfile
import uuid
from pathlib import Path
name = "csoc-migration-" + uuid.uuid4().hex[:10]
root = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--app-image",
default="forgejo.coulomb.social/coulomb/coulomb-social:dd83c90",
)
parser.add_argument(
"--packaged-source",
action="store_true",
help="Use image source/dependencies, injecting only test tools",
)
args = parser.parse_args()
def run(*args):
return subprocess.run(args, capture_output=True, text=True, check=True).stdout.strip()
app = run(
"docker",
"image",
"inspect",
args.app_image,
"--format",
"{{.Id}}",
)
pg = run("docker", "image", "inspect", "postgres:16", "--format", "{{.Id}}")
code = """import time, json, django
django.setup()
from django.db import connection
for i in range(30):
try:
connection.ensure_connection()
break
except Exception:
time.sleep(1)
else:
raise RuntimeError('isolated_postgres_not_ready')
from django.core.management import call_command
call_command('migrate', verbosity=0)
from tests import test_storage_migration_lifecycle as lifecycle
assert connection.vendor == 'postgresql'
lifecycle.test_forward_refusal_rollback_and_reapply_preserve_rows()
print(json.dumps({
'status': 'passed', 'database_vendor': connection.vendor,
'server_version': connection.pg_version, 'forward_collision_refusal': True,
'refusal_preserves_rows_and_grants': True,
'rollback_preserves_rows_grants_and_tenant_constraint': True,
'reapply_enforces_global_constraint': True, 'production_access': False,
}))
"""
tool_directory = None
source_mounts = ["-v", str(root / "coulomb_social") + ":/app/coulomb_social:ro"]
dependency_directory = sysconfig.get_path("purelib")
test_tools = ["pytest", "_pytest", "pluggy", "iniconfig", "pygments", "py.py"]
if args.packaged_source:
source_mounts = []
tool_directory = Path(tempfile.mkdtemp(prefix="csoc-migration-test-tools-"))
for package in test_tools:
source = Path(dependency_directory) / package
if source.is_dir():
shutil.copytree(source, tool_directory / package)
else:
shutil.copyfile(source, tool_directory / package)
# Only public test-tool sources are copied. The image runs as UID10001.
tool_directory.chmod(0o755)
dependency_directory = str(tool_directory)
code = code.replace(
"django.setup()",
"assert django.__file__.startswith('/app/.venv/'), 'packaged_django_required'\n"
"django.setup()",
)
try:
run("docker", "network", "create", "--internal", name)
run(
"docker",
"run",
"-d",
"--rm",
"--name",
name,
"--network",
name,
"--network-alias",
"storage-db",
"-e",
"POSTGRES_HOST_AUTH_METHOD=trust",
"-e",
"POSTGRES_DB=storage_rehearsal",
pg,
)
result = run(
"docker",
"run",
"--rm",
"--network",
name,
"--read-only",
"--tmpfs",
"/tmp",
"--entrypoint",
"/app/.venv/bin/python",
"-e",
"DJANGO_SETTINGS_MODULE=coulomb_social.settings.base",
"-e",
"DATABASE_URL=postgres://postgres@storage-db/storage_rehearsal",
"-e",
"PYTHONDONTWRITEBYTECODE=1",
*source_mounts,
"-v",
str(root / "tests") + ":/app/tests:ro",
"-v",
dependency_directory + ":/test-deps:ro",
"-e",
"PYTHONPATH=/test-deps",
app,
"-c",
code,
)
print(
json.dumps(
{
"app_dependency_image": app,
"packaged_source_and_django": args.packaged_source,
"injected_test_tools": test_tools
if args.packaged_source
else "locked dev environment",
"postgres_image": pg,
"proof": json.loads(result.splitlines()[-1]),
"source_sha256": {
str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest()
for p in [
root / "tests/test_storage_migration_lifecycle.py",
root
/ "coulomb_social/apps/spaces/migrations/0003_space_slug_global_uniq.py",
root / "uv.lock",
]
},
},
indent=2,
)
)
except subprocess.CalledProcessError as e:
print(e.stdout, e.stderr)
raise
finally:
subprocess.run(["docker", "rm", "-fv", name], capture_output=True)
subprocess.run(["docker", "network", "rm", name], capture_output=True)
if tool_directory is not None:
shutil.rmtree(tool_directory)