#!/usr/bin/env python3 from __future__ import annotations import hashlib import json import re import sys from pathlib import Path, PurePosixPath from typing import Any REPO_ROOT = Path(__file__).resolve().parents[3] if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from scripts import deployment_bundle from deployment_main_flow import CandidateLifecycle, run_deployment_main_flow SCHEMA_VERSION = "test-deployment-authority/v1" STAGING_SCHEMA_VERSION = "staging-deployment-authority/v1" NON_RELEASE_TEST_AUTHORITY = "non_release_test" FORMAL_STAGING_AUTHORITY = "formal_staging" REQUIRED_REFS = ("downstream", "upstream", "official_app_packs") SHA256_RE = re.compile(r"^[0-9a-f]{64}$") RELEASE_ELIGIBILITY_KEYS = { "releasable", "release_eligible", "prod_release_eligible", } class CandidateError(RuntimeError): pass def _canonical_json(payload: object) -> bytes: return json.dumps( payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False, ).encode("utf-8") def _validate_refs(refs: object) -> dict[str, str]: if not isinstance(refs, dict) or set(refs) != set(REQUIRED_REFS): raise CandidateError( "candidate must bind exact downstream, upstream, and official app-pack refs" ) normalized = {key: str(refs[key]).strip() for key in REQUIRED_REFS} if any(re.fullmatch(r"[0-9a-f]{40}", value) is None for value in normalized.values()): raise CandidateError("candidate refs must be exact 40-character commit hashes") return normalized def _reject_release_eligibility(value: object) -> None: if isinstance(value, dict): forbidden = RELEASE_ELIGIBILITY_KEYS.intersection(value) if forbidden: raise CandidateError( "deployment Candidate must not carry release eligibility: " + ", ".join(sorted(forbidden)) ) for nested in value.values(): _reject_release_eligibility(nested) elif isinstance(value, list): for nested in value: _reject_release_eligibility(nested) def _require_sha256(value: object, label: str) -> str: normalized = str(value or "").strip() if not SHA256_RE.fullmatch(normalized): raise CandidateError(f"{label} must be a lowercase SHA-256") return normalized def _load_authority( manifest_path: Path, *, expected_schema_version: str, expected_authority: str, expected_candidate_id: str, expected_environment: str, expected_target: str, ) -> tuple[Path, dict[str, object]]: resolved_manifest = manifest_path.expanduser().resolve() try: payload = json.loads(resolved_manifest.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise CandidateError( f"cannot read candidate manifest: {resolved_manifest}: {exc}" ) from exc if ( not isinstance(payload, dict) or payload.get("schema_version") != expected_schema_version ): raise CandidateError("unsupported candidate manifest schema") if payload.get("authority") != expected_authority: raise CandidateError("Candidate authority does not match the expected environment policy") _reject_release_eligibility(payload) if ( payload.get("environment") != expected_environment or payload.get("target") != expected_target ): raise CandidateError("candidate environment or target mismatch") refs = _validate_refs(payload.get("refs")) candidate_id = _require_sha256(payload.get("candidate_id"), "Candidate ID") expected_candidate_id = _require_sha256( expected_candidate_id, "expected Candidate ID", ) identity_payload = dict(payload) identity_payload.pop("candidate_id", None) computed_candidate_id = hashlib.sha256(_canonical_json(identity_payload)).hexdigest() if candidate_id != computed_candidate_id: raise CandidateError("candidate manifest identity mismatch") if candidate_id != expected_candidate_id: raise CandidateError("candidate does not match expected Candidate ID") bundle = payload.get("bundle") if not isinstance(bundle, dict): raise CandidateError("Candidate deployment bundle binding is missing") if bundle.get("contract_version") != deployment_bundle.BUNDLE_CONTRACT_VERSION: raise CandidateError("unsupported Candidate deployment bundle contract") if bundle.get("receipt_version") != deployment_bundle.BUNDLE_RECEIPT_VERSION: raise CandidateError("unsupported Candidate deployment bundle receipt") relative = PurePosixPath(str(bundle.get("archive_relative_path", ""))) if relative.is_absolute() or ".." in relative.parts or not relative.parts: raise CandidateError("Candidate deployment bundle path is unsafe") for key in ("archive_sha256", "manifest_sha256", "final_pack_hash"): _require_sha256(bundle.get(key), f"Candidate bundle {key}") expected = { "archive_sha256": bundle["archive_sha256"], "manifest_sha256": bundle["manifest_sha256"], "downstream_ref": refs["downstream"], "upstream_ref": refs["upstream"], "official_app_packs_ref": refs["official_app_packs"], "final_pack_hash": bundle["final_pack_hash"], "connect_mcp_url": str(bundle.get("connect_mcp_url", "")).strip(), } payload["refs"] = refs payload["bundle"] = dict(bundle) payload["_bundle_archive_path"] = str( resolved_manifest.parent / Path(*relative.parts) ) payload["_bundle_expected"] = expected return resolved_manifest, payload def _validate_candidate( manifest_path: Path, *, expected_schema_version: str, expected_authority: str, expected_candidate_id: str, expected_environment: str, expected_target: str, ) -> dict[str, object]: resolved_manifest, payload = _load_authority( manifest_path, expected_schema_version=expected_schema_version, expected_authority=expected_authority, expected_candidate_id=expected_candidate_id, expected_environment=expected_environment, expected_target=expected_target, ) archive_path = Path(str(payload.pop("_bundle_archive_path"))) expected = payload.pop("_bundle_expected") assert isinstance(expected, dict) materialized_root = resolved_manifest.parent / "materialized-deployment-bundle" downstream_checkout_root = resolved_manifest.parent / "downstream-source" official_checkout_root = resolved_manifest.parent / "official-app-packs-source" try: layout = deployment_bundle.materialize_deployment_bundle( archive_path, materialized_root, expected=expected, ) downstream_checkout = deployment_bundle.materialize_downstream_checkout( layout, downstream_checkout_root, expected_commit=str(expected["downstream_ref"]), ) deployment_bundle.require_exact_downstream_release_pack_sources( layout, downstream_checkout, ) official_snapshot_root = str(layout["official_app_packs_root"]) official_checkout = deployment_bundle.materialize_official_app_packs_checkout( layout, official_checkout_root, expected_commit=str(expected["official_app_packs_ref"]), ) except deployment_bundle.BundleError as exc: raise CandidateError(str(exc)) from exc layout["official_app_packs_snapshot_root"] = official_snapshot_root layout["official_app_packs_root"] = str(official_checkout) layout["downstream_source_root"] = str(downstream_checkout) payload["bundle_layout"] = layout return payload def validate_candidate( manifest_path: Path, *, expected_candidate_id: str, expected_environment: str, expected_target: str, ) -> dict[str, object]: return _validate_candidate( manifest_path, expected_schema_version=SCHEMA_VERSION, expected_authority=NON_RELEASE_TEST_AUTHORITY, expected_candidate_id=expected_candidate_id, expected_environment=expected_environment, expected_target=expected_target, ) def validate_staging_candidate( manifest_path: Path, *, expected_candidate_id: str, expected_target: str, ) -> dict[str, object]: return _validate_candidate( manifest_path, expected_schema_version=STAGING_SCHEMA_VERSION, expected_authority=FORMAL_STAGING_AUTHORITY, expected_candidate_id=expected_candidate_id, expected_environment="staging", expected_target=expected_target, ) def run_candidate_deployment( manifest_path: Path, *, expected_candidate_id: str, expected_environment: str, expected_target: str, lifecycle: CandidateLifecycle, ) -> dict[str, Any]: candidate = validate_candidate( manifest_path, expected_candidate_id=expected_candidate_id, expected_environment=expected_environment, expected_target=expected_target, ) return run_deployment_main_flow(candidate, lifecycle) def run_staging_candidate_deployment( manifest_path: Path, *, expected_candidate_id: str, expected_target: str, lifecycle: CandidateLifecycle, ) -> dict[str, Any]: candidate = validate_staging_candidate( manifest_path, expected_candidate_id=expected_candidate_id, expected_target=expected_target, ) return run_deployment_main_flow(candidate, lifecycle)