diff --git a/config/repos.yaml b/config/repos.yaml index ad6ef65..f6a53b2 100644 --- a/config/repos.yaml +++ b/config/repos.yaml @@ -90,6 +90,19 @@ required_workflows: workspaces_test: ["Smoke Tests"] # smoke_tests.yml howto: ["Smoke Tests", "Navigator Check"] # smoke_tests.yml + navigator_check.yml +# Version-skew policy: workspace pin vs installed library. Iterated directly +# by heart/checks/version_skew.py — independent of polling (includes unpolled +# repos like the assistant). An adopter replaces this with their own map. +version_skew: + autofit_workspace: {library: PyAutoFit, package: autofit} + autogalaxy_workspace: {library: PyAutoGalaxy, package: autogalaxy} + autolens_workspace: {library: PyAutoLens, package: autolens} + HowToFit: {library: PyAutoFit, package: autofit} + HowToGalaxy: {library: PyAutoGalaxy, package: autogalaxy} + HowToLens: {library: PyAutoLens, package: autolens} + euclid_strong_lens_modeling_pipeline: {library: PyAutoLens, package: autolens} + autolens_assistant: {library: PyAutoLens, package: autolens} + # Excluded — not polled. Listed here for documentation only. excluded: - admin_jammy # personal tooling diff --git a/heart/checks/version_skew.py b/heart/checks/version_skew.py index 00366d3..0f330eb 100644 --- a/heart/checks/version_skew.py +++ b/heart/checks/version_skew.py @@ -50,17 +50,13 @@ _VERSION_RE = re.compile(r'^__version__\s*=\s*["\']([^"\']+)["\']', re.MULTILINE) -# workspace name -> (library repo dir, package dir holding __init__.py) -WORKSPACE_LIBRARY = { - "autofit_workspace": ("PyAutoFit", "autofit"), - "autogalaxy_workspace": ("PyAutoGalaxy", "autogalaxy"), - "autolens_workspace": ("PyAutoLens", "autolens"), - "HowToFit": ("PyAutoFit", "autofit"), - "HowToGalaxy": ("PyAutoGalaxy", "autogalaxy"), - "HowToLens": ("PyAutoLens", "autolens"), - "euclid_strong_lens_modeling_pipeline": ("PyAutoLens", "autolens"), - "autolens_assistant": ("PyAutoLens", "autolens"), -} +def workspace_library(config_path: Path | str = CONFIG_PATH) -> dict[str, tuple[str, str]]: + """workspace name -> (library repo dir, package dir), from the policy + file's ``version_skew`` block. Strict: a missing block is a config bug + and fails loudly rather than silently checking nothing.""" + cfg = yaml.safe_load(Path(config_path).read_text()) or {} + block = cfg["version_skew"] + return {ws: (spec["library"], spec["package"]) for ws, spec in block.items()} def read_library_version(repo: str, pkg: str, root: Path = PYAUTO_ROOT) -> str | None: @@ -126,7 +122,7 @@ def compare(pinned: str | None, installed: str | None) -> str: def run(root: Path = PYAUTO_ROOT) -> dict[str, Any]: workspaces = [] - for workspace, (repo, pkg) in WORKSPACE_LIBRARY.items(): + for workspace, (repo, pkg) in workspace_library().items(): yaml_v, txt_v = read_workspace_pin_sources(workspace, root) if yaml_v is None and txt_v is None: continue # no pin (e.g. *_test workspaces) → not a skew candidate diff --git a/heart/dashboard.py b/heart/dashboard.py index 58d2e39..33742c8 100644 --- a/heart/dashboard.py +++ b/heart/dashboard.py @@ -67,8 +67,20 @@ STALE_AFTER_SECONDS = 3600 # Library repos, used to split the per-repo table into libraries vs workspaces -# when a repo body carries no group label. -DEFAULT_LIBRARIES = ("PyAutoConf", "PyAutoFit", "PyAutoArray", "PyAutoGalaxy", "PyAutoLens") +# when a repo body carries no group label. Derived from the policy file +# (config/repos.yaml `repos.libraries`) — dashboard cannot import readiness +# (cycle), so it reads the same file directly. +def _library_names() -> tuple: + import pathlib + + import yaml + + cfg_path = pathlib.Path(__file__).resolve().parents[1] / "config" / "repos.yaml" + cfg = yaml.safe_load(cfg_path.read_text()) or {} + return tuple(r["name"] for r in cfg["repos"]["libraries"]) + + +DEFAULT_LIBRARIES = _library_names() GATED_WORKSPACE_GROUPS = frozenset({"workspaces", "workspaces_test", "howto"}) # The public Pages board (the badge's entry point). Kept here so every surface diff --git a/heart/readiness.py b/heart/readiness.py index d895a48..b70e5e4 100644 --- a/heart/readiness.py +++ b/heart/readiness.py @@ -92,7 +92,6 @@ CONFIG_PATH = HEART_HOME / "config" / "repos.yaml" RELEASE_READY_FILE = state.HEART_STATE_DIR / "release_ready.json" -DEFAULT_LIBRARIES = ("PyAutoConf", "PyAutoFit", "PyAutoArray", "PyAutoGalaxy", "PyAutoLens") # Workspace repo groups whose required-workflow conclusions on `main` HEAD are a # hard release gate (RED on failure). Libraries are gated separately (the lib @@ -137,13 +136,14 @@ def load_library_names(config_path: Path | str = CONFIG_PATH) -> list[str]: - """Return repos.libraries[].name, or DEFAULT_LIBRARIES if unavailable.""" - try: - cfg = yaml.safe_load(Path(config_path).read_text()) or {} - libs = [r["name"] for r in cfg.get("repos", {}).get("libraries", [])] - return libs or list(DEFAULT_LIBRARIES) - except (OSError, yaml.YAMLError, KeyError, TypeError): - return list(DEFAULT_LIBRARIES) + """Return repos.libraries[].name from the policy file. Strict: the file + is in-repo and the group is load-bearing (the release gate) — a missing + or empty group is a config bug that must fail loudly.""" + cfg = yaml.safe_load(Path(config_path).read_text()) or {} + libs = [r["name"] for r in cfg["repos"]["libraries"]] + if not libs: + raise ValueError(f"empty repos.libraries group in {config_path}") + return libs def _as_int(v: Any, default: int = 0) -> int: @@ -164,7 +164,7 @@ def _as_int(v: Any, default: int = 0) -> int: # The library repos whose current `main` HEAD must match a validation report's # recorded commit_shas for the release-validation gate to go GREEN. -_GATE_SHA_LIBS = DEFAULT_LIBRARIES +_GATE_SHA_LIBS = tuple(load_library_names()) def _sha_eq(a: Any, b: Any) -> bool: diff --git a/tests/test_version_skew.py b/tests/test_version_skew.py index 431b5b6..28424f6 100644 --- a/tests/test_version_skew.py +++ b/tests/test_version_skew.py @@ -115,5 +115,7 @@ def test_run_unknown_when_library_not_checked_out(tmp_path): def test_autolens_assistant_is_a_pinned_workspace(): # Gap closed vs verify_workspace_versions.sh, which covers 8 workspaces. - assert "autolens_assistant" in vs.WORKSPACE_LIBRARY - assert vs.WORKSPACE_LIBRARY["autolens_assistant"] == ("PyAutoLens", "autolens") + # The map now lives in config/repos.yaml `version_skew` (the policy file). + mapping = vs.workspace_library() + assert "autolens_assistant" in mapping + assert mapping["autolens_assistant"] == ("PyAutoLens", "autolens")