from __future__ import annotations import argparse import importlib.util import json import os from pathlib import Path import shutil import sys import tempfile import unittest from unittest.mock import patch from urllib.error import HTTPError ROOT = Path(__file__).resolve().parents[1] def _load(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module OCI = _load("resolve_oci_platforms", ROOT / "tools/release/resolve-oci-platforms.py") FINALIZE = _load( "finalize_runtime_distribution", ROOT / "tools/release/finalize-runtime-distribution.py", ) DEPLOYER_BUILD = _load( "build_deployer_zipapp", ROOT / "tools/deployment/build-deployer-zipapp.py", ) PUBLISH = _load( "publish_runtime_release", ROOT / "tools/release/publish-runtime-release.py", ) class RuntimeDistributionBuildTests(unittest.TestCase): def test_deployment_zipapp_is_reproducible_across_source_mtimes(self) -> None: with tempfile.TemporaryDirectory(prefix="govoplan-reproducible-zipapp-") as value: root = Path(value) source = root / "source" shutil.copytree(ROOT / "tools/deployment", source) first = root / "first.pyz" second = root / "second.pyz" original_root = DEPLOYER_BUILD.ROOT try: DEPLOYER_BUILD.ROOT = source self.assertEqual(0, DEPLOYER_BUILD.main(["--output", str(first)])) for path in source.rglob("*.py"): os.utime(path, (2_000_000_000, 2_000_000_000)) self.assertEqual(0, DEPLOYER_BUILD.main(["--output", str(second)])) finally: DEPLOYER_BUILD.ROOT = original_root self.assertEqual(first.read_bytes(), second.read_bytes()) def test_workflow_signs_with_the_release_environment(self) -> None: workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text( encoding="utf-8" ) self.assertIn( ".runtime-build/bin/python tools/release/generate-runtime-distribution.py", workflow, ) self.assertNotIn( "\n python tools/release/generate-runtime-distribution.py", 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", "binfmt_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_web_runtime_uses_only_writable_tmpfs_for_nginx_temp_files(self) -> None: nginx = (ROOT / "tools/release/runtime/nginx.conf").read_text( encoding="utf-8" ) for temporary_path in ( "client_body_temp_path /tmp/client_temp;", "fastcgi_temp_path /tmp/fastcgi_temp;", "proxy_temp_path /tmp/proxy_temp;", "scgi_temp_path /tmp/scgi_temp;", "uwsgi_temp_path /tmp/uwsgi_temp;", ): self.assertIn(temporary_path, nginx) def test_workflow_verifies_portable_bootstrap_artifacts_before_execution( self, ) -> None: workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text( encoding="utf-8" ) self.assertIn( "(cd runtime-output && sha256sum govoplan-deploy.pyz > " "govoplan-deploy.pyz.sha256)", workflow, ) self.assertIn("openssl pkeyutl -verify -pubin", workflow) self.assertIn("govoplan-deploy.tampered.pyz", workflow) self.assertLess( workflow.index("openssl pkeyutl -verify -pubin"), workflow.index("python runtime-output/govoplan-deploy.pyz init"), ) def test_workflow_retains_both_platform_runtime_smoke_receipts(self) -> None: workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text( encoding="utf-8" ) self.assertIn('for ARCH in amd64 arm64; do', workflow) self.assertIn("tools/checks/runtime-image-smoke.py", workflow) self.assertIn("Resolve managed dependency platform images", workflow) self.assertIn("--postgres-metadata", workflow) self.assertIn("--redis-metadata", workflow) self.assertIn("Register arm64 execution for runtime smoke", workflow) self.assertIn( 'docker run --privileged --rm "$BINFMT_IMAGE" --install arm64', workflow, ) self.assertIn("runtime-smoke-amd64.json", workflow) self.assertIn("runtime-smoke-arm64.json", workflow) def test_workflow_binds_the_release_tag_to_the_workflow_commit(self) -> None: workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text( encoding="utf-8" ) publisher = (ROOT / "tools/release/publish-runtime-release.py").read_text( encoding="utf-8" ) self.assertIn("SOURCE_COMMIT: ${{ gitea.sha }}", workflow) self.assertIn('--target-commit "$SOURCE_COMMIT"', workflow) self.assertIn('"target_commitish": target_commit', publisher) self.assertIn("self._resolve_commit(tag) != target_commit", publisher) def test_runtime_publisher_rejects_a_tag_on_another_commit(self) -> None: publisher = PUBLISH.GiteaReleasePublisher( base_url="https://git.example.test", owner="GovOPlaN", repo="govoplan", token="secret", ) target = "1" * 40 with ( patch.object( publisher, "_resolve_commit", side_effect=(target, "2" * 40), ), self.assertRaisesRegex(PUBLISH.PublishError, "another commit"), ): publisher.release( tag="v1.2.3", target_commit=target, title="Release", body="Body", ) def test_runtime_publisher_creates_the_tag_at_the_exact_commit(self) -> None: publisher = PUBLISH.GiteaReleasePublisher( base_url="https://git.example.test", owner="GovOPlaN", repo="govoplan", token="secret", ) target = "1" * 40 requests: list[tuple[str, dict[str, object] | None]] = [] def request(method: str, _url: str, **kwargs): payload = kwargs.get("payload") requests.append((method, payload)) if method == "GET": raise HTTPError(_url, 404, "not found", {}, None) return {"id": 1} with ( patch.object( publisher, "_resolve_commit", side_effect=(target, None, target), ), patch.object(publisher, "_json", side_effect=request), ): release = publisher.release( tag="v1.2.3", target_commit=target, title="Release", body="Body", ) self.assertEqual({"id": 1}, release) self.assertEqual("POST", requests[-1][0]) assert requests[-1][1] is not None self.assertEqual(target, requests[-1][1]["target_commitish"]) def test_resolves_platforms_and_builds_evidence_descriptor(self) -> None: index = { "schemaVersion": 2, "manifests": [ { "digest": "sha256:" + "1" * 64, "platform": {"os": "linux", "architecture": "amd64"}, }, { "digest": "sha256:" + "2" * 64, "platform": {"os": "linux", "architecture": "arm64"}, }, ], } metadata = OCI.resolve_platforms( index, repository="registry.example/govoplan/api", index_digest="sha256:" + "a" * 64, ) self.assertEqual( "registry.example/govoplan/api@sha256:" + "1" * 64, metadata["platforms"]["linux/amd64"], ) dependency_metadata = OCI.resolve_platforms( index, repository="registry.example:5000/library/postgres:16-alpine", index_digest="sha256:" + "a" * 64, ) self.assertEqual( "registry.example:5000/library/postgres@sha256:" + "2" * 64, dependency_metadata["platforms"]["linux/arm64"], ) with tempfile.TemporaryDirectory(prefix="govoplan-runtime-finalize-") as value: root = Path(value) composition = { "schema_version": "1", "python": { "packages": [ { "package": "govoplan-core", "version": "1.2.3", "sha256": "8" * 64, } ], "module_ids": ["access"], "wheelhouse_sha256": "9" * 64, "wheel_count": 1, }, "web": {"sha256": "7" * 64, "file_count": 4}, } (root / "composition.json").write_text(json.dumps(composition)) (root / "api.json").write_text(json.dumps(metadata)) web_metadata = { "index": "registry.example/govoplan/web@sha256:" + "b" * 64, "platforms": { "linux/amd64": "registry.example/govoplan/web@sha256:" + "3" * 64, "linux/arm64": "registry.example/govoplan/web@sha256:" + "4" * 64, }, } (root / "web.json").write_text(json.dumps(web_metadata)) deployer = root / "govoplan-deploy.pyz" deployer.write_bytes(b"zipapp") args = argparse.Namespace( composition=root / "composition.json", api_metadata=root / "api.json", web_metadata=root / "web.json", deployer=deployer, deployer_url="https://downloads.example/govoplan-deploy.pyz", artifact_base_url="https://downloads.example/runtime/v1.2.3", source_commit="f" * 40, version="1.2.3", channel="stable", sequence=1, expires_days=30, dependency=[ "postgres=docker.io/library/postgres@sha256:" + "5" * 64, "redis=docker.io/library/redis@sha256:" + "6" * 64, ], output_directory=root / "evidence", descriptor=root / "descriptor.json", ) descriptor = FINALIZE.finalize(args) self.assertEqual(["access"], descriptor["composition"]["module_ids"]) self.assertEqual( "registry.example/govoplan/api@sha256:" + "a" * 64, descriptor["images"]["api"]["index"], ) self.assertTrue((root / "evidence/api-sbom.cdx.json").is_file()) self.assertTrue((root / "evidence/web-provenance.json").is_file()) def test_rejects_incomplete_oci_index(self) -> None: with self.assertRaisesRegex(ValueError, "linux/amd64 and linux/arm64"): OCI.resolve_platforms( { "manifests": [ { "digest": "sha256:" + "1" * 64, "platform": {"os": "linux", "architecture": "amd64"}, } ] }, repository="registry.example/govoplan/api", index_digest="sha256:" + "a" * 64, ) if __name__ == "__main__": unittest.main()