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 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", ) 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", ): 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: 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("runtime-smoke-amd64.json", workflow) self.assertIn("runtime-smoke-arm64.json", workflow) 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"], ) 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()