mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
categories.yml moves from roles/ to the repository-root meta/, joining packages.yml. utils/roles/categories.py owns its location plus the tree-walking helpers that were stranded in entity/name.py, so no caller spells the path or duplicates the lookup. meta/distros.yml becomes the SPOT for every distribution: os_family, Galaxy platform name, dev-environment base image, and the pkgmgr/pkgmgr-virgin/environment reference templates. It replaces seven scattered copies (DISTRO_FAMILY, VALID_DISTROS, _DISTROS, _CANONICAL_PLATFORMS, two shell arrays, a Makefile literal). INFINITO_DISTROS is no longer a static default but generated by utils/env/handlers/infinito/distros.py; the shell consumers read it through the env layer. Drops dead CLI surface found along the way: --categories-file and --roles-dir on the provision CLI plus three unused parameters of generate_dynamic_inventory, and the positional roles_file of the invokable categories command. tests/lint/repository/test_distros_spot.py pins the copies that cannot call the SPOT (workflow literals, docs table) and forbids composing a pkgmgr reference outside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
"""Static classifiers used by the `required_by` lint and verifier:
|
|
role invokability (per categories.yml), `required_by` presence in
|
|
`meta/services.yml`, and `# nocheck: <id>` opt-outs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from utils.cache.files import read_text
|
|
from utils.cache.yaml import load_yaml_any
|
|
from utils.roles.categories import categories_file
|
|
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
|
|
def _default_roles_dir(roles_dir: str | Path | None) -> Path:
|
|
return Path(roles_dir) if roles_dir else (PROJECT_ROOT / "roles")
|
|
|
|
|
|
def role_is_invokable(role_id: str, roles_dir: str | Path | None = None) -> bool:
|
|
"""True when any node along `role_id`'s category path has `invokable: true`."""
|
|
if not role_id:
|
|
return False
|
|
spot = categories_file(_default_roles_dir(roles_dir).parent)
|
|
tree_doc = load_yaml_any(str(spot), default_if_missing={})
|
|
tree = tree_doc.get("roles") or {} if isinstance(tree_doc, dict) else {}
|
|
|
|
node = tree
|
|
for seg in str(role_id).split("-"):
|
|
if not seg or not isinstance(node, dict) or seg not in node:
|
|
break
|
|
node = node[seg]
|
|
if isinstance(node, dict) and node.get("invokable") is True:
|
|
return True
|
|
return False
|
|
|
|
|
|
def role_has_required_by(role_id: str, roles_dir: str | Path | None = None) -> bool:
|
|
"""True if any entity in `roles/<role_id>/meta/services.yml` declares
|
|
non-empty `required_by` categories/roles, either flat or nested under a
|
|
`compose:`/`swarm:` mode block."""
|
|
if not role_id:
|
|
return False
|
|
base = _default_roles_dir(roles_dir)
|
|
services_yml = base / str(role_id) / ROLE_FILE_META_SERVICES
|
|
if not services_yml.is_file():
|
|
return False
|
|
data = load_yaml_any(str(services_yml), default_if_missing={})
|
|
if not isinstance(data, dict):
|
|
return False
|
|
for entry in data.values():
|
|
if not isinstance(entry, dict):
|
|
continue
|
|
rb = entry.get("required_by")
|
|
if not isinstance(rb, dict):
|
|
continue
|
|
blocks = (
|
|
[rb[m] for m in ("compose", "swarm") if isinstance(rb.get(m), dict)]
|
|
if ("compose" in rb or "swarm" in rb)
|
|
else [rb]
|
|
)
|
|
if any(b.get("categories") or b.get("roles") for b in blocks):
|
|
return True
|
|
return False
|
|
|
|
|
|
def role_has_nocheck(
|
|
role_id: str,
|
|
check_id: str,
|
|
roles_dir: str | Path | None = None,
|
|
) -> bool:
|
|
"""True if `roles/<role_id>/meta/services.yml` contains
|
|
`# nocheck: <check_id>` (whole-word match)."""
|
|
if not role_id or not check_id:
|
|
return False
|
|
base = _default_roles_dir(roles_dir)
|
|
services_yml = base / str(role_id) / ROLE_FILE_META_SERVICES
|
|
if not services_yml.is_file():
|
|
return False
|
|
pattern = re.compile(rf"#\s*nocheck:\s*{re.escape(check_id)}(?!\S)")
|
|
try:
|
|
return bool(pattern.search(read_text(str(services_yml))))
|
|
except OSError:
|
|
return False
|