From 25424187a87e37af6c4f79d310e3cb6566c878eb Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 17:54:09 +0200 Subject: [PATCH] Package runtime migration scripts correctly --- .gitea/workflows/runtime-distribution.yml | 33 +++++++++++++++++++ ...NSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md | 4 +++ tests/test_runtime_distribution_build.py | 31 +++++++++++++++++ tools/release/runtime/Dockerfile.api | 1 + 4 files changed, 69 insertions(+) diff --git a/.gitea/workflows/runtime-distribution.yml b/.gitea/workflows/runtime-distribution.yml index 546ec1a..7c9ac78 100644 --- a/.gitea/workflows/runtime-distribution.yml +++ b/.gitea/workflows/runtime-distribution.yml @@ -53,6 +53,39 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: "22" + - name: Validate immutable release inputs + env: + VERSION: ${{ inputs.version }} + PYTHON_IMAGE: ${{ inputs.python_image }} + NGINX_IMAGE: ${{ inputs.nginx_image }} + POSTGRES_IMAGE: ${{ inputs.postgres_image }} + REDIS_IMAGE: ${{ inputs.redis_image }} + LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }} + MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }} + GARAGE_IMAGE: ${{ inputs.garage_image }} + TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }} + run: | + python - <<'PY' + import os + import re + + version = os.environ["VERSION"] + if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?", version) is None: + raise SystemExit("version must be a SemVer value without a leading v") + image_pattern = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$") + for name in ( + "PYTHON_IMAGE", + "NGINX_IMAGE", + "POSTGRES_IMAGE", + "REDIS_IMAGE", + "LOAD_BALANCER_IMAGE", + "MANAGED_INGRESS_IMAGE", + "GARAGE_IMAGE", + "TEST_MAIL_IMAGE", + ): + if image_pattern.fullmatch(os.environ[name]) is None: + raise SystemExit(f"{name} must be an exact sha256 image reference") + PY - name: Use HTTPS for GovOPlaN repositories run: | git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan" diff --git a/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md b/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md index 73380cd..8d6bfcf 100644 --- a/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md +++ b/docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md @@ -148,6 +148,10 @@ single-file deployer, its detached Ed25519 signature, and a signed, expiring distribution manifest. Evidence generation and signing run through the workflow's isolated release Python environment so their cryptographic tooling is explicit and independent of packages preinstalled in the Actions runner. +The API image points Core at the migration scripts installed from the verified +wheel under `/opt/govoplan/runtime/govoplan_core_runtime`; migrations therefore +do not depend on a source checkout or the build host's Python installation +scheme. Before publication, the exact amd64 and arm64 image manifests each run release migrations against the pinned PostgreSQL image, reach API and WebUI readiness as non-root/read-only processes, and complete a task through the pinned Redis diff --git a/tests/test_runtime_distribution_build.py b/tests/test_runtime_distribution_build.py index 441880c..8003d66 100644 --- a/tests/test_runtime_distribution_build.py +++ b/tests/test_runtime_distribution_build.py @@ -68,6 +68,37 @@ class RuntimeDistributionBuildTests(unittest.TestCase): workflow, ) + def test_workflow_rejects_missing_or_mutable_image_inputs_before_build(self) -> None: + workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text( + encoding="utf-8" + ) + + validation = workflow.index("- name: Validate immutable release inputs") + bootstrap = workflow.index("- name: Bootstrap release sources") + self.assertLess(validation, bootstrap) + self.assertIn('image_pattern = re.compile(r"^[^@\\s]+@sha256:', workflow) + for input_name in ( + "python_image", + "nginx_image", + "postgres_image", + "redis_image", + "load_balancer_image", + "managed_ingress_image", + "garage_image", + "test_mail_image", + ): + self.assertIn(f"inputs.{input_name}", workflow) + + def test_api_runtime_points_core_at_packaged_migration_scripts(self) -> None: + dockerfile = (ROOT / "tools/release/runtime/Dockerfile.api").read_text( + encoding="utf-8" + ) + + self.assertIn( + "GOVOPLAN_CORE_SOURCE_ROOT=/opt/govoplan/runtime/govoplan_core_runtime", + dockerfile, + ) + def test_workflow_verifies_portable_bootstrap_artifacts_before_execution( self, ) -> None: diff --git a/tools/release/runtime/Dockerfile.api b/tools/release/runtime/Dockerfile.api index 81453ab..e952107 100644 --- a/tools/release/runtime/Dockerfile.api +++ b/tools/release/runtime/Dockerfile.api @@ -12,6 +12,7 @@ ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONPATH=/opt/govoplan/runtime \ PATH=/opt/govoplan/runtime/bin:${PATH} \ + GOVOPLAN_CORE_SOURCE_ROOT=/opt/govoplan/runtime/govoplan_core_runtime \ HOME=/var/lib/govoplan COPY wheelhouse/ /opt/govoplan/wheels/