feat(release): define commit-addressed bundle index
prd-number-check / require-numbered-prds (pull_request) Successful in 8s
lint / lint (push) Successful in 59s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Waiting to run
test / coverage (pull_request) Blocked by required conditions
test / image-input-builds (pull_request) Has started running
test / unit (pull_request) Has started running
prd-number-check / require-numbered-prds (pull_request) Successful in 8s
lint / lint (push) Successful in 59s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Waiting to run
test / coverage (pull_request) Blocked by required conditions
test / image-input-builds (pull_request) Has started running
test / unit (pull_request) Has started running
This commit is contained in:
@@ -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",
|
||||
]
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user