diff --git a/.github/release-blessing-surfaces.json b/.github/release-blessing-surfaces.json new file mode 100644 index 00000000000..cfab0856cee --- /dev/null +++ b/.github/release-blessing-surfaces.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "surfaces": { + "python-backend": { + "display_name": "Python backend", + "blessing_tag_prefix": "python-backend-bless-", + "depends_on": [], + "promotion_workflows": [".github/workflows/gcp_backend.yml"] + }, + "desktop-rust-backend": { + "display_name": "Desktop Rust backend", + "depends_on": ["python-backend"], + "promotion_workflows": [".github/workflows/desktop_promote_prod.yml"] + }, + "desktop-macos": { + "display_name": "macOS Desktop app", + "depends_on": ["desktop-rust-backend", "python-backend"], + "promotion_workflows": [".github/workflows/desktop_promote_prod.yml"] + }, + "flutter-ios": { + "display_name": "Flutter iOS app", + "depends_on": ["python-backend"], + "promotion_workflows": [] + }, + "flutter-android": { + "display_name": "Flutter Android app", + "depends_on": ["python-backend"], + "promotion_workflows": [] + } + } +} diff --git a/.github/scripts/release_blessing.py b/.github/scripts/release_blessing.py new file mode 100644 index 00000000000..217d66d7db4 --- /dev/null +++ b/.github/scripts/release_blessing.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Shared release blessing metadata and dependency helpers.""" + +from __future__ import annotations + +from dataclasses import dataclass +import json +from pathlib import Path + +from desktop_release_metadata import fail + + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_REGISTRY_PATH = ROOT / ".github/release-blessing-surfaces.json" +TRUE_VALUES = {"true", "1", "yes"} + + +@dataclass(frozen=True) +class BlessingSurface: + id: str + display_name: str + depends_on: tuple[str, ...] + promotion_workflows: tuple[str, ...] + + +@dataclass(frozen=True) +class SurfaceBlessing: + surface_id: str + blessed: bool + sha: str + blessed_at: str + tier: str + evidence: str + + +def load_surface_registry(path: Path = DEFAULT_REGISTRY_PATH) -> dict[str, BlessingSurface]: + payload = json.loads(path.read_text(encoding="utf-8")) + raw_surfaces = payload.get("surfaces") + if not isinstance(raw_surfaces, dict) or not raw_surfaces: + fail(f"{path} must contain a non-empty surfaces object") + + surfaces: dict[str, BlessingSurface] = {} + for surface_id, config in raw_surfaces.items(): + if not isinstance(config, dict): + fail(f"surface {surface_id!r} must be an object") + depends_on = tuple(config.get("depends_on") or ()) + promotion_workflows = tuple(config.get("promotion_workflows") or ()) + surfaces[surface_id] = BlessingSurface( + id=surface_id, + display_name=str(config.get("display_name") or surface_id), + depends_on=depends_on, + promotion_workflows=promotion_workflows, + ) + + for surface in surfaces.values(): + for dependency in surface.depends_on: + if dependency not in surfaces: + fail(f"surface {surface.id!r} depends on unknown surface {dependency!r}") + if dependency == surface.id: + fail(f"surface {surface.id!r} cannot depend on itself") + + return surfaces + + +def namespaced_key(surface_id: str, field: str | None = None) -> str: + if field is None or field == "blessed": + return f"blessed.{surface_id}" + return f"blessed.{surface_id}.{field}" + + +def parse_bool(value: str) -> bool: + return value.strip().lower() in TRUE_VALUES + + +def surface_blessing_from_metadata( + metadata: dict[str, str], + surface_id: str, + *, + allow_legacy_desktop: bool = False, +) -> SurfaceBlessing: + if ( + allow_legacy_desktop + and surface_id == "desktop-macos" + and namespaced_key(surface_id) not in metadata + and "blessed" in metadata + ): + return SurfaceBlessing( + surface_id=surface_id, + blessed=parse_bool(metadata.get("blessed", "")), + sha=metadata.get("blessedSha", "").strip(), + blessed_at=metadata.get("blessedAt", "").strip(), + tier=metadata.get("blessedTier", "").strip(), + evidence=metadata.get("blessedEvidence", "").strip(), + ) + + return SurfaceBlessing( + surface_id=surface_id, + blessed=parse_bool(metadata.get(namespaced_key(surface_id), "")), + sha=metadata.get(namespaced_key(surface_id, "sha"), "").strip(), + blessed_at=metadata.get(namespaced_key(surface_id, "at"), "").strip(), + tier=metadata.get(namespaced_key(surface_id, "tier"), "").strip(), + evidence=metadata.get(namespaced_key(surface_id, "evidence"), "").strip(), + ) + + +def require_surface_blessed(blessing: SurfaceBlessing) -> None: + if not blessing.blessed: + fail(f"{blessing.surface_id} must be blessed before prod promotion") + if not blessing.sha: + fail(f"{blessing.surface_id} blessing is missing sha metadata") + if not blessing.blessed_at: + fail(f"{blessing.surface_id} blessing is missing at metadata") + + +def dependency_closure(surface_id: str, surfaces: dict[str, BlessingSurface]) -> list[str]: + if surface_id not in surfaces: + fail(f"unknown blessing surface: {surface_id}") + + ordered: list[str] = [] + visiting: set[str] = set() + visited: set[str] = set() + + def visit(current: str) -> None: + if current in visited: + return + if current in visiting: + fail(f"cycle detected in blessing surface dependencies at {current}") + visiting.add(current) + for dependency in surfaces[current].depends_on: + visit(dependency) + visiting.remove(current) + visited.add(current) + ordered.append(current) + + visit(surface_id) + return ordered + + +def require_blessed_closure(surface_id: str, blessings: dict[str, SurfaceBlessing]) -> None: + surfaces = load_surface_registry() + for required_surface in dependency_closure(surface_id, surfaces): + blessing = blessings.get(required_surface) + if blessing is None: + fail(f"{surface_id} promotion is missing blessing data for {required_surface}") + if blessing.surface_id != required_surface: + fail(f"{surface_id} promotion has {blessing.surface_id} blessing data for {required_surface}") + require_surface_blessed(blessing) diff --git a/.github/scripts/test_release_blessing.py b/.github/scripts/test_release_blessing.py new file mode 100644 index 00000000000..eb2a1ec403c --- /dev/null +++ b/.github/scripts/test_release_blessing.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Tests for shared release blessing helpers.""" + +from __future__ import annotations + +from pathlib import Path +import sys + + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(SCRIPT_DIR)) + +from release_blessing import ( # noqa: E402 + SurfaceBlessing, + dependency_closure, + load_surface_registry, + require_blessed_closure, + surface_blessing_from_metadata, +) + + +def test_registry_loads_and_orders_dependencies() -> None: + surfaces = load_surface_registry() + assert "python-backend" in surfaces + assert dependency_closure("desktop-macos", surfaces) == [ + "python-backend", + "desktop-rust-backend", + "desktop-macos", + ] + + +def test_namespaced_python_backend_metadata() -> None: + blessing = surface_blessing_from_metadata( + { + "blessed.python-backend": "true", + "blessed.python-backend.sha": "abc123", + "blessed.python-backend.at": "2026-07-07T00:00:00Z", + "blessed.python-backend.tier": "unit+contracts", + "blessed.python-backend.evidence": "backend-evidence.json", + }, + "python-backend", + ) + assert blessing.blessed + assert blessing.sha == "abc123" + assert blessing.blessed_at == "2026-07-07T00:00:00Z" + assert blessing.tier == "unit+contracts" + assert blessing.evidence == "backend-evidence.json" + + +def test_legacy_desktop_metadata_still_parses() -> None: + blessing = surface_blessing_from_metadata( + { + "blessed": "true", + "blessedSha": "desktop123", + "blessedAt": "2026-07-07T00:00:00Z", + "blessedTier": "2", + }, + "desktop-macos", + allow_legacy_desktop=True, + ) + assert blessing.blessed + assert blessing.sha == "desktop123" + assert blessing.blessed_at == "2026-07-07T00:00:00Z" + assert blessing.tier == "2" + + +def test_namespaced_desktop_metadata_preferred_over_legacy() -> None: + blessing = surface_blessing_from_metadata( + { + "blessed": "true", + "blessedSha": "stale", + "blessedAt": "2026-07-06T00:00:00Z", + "blessed.desktop-macos": "false", + "blessed.desktop-macos.sha": "current", + "blessed.desktop-macos.at": "2026-07-07T00:00:00Z", + }, + "desktop-macos", + allow_legacy_desktop=True, + ) + assert not blessing.blessed + assert blessing.sha == "current" + assert blessing.blessed_at == "2026-07-07T00:00:00Z" + + +def test_closure_requires_every_surface_blessed() -> None: + blessings = { + "python-backend": SurfaceBlessing("python-backend", True, "py123", "2026-07-07T00:00:00Z", "unit", ""), + "desktop-rust-backend": SurfaceBlessing( + "desktop-rust-backend", + True, + "rust123", + "2026-07-07T00:00:00Z", + "contracts", + "", + ), + } + try: + require_blessed_closure("desktop-macos", blessings) + except SystemExit as exc: + assert "desktop-macos" in str(exc) + else: + raise AssertionError("missing desktop-macos blessing should fail") + + +def test_closure_rejects_mismatched_blessing_surface() -> None: + blessings = { + "python-backend": SurfaceBlessing("desktop-macos", True, "py123", "2026-07-07T00:00:00Z", "unit", ""), + "desktop-rust-backend": SurfaceBlessing( + "desktop-rust-backend", + True, + "rust123", + "2026-07-07T00:00:00Z", + "contracts", + "", + ), + "desktop-macos": SurfaceBlessing("desktop-macos", True, "desk123", "2026-07-07T00:00:00Z", "2", ""), + } + try: + require_blessed_closure("desktop-macos", blessings) + except SystemExit as exc: + assert "desktop-macos promotion has desktop-macos blessing data for python-backend" in str(exc) + else: + raise AssertionError("mismatched blessing surface should fail") + + +if __name__ == "__main__": + test_registry_loads_and_orders_dependencies() + test_namespaced_python_backend_metadata() + test_legacy_desktop_metadata_still_parses() + test_namespaced_desktop_metadata_preferred_over_legacy() + test_closure_requires_every_surface_blessed() + test_closure_rejects_mismatched_blessing_surface() + print("release blessing tests OK")