164 lines
7.2 KiB
Python
164 lines
7.2 KiB
Python
"""Fail-closed checks for the production delivery workflow.
|
|
|
|
This test intentionally uses only the Python standard library so it can run before
|
|
project dependencies are installed. The small helpers parse indentation-delimited
|
|
YAML blocks that are relevant to the policy; they are not a general YAML parser.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
|
VALIDATION_WORKFLOW = REPOSITORY_ROOT / ".gitea" / "workflows" / "managed-validation.yml"
|
|
|
|
|
|
def _indented_block(text: str, header: str) -> str:
|
|
"""Return a YAML block beginning at an exact header line."""
|
|
|
|
lines = text.splitlines()
|
|
try:
|
|
start = lines.index(header)
|
|
except ValueError as exc:
|
|
raise AssertionError(f"Missing YAML header: {header!r}") from exc
|
|
indent = len(header) - len(header.lstrip())
|
|
end = len(lines)
|
|
for index in range(start + 1, len(lines)):
|
|
line = lines[index]
|
|
if not line.strip() or line.lstrip().startswith("#"):
|
|
continue
|
|
current_indent = len(line) - len(line.lstrip())
|
|
if current_indent <= indent:
|
|
end = index
|
|
break
|
|
return "\n".join(lines[start:end])
|
|
|
|
|
|
def _named_step(text: str, name: str) -> str:
|
|
return _indented_block(text, f" - name: {name}")
|
|
|
|
|
|
def _run_script(step: str) -> str:
|
|
"""Extract one inline or literal run body without normalizing shell content."""
|
|
|
|
lines = step.splitlines()
|
|
if " run: |" not in lines:
|
|
inline = next((line for line in lines if line.startswith(" run: ")), None)
|
|
if inline is None:
|
|
raise AssertionError("Named step does not contain a run command")
|
|
return inline.removeprefix(" run: ") + "\n"
|
|
start = lines.index(" run: |") + 1
|
|
script_lines = lines[start:]
|
|
if any(line and not line.startswith(" ") for line in script_lines):
|
|
raise AssertionError("Unexpected indentation in literal run block")
|
|
return "\n".join(line[10:] if line else "" for line in script_lines) + "\n"
|
|
|
|
|
|
class ProductionDeployPolicyTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.validation = VALIDATION_WORKFLOW.read_text(encoding="utf-8")
|
|
|
|
def test_legacy_production_delivery_workflow_is_retired(self) -> None:
|
|
self.assertFalse(
|
|
(REPOSITORY_ROOT / ".gitea" / "workflows" / "unraid-deploy.yml").exists()
|
|
)
|
|
|
|
def test_managed_validation_uses_the_shared_validation_pool(self) -> None:
|
|
self.assertRegex(self.validation, r"(?m)^\s*runs-on:\s*linux-validation\s*$")
|
|
|
|
def test_managed_validation_runs_policy_before_profile_work(self) -> None:
|
|
policy_step = _named_step(self.validation, "Production delivery policy")
|
|
self.assertIn("python3 -m unittest discover -s .gitea/tests", policy_step)
|
|
self.assertNotRegex(policy_step, r"(?m)^ if:")
|
|
self.assertLess(
|
|
self.validation.index(policy_step),
|
|
self.validation.index(" - name: Validate the requested profile"),
|
|
)
|
|
|
|
def test_full_validation_uses_a_disposable_managed_postgres_boundary(self) -> None:
|
|
job = _indented_block(self.validation, " full:")
|
|
boundary = _named_step(self.validation, "Managed PostgreSQL boundary")
|
|
cleanup = _named_step(self.validation, "Remove managed PostgreSQL boundary")
|
|
|
|
for variable in ("VALIDATION_COMPOSE_PROJECT", "MODELFORGE_POSTGRES_PORT"):
|
|
self.assertRegex(job, rf"(?m)^ {variable}: .+$")
|
|
self.assertIn("modelforge-validation-${{ gitea.run_id }}", job)
|
|
self.assertIn('MODELFORGE_POSTGRES_PORT: "0"', job)
|
|
self.assertIn("env.PROFILE == 'full'", boundary)
|
|
self.assertIn("export MODELFORGE_MIGRATION_DATABASE_URL=", boundary)
|
|
self.assertIn("export MODELFORGE_RUNTIME_DATABASE_URL=", boundary)
|
|
self.assertIn("up --detach --wait postgres", boundary)
|
|
self.assertIn('validation_network="${VALIDATION_COMPOSE_PROJECT}_default"', boundary)
|
|
self.assertIn('job_container="$(hostname)"', boundary)
|
|
self.assertIn('docker network connect "${validation_network}" "${job_container}"', boundary)
|
|
self.assertIn('socket.create_connection(("postgres", 5432)', boundary)
|
|
self.assertNotIn("port postgres 5432", boundary)
|
|
for variable in (
|
|
"MODELFORGE_TEST_POSTGRES_URL",
|
|
"MODELFORGE_TEST_POSTGRES_OWNER_URL",
|
|
"MODELFORGE_TEST_POSTGRES_RUNTIME_URL",
|
|
):
|
|
self.assertIn(f'echo "{variable}=', boundary)
|
|
self.assertIn('>> "${GITHUB_ENV}"', boundary)
|
|
self.assertNotIn("SKIPPED", boundary)
|
|
self.assertIn("always() && env.PROFILE == 'full'", cleanup)
|
|
self.assertIn("docker network disconnect", cleanup)
|
|
self.assertIn('"$(hostname)"', cleanup)
|
|
self.assertIn("down --volumes --remove-orphans", cleanup)
|
|
self.assertIn("export MODELFORGE_MIGRATION_DATABASE_URL=", cleanup)
|
|
self.assertIn("export MODELFORGE_RUNTIME_DATABASE_URL=", cleanup)
|
|
|
|
def test_full_validation_traverses_the_live_schema_and_tests_the_built_console(self) -> None:
|
|
backend = _named_step(self.validation, "Backend — install, lint, typecheck, test")
|
|
console = _named_step(self.validation, "Console — install frozen, typecheck, test, build")
|
|
boundary = _named_step(self.validation, "Managed PostgreSQL boundary")
|
|
|
|
self.assertIn('MODELFORGE_MIGRATION_DATABASE_URL="${MODELFORGE_TEST_POSTGRES_OWNER_URL}"', backend)
|
|
upgrade_commands = re.findall(
|
|
r'"\$\{BACKEND_VENV\}/bin/python" -m alembic ([^\n]+)',
|
|
backend,
|
|
)
|
|
self.assertEqual(
|
|
upgrade_commands,
|
|
["upgrade head", "downgrade 20260828_0022", "upgrade head"],
|
|
)
|
|
self.assertLess(self.validation.index(console), self.validation.index(boundary))
|
|
self.assertLess(self.validation.index(boundary), self.validation.index(backend))
|
|
|
|
def test_managed_postgres_shell_blocks_parse(self) -> None:
|
|
for name in (
|
|
"Managed PostgreSQL boundary",
|
|
"Backend — install, lint, typecheck, test",
|
|
"Remove managed PostgreSQL boundary",
|
|
):
|
|
script = _run_script(_named_step(self.validation, name))
|
|
result = subprocess.run(
|
|
["bash", "-n"],
|
|
input=script.encode("utf-8"),
|
|
capture_output=True,
|
|
check=False,
|
|
)
|
|
with self.subTest(step=name):
|
|
self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace"))
|
|
|
|
def test_compose_projection_uses_explicit_non_secret_validation_inputs(self) -> None:
|
|
projection = _named_step(self.validation, "Compose projections")
|
|
for variable in (
|
|
"MODELFORGE_POSTGRES_ADMIN_PASSWORD",
|
|
"MODELFORGE_MIGRATION_DB_PASSWORD",
|
|
"MODELFORGE_RUNTIME_DB_PASSWORD",
|
|
"MODELFORGE_RUNTIME_DATABASE_URL",
|
|
"MODELFORGE_MIGRATION_DATABASE_URL",
|
|
):
|
|
self.assertIn(f"export {variable}=", projection)
|
|
self.assertIn("docker compose --env-file .env.example", projection)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|