diff --git a/scripts/check-tags.py b/scripts/check-tags.py index 257f284..74cb20b 100644 --- a/scripts/check-tags.py +++ b/scripts/check-tags.py @@ -29,7 +29,6 @@ def _ignore(loader, tag_suffix, node): _IgnoreUnknownTags.add_multi_constructor("", _ignore) -_IgnoreUnknownTags.add_multi_constructor("!", _ignore) def _static_str(value): @@ -68,5 +67,52 @@ def collect_tags(node): return tags -if __name__ == "__main__": # pragma: no cover - sys.exit(0) +def scan_text(text): + """Collect static tags from a (possibly multi-document) YAML string.""" + found = set() + for doc in yaml.load_all(text, Loader=_IgnoreUnknownTags): + found |= collect_tags(doc) + return found + + +def iter_yaml_files(repo=REPO, scan_dirs=SCAN_DIRS): + for name in scan_dirs: + base = repo / name + if not base.is_dir(): + continue + for ext in ("*.yml", "*.yaml"): + yield from sorted(base.rglob(ext)) + + +def find_violations(used, allowed): + return sorted(used - allowed) + + +def main(): + allowed = load_vocab() | role_names() + violations = [] + for path in iter_yaml_files(): + try: + used = scan_text(path.read_text()) + except yaml.YAMLError as exc: + print(f"warning: could not parse {path}: {exc}", file=sys.stderr) + continue + for tag in find_violations(used, allowed): + violations.append((path.relative_to(REPO), tag)) + + if violations: + print( + "error: Ansible tag(s) not in tests/tags.yml or role names " + "(see docs/decisions/019-tagging.md):", + file=sys.stderr, + ) + for relpath, tag in violations: + print(f" {relpath}: '{tag}'", file=sys.stderr) + print(f"\nallowed: {', '.join(sorted(allowed))}", file=sys.stderr) + sys.exit(1) + + print(f"check-tags: OK ({len(allowed)} tags allowed across {len(SCAN_DIRS)} dirs)") + + +if __name__ == "__main__": + main() diff --git a/tests/test_check_tags.py b/tests/test_check_tags.py index d61b0fc..e25ed4e 100644 --- a/tests/test_check_tags.py +++ b/tests/test_check_tags.py @@ -35,10 +35,38 @@ def test_load_vocab_unions_all_categories(): assert "firewall" in vocab # concern assert "always" in vocab # special assert "bootstrap" in vocab # playbook identity - assert len([c for c in vocab]) >= 12 + assert len(vocab) >= 10 def test_role_names_reads_role_dirs(): names = ct.role_names() assert "base" in names assert "docker_host" in names + + +def test_scan_text_collects_from_yaml_string(): + text = """ +- hosts: all + roles: + - role: base + tags: [base] + tasks: + - name: open port + tags: [firewall] +""" + assert ct.scan_text(text) == {"base", "firewall"} + + +def test_scan_text_tolerates_custom_yaml_tags(): + text = "- name: t\n secret: !vault xxx\n tags: [users]\n" + assert ct.scan_text(text) == {"users"} + + +def test_find_violations_flags_unknown_tag(): + allowed = {"base", "firewall"} + used = {"base", "frewall"} # typo + assert ct.find_violations(used, allowed) == ["frewall"] + + +def test_find_violations_empty_when_all_allowed(): + assert ct.find_violations({"base", "firewall"}, {"base", "firewall"}) == []