Files
computer-playbook/utils/roles/required_by_coverage.py
T
kevinveenbirkenbachandClaude Opus 4.7 053fedcbeb refactor(required_by-lint): SPOT helpers + yaml-aware mailu test
- Move role_is_invokable / role_has_required_by / role_has_nocheck out
  of plugins/filter (where they were flagged as test-only filters) into
  utils/roles/required_by_coverage.py. All three use load_yaml_any,
  read_text, and ROLE_FILE_META_SERVICES from the canonical SPOTs.
- Refactor the verifier (cli.meta.roles.services.called) the same way:
  iterate roles/*/ directly, look up meta/services.yml via the constant,
  parse with load_yaml_any, and tolerate a missing roles_dir (unit tests
  point at a fake /repo).
- Make the mailu-dependency integration test yaml-aware for meta/
  services.yml: walk the parsed tree and skip values inside any
  `required_by` subtree. `required_by` is inverse coverage metadata for
  the deploy verifier, not a "consumer of mailu" reference, so previous
  regex-on-text matching produced false positives for cert-deploy.
- sys-ctl-mtn-cert-renew is gated by `lookup('tls', application_id,
  'mode') == 'letsencrypt'`, which the verifier can't express via
  role-id/category match — mark nocheck with the reason inline.
- Add README.md for cli/meta/roles/services (CLI-category convention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 04:23:45 +02:00

81 lines
2.7 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.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
base = _default_roles_dir(roles_dir)
tree_doc = load_yaml_any(str(base / "categories.yml"), 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
`required_by.categories` or `required_by.roles` (non-empty)."""
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
if rb.get("categories") or rb.get("roles"):
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