From 46b7e06b37a38a01bd744ca3c064c3e367e253d1 Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 27 Jul 2026 17:00:56 +0000 Subject: [PATCH] feat(release): define commit-addressed bundle index --- bot_bottle/release_bundle.py | 123 +++++++++++++++++++++++++++++ scripts/generate_release_bundle.py | 34 ++++++++ tests/unit/test_release_bundle.py | 60 ++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 bot_bottle/release_bundle.py create mode 100644 scripts/generate_release_bundle.py create mode 100644 tests/unit/test_release_bundle.py diff --git a/bot_bottle/release_bundle.py b/bot_bottle/release_bundle.py new file mode 100644 index 00000000..4ee56709 --- /dev/null +++ b/bot_bottle/release_bundle.py @@ -0,0 +1,123 @@ +"""External, commit-addressed release bundle metadata.""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path +from typing import Any + +from .release_manifest import ReleaseManifestError, parse_manifest + +_COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") +_SHA_RE = re.compile(r"^[0-9a-f]{64}$") + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1 << 20), b""): + digest.update(chunk) + return digest.hexdigest() + + +def build_bundle_index( + manifest_data: Any, + *, + wheel: Path, + wheel_url: str, + workflow_run: str, + published_at: str, +) -> dict[str, Any]: + """Build and validate the immutable external index for one source commit.""" + manifest = parse_manifest(manifest_data) + if not wheel.is_file() or wheel.suffix != ".whl": + raise ReleaseManifestError("release bundle wheel must be one .whl file") + if not wheel_url.startswith("https://") or not wheel_url.endswith(wheel.name): + raise ReleaseManifestError( + "release bundle wheel URL must be HTTPS and end with the wheel filename") + if not workflow_run.strip() or not published_at.strip(): + raise ReleaseManifestError( + "release bundle provenance requires workflow_run and published_at") + return { + "schema": 1, + "source_commit": manifest.source_commit, + "wheel": { + "filename": wheel.name, + "url": wheel_url, + "sha256": sha256_file(wheel), + }, + "oci": { + "orchestrator": manifest.orchestrator_image, + "gateway": manifest.gateway_image, + }, + "firecracker": { + "orchestrator": { + "version": manifest.firecracker_orchestrator.version, + "sha256": manifest.firecracker_orchestrator.sha256, + }, + "gateway": { + "version": manifest.firecracker_gateway.version, + "sha256": manifest.firecracker_gateway.sha256, + }, + }, + "provenance": { + "workflow_run": workflow_run.strip(), + "published_at": published_at.strip(), + }, + "qualifications": [], + } + + +def parse_bundle_index(data: Any) -> dict[str, Any]: + """Validate an index before publication or installer consumption.""" + if not isinstance(data, dict) or data.get("schema") != 1: + raise ReleaseManifestError("release bundle index must use schema 1") + commit = data.get("source_commit") + if not isinstance(commit, str) or _COMMIT_RE.fullmatch(commit) is None: + raise ReleaseManifestError( + "release bundle source_commit must be 40 lowercase hex") + wheel = data.get("wheel") + if not isinstance(wheel, dict): + raise ReleaseManifestError("release bundle wheel must be an object") + filename, url, digest = ( + wheel.get("filename"), wheel.get("url"), wheel.get("sha256")) + if not isinstance(filename, str) or not filename.endswith(".whl"): + raise ReleaseManifestError("release bundle wheel filename must end in .whl") + if not isinstance(url, str) or not url.startswith("https://") or not url.endswith(filename): + raise ReleaseManifestError( + "release bundle wheel URL must be HTTPS and match its filename") + if not isinstance(digest, str) or _SHA_RE.fullmatch(digest) is None: + raise ReleaseManifestError( + "release bundle wheel sha256 must be 64 lowercase hex") + manifest_data = { + "schema": 1, + "source_commit": commit, + "oci": data.get("oci"), + "firecracker": data.get("firecracker"), + } + parse_manifest(manifest_data) + provenance = data.get("provenance") + if not isinstance(provenance, dict) or not all( + isinstance(provenance.get(key), str) and provenance[key].strip() + for key in ("workflow_run", "published_at") + ): + raise ReleaseManifestError("release bundle provenance is incomplete") + qualifications = data.get("qualifications") + if not isinstance(qualifications, list): + raise ReleaseManifestError("release bundle qualifications must be an array") + return data + + +def canonical_bytes(data: Any) -> bytes: + parse_bundle_index(data) + return (json.dumps(data, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +__all__ = [ + "build_bundle_index", + "canonical_bytes", + "parse_bundle_index", + "sha256_file", +] diff --git a/scripts/generate_release_bundle.py b/scripts/generate_release_bundle.py new file mode 100644 index 00000000..13379249 --- /dev/null +++ b/scripts/generate_release_bundle.py @@ -0,0 +1,34 @@ +"""Generate the external commit-addressed release bundle index.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from bot_bottle.release_bundle import build_bundle_index, canonical_bytes + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", required=True, type=Path) + parser.add_argument("--wheel", required=True, type=Path) + parser.add_argument("--wheel-url", required=True) + parser.add_argument("--workflow-run", required=True) + parser.add_argument("--published-at", required=True) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args(argv) + manifest = json.loads(args.manifest.read_text(encoding="utf-8")) + index = build_bundle_index( + manifest, + wheel=args.wheel, + wheel_url=args.wheel_url, + workflow_run=args.workflow_run, + published_at=args.published_at, + ) + args.output.write_bytes(canonical_bytes(index)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_release_bundle.py b/tests/unit/test_release_bundle.py new file mode 100644 index 00000000..a0d60ae0 --- /dev/null +++ b/tests/unit/test_release_bundle.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from bot_bottle.release_bundle import ( + build_bundle_index, + canonical_bytes, + parse_bundle_index, +) +from bot_bottle.release_manifest import ReleaseManifestError +from tests.unit.test_release_manifest import manifest + + +class TestReleaseBundle(unittest.TestCase): + def test_builds_commit_addressed_index(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + wheel = Path(tmp) / "bot_bottle-0.1.0-py3-none-any.whl" + wheel.write_bytes(b"wheel") + index = build_bundle_index( + manifest(), + wheel=wheel, + wheel_url=f"https://packages.example/{wheel.name}", + workflow_run="actions/123", + published_at="2026-07-27T00:00:00Z", + ) + self.assertEqual("1" * 40, index["source_commit"]) + self.assertEqual(64, len(index["wheel"]["sha256"])) + self.assertEqual([], index["qualifications"]) + self.assertIs(index, parse_bundle_index(index)) + self.assertTrue(canonical_bytes(index).endswith(b"\n")) + + def test_rejects_mutable_or_mismatched_wheel_url(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + wheel = Path(tmp) / "release.whl" + wheel.write_bytes(b"wheel") + with self.assertRaisesRegex(ReleaseManifestError, "HTTPS"): + build_bundle_index( + manifest(), + wheel=wheel, + wheel_url="http://packages.example/other.whl", + workflow_run="actions/123", + published_at="now", + ) + + def test_rejects_index_with_changed_runtime_identity(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + wheel = Path(tmp) / "release.whl" + wheel.write_bytes(b"wheel") + index = build_bundle_index( + manifest(), + wheel=wheel, + wheel_url=f"https://packages.example/{wheel.name}", + workflow_run="actions/123", + published_at="now", + ) + index["oci"]["gateway"] = "registry/gateway:latest" + with self.assertRaisesRegex(ReleaseManifestError, "digest-pinned"): + parse_bundle_index(index)