233 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			233 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import annotations
 | |
| 
 | |
| import json
 | |
| import tempfile
 | |
| import unittest
 | |
| from collections import OrderedDict
 | |
| from pathlib import Path
 | |
| import sys
 | |
| 
 | |
| sys.path.append(str(Path(__file__).resolve().parent))
 | |
| 
 | |
| from build_release import write_manifest  # type: ignore import-not-found
 | |
| from verify_release import VerificationError, compute_sha256, verify_release
 | |
| 
 | |
| 
 | |
| class VerifyReleaseTests(unittest.TestCase):
 | |
|     def setUp(self) -> None:
 | |
|         self._temp = tempfile.TemporaryDirectory()
 | |
|         self.base_path = Path(self._temp.name)
 | |
|         self.out_dir = self.base_path / "out"
 | |
|         self.release_dir = self.out_dir / "release"
 | |
|         self.release_dir.mkdir(parents=True, exist_ok=True)
 | |
| 
 | |
|     def tearDown(self) -> None:
 | |
|         self._temp.cleanup()
 | |
| 
 | |
|     def _relative_to_out(self, path: Path) -> str:
 | |
|         return path.relative_to(self.out_dir).as_posix()
 | |
| 
 | |
|     def _write_json(self, path: Path, payload: dict[str, object]) -> None:
 | |
|         path.parent.mkdir(parents=True, exist_ok=True)
 | |
|         with path.open("w", encoding="utf-8") as handle:
 | |
|             json.dump(payload, handle, indent=2)
 | |
|             handle.write("\n")
 | |
| 
 | |
|     def _create_sample_release(self) -> None:
 | |
|         sbom_path = self.release_dir / "artifacts/sboms/sample.cyclonedx.json"
 | |
|         sbom_path.parent.mkdir(parents=True, exist_ok=True)
 | |
|         sbom_path.write_text('{"bomFormat":"CycloneDX","specVersion":"1.5"}\n', encoding="utf-8")
 | |
|         sbom_sha = compute_sha256(sbom_path)
 | |
| 
 | |
|         provenance_path = self.release_dir / "artifacts/provenance/sample.provenance.json"
 | |
|         self._write_json(
 | |
|             provenance_path,
 | |
|             {
 | |
|                 "buildDefinition": {"buildType": "https://example/build", "externalParameters": {}},
 | |
|                 "runDetails": {"builder": {"id": "https://example/ci"}},
 | |
|             },
 | |
|         )
 | |
|         provenance_sha = compute_sha256(provenance_path)
 | |
| 
 | |
|         signature_path = self.release_dir / "artifacts/signatures/sample.signature"
 | |
|         signature_path.parent.mkdir(parents=True, exist_ok=True)
 | |
|         signature_path.write_text("signature-data\n", encoding="utf-8")
 | |
|         signature_sha = compute_sha256(signature_path)
 | |
| 
 | |
|         metadata_path = self.release_dir / "artifacts/metadata/sample.metadata.json"
 | |
|         self._write_json(metadata_path, {"digest": "sha256:1234"})
 | |
|         metadata_sha = compute_sha256(metadata_path)
 | |
| 
 | |
|         chart_path = self.release_dir / "helm/stellaops-1.0.0.tgz"
 | |
|         chart_path.parent.mkdir(parents=True, exist_ok=True)
 | |
|         chart_path.write_bytes(b"helm-chart-data")
 | |
|         chart_sha = compute_sha256(chart_path)
 | |
| 
 | |
|         compose_path = self.release_dir.parent / "deploy/compose/docker-compose.dev.yaml"
 | |
|         compose_path.parent.mkdir(parents=True, exist_ok=True)
 | |
|         compose_path.write_text("services: {}\n", encoding="utf-8")
 | |
|         compose_sha = compute_sha256(compose_path)
 | |
| 
 | |
|         debug_file = self.release_dir / "debug/.build-id/ab/cdef.debug"
 | |
|         debug_file.parent.mkdir(parents=True, exist_ok=True)
 | |
|         debug_file.write_bytes(b"\x7fELFDEBUGDATA")
 | |
|         debug_sha = compute_sha256(debug_file)
 | |
| 
 | |
|         debug_manifest_path = self.release_dir / "debug/debug-manifest.json"
 | |
|         debug_manifest = OrderedDict(
 | |
|             (
 | |
|                 ("generatedAt", "2025-10-26T00:00:00Z"),
 | |
|                 ("version", "1.0.0"),
 | |
|                 ("channel", "edge"),
 | |
|                 (
 | |
|                     "artifacts",
 | |
|                     [
 | |
|                         OrderedDict(
 | |
|                             (
 | |
|                                 ("buildId", "abcdef1234"),
 | |
|                                 ("platform", "linux/amd64"),
 | |
|                                 ("debugPath", "debug/.build-id/ab/cdef.debug"),
 | |
|                                 ("sha256", debug_sha),
 | |
|                                 ("size", debug_file.stat().st_size),
 | |
|                                 ("components", ["sample"]),
 | |
|                                 ("images", ["registry.example/sample@sha256:feedface"]),
 | |
|                                 ("sources", ["app/sample.dll"]),
 | |
|                             )
 | |
|                         )
 | |
|                     ],
 | |
|                 ),
 | |
|             )
 | |
|         )
 | |
|         self._write_json(debug_manifest_path, debug_manifest)
 | |
|         debug_manifest_sha = compute_sha256(debug_manifest_path)
 | |
|         (debug_manifest_path.with_suffix(debug_manifest_path.suffix + ".sha256")).write_text(
 | |
|             f"{debug_manifest_sha}  {debug_manifest_path.name}\n", encoding="utf-8"
 | |
|         )
 | |
| 
 | |
|         manifest = OrderedDict(
 | |
|             (
 | |
|                 (
 | |
|                     "release",
 | |
|                     OrderedDict(
 | |
|                         (
 | |
|                             ("version", "1.0.0"),
 | |
|                             ("channel", "edge"),
 | |
|                             ("date", "2025-10-26T00:00:00Z"),
 | |
|                             ("calendar", "2025.10"),
 | |
|                         )
 | |
|                     ),
 | |
|                 ),
 | |
|                 (
 | |
|                     "components",
 | |
|                     [
 | |
|                         OrderedDict(
 | |
|                             (
 | |
|                                 ("name", "sample"),
 | |
|                                 ("image", "registry.example/sample@sha256:feedface"),
 | |
|                                 ("tags", ["registry.example/sample:1.0.0"]),
 | |
|                                 (
 | |
|                                     "sbom",
 | |
|                                     OrderedDict(
 | |
|                                         (
 | |
|                                             ("path", self._relative_to_out(sbom_path)),
 | |
|                                             ("sha256", sbom_sha),
 | |
|                                         )
 | |
|                                     ),
 | |
|                                 ),
 | |
|                                 (
 | |
|                                     "provenance",
 | |
|                                     OrderedDict(
 | |
|                                         (
 | |
|                                             ("path", self._relative_to_out(provenance_path)),
 | |
|                                             ("sha256", provenance_sha),
 | |
|                                         )
 | |
|                                     ),
 | |
|                                 ),
 | |
|                                 (
 | |
|                                     "signature",
 | |
|                                     OrderedDict(
 | |
|                                         (
 | |
|                                             ("path", self._relative_to_out(signature_path)),
 | |
|                                             ("sha256", signature_sha),
 | |
|                                             ("ref", "sigstore://example"),
 | |
|                                             ("tlogUploaded", True),
 | |
|                                         )
 | |
|                                     ),
 | |
|                                 ),
 | |
|                                 (
 | |
|                                     "metadata",
 | |
|                                     OrderedDict(
 | |
|                                         (
 | |
|                                             ("path", self._relative_to_out(metadata_path)),
 | |
|                                             ("sha256", metadata_sha),
 | |
|                                         )
 | |
|                                     ),
 | |
|                                 ),
 | |
|                             )
 | |
|                         )
 | |
|                     ],
 | |
|                 ),
 | |
|                 (
 | |
|                     "charts",
 | |
|                     [
 | |
|                         OrderedDict(
 | |
|                             (
 | |
|                                 ("name", "stellaops"),
 | |
|                                 ("version", "1.0.0"),
 | |
|                                 ("path", self._relative_to_out(chart_path)),
 | |
|                                 ("sha256", chart_sha),
 | |
|                             )
 | |
|                         )
 | |
|                     ],
 | |
|                 ),
 | |
|                 (
 | |
|                     "compose",
 | |
|                     [
 | |
|                         OrderedDict(
 | |
|                             (
 | |
|                                 ("name", "docker-compose.dev.yaml"),
 | |
|                                 ("path", compose_path.relative_to(self.out_dir).as_posix()),
 | |
|                                 ("sha256", compose_sha),
 | |
|                             )
 | |
|                         )
 | |
|                     ],
 | |
|                 ),
 | |
|                 (
 | |
|                     "debugStore",
 | |
|                     OrderedDict(
 | |
|                         (
 | |
|                             ("manifest", "debug/debug-manifest.json"),
 | |
|                             ("sha256", debug_manifest_sha),
 | |
|                             ("entries", 1),
 | |
|                             ("platforms", ["linux/amd64"]),
 | |
|                             ("directory", "debug/.build-id"),
 | |
|                         )
 | |
|                     ),
 | |
|                 ),
 | |
|             )
 | |
|         )
 | |
|         write_manifest(manifest, self.release_dir)
 | |
| 
 | |
|     def test_verify_release_success(self) -> None:
 | |
|         self._create_sample_release()
 | |
|         # Should not raise
 | |
|         verify_release(self.release_dir)
 | |
| 
 | |
|     def test_verify_release_detects_sha_mismatch(self) -> None:
 | |
|         self._create_sample_release()
 | |
|         tampered = self.release_dir / "artifacts/sboms/sample.cyclonedx.json"
 | |
|         tampered.write_text("tampered\n", encoding="utf-8")
 | |
|         with self.assertRaises(VerificationError):
 | |
|             verify_release(self.release_dir)
 | |
| 
 | |
|     def test_verify_release_detects_missing_debug_file(self) -> None:
 | |
|         self._create_sample_release()
 | |
|         debug_file = self.release_dir / "debug/.build-id/ab/cdef.debug"
 | |
|         debug_file.unlink()
 | |
|         with self.assertRaises(VerificationError):
 | |
|             verify_release(self.release_dir)
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     unittest.main()
 |