diff --git a/docs/decisions/019-tagging.md b/docs/decisions/019-tagging.md index 3772a68..31c19d9 100644 --- a/docs/decisions/019-tagging.md +++ b/docs/decisions/019-tagging.md @@ -90,6 +90,7 @@ keeps building groups from the `group` output field, the single source of truth. opt-in/playbook tags. `scripts/check-tags.py` (run by `make lint`, covered by `tests/test_check_tags.py`) scans `roles/` and `playbooks/` and fails on any tag outside `{role directory names} ∪ {tests/tags.yml entries}`. +Molecule scenario files (`roles/*/molecule/**`) are excluded from the scan — they are test orchestration, not the production run-targeting surface this standard governs. ## Extending the vocabulary @@ -103,7 +104,7 @@ leaves a paper trail. - Targeted runs are predictable: only two kinds of tags exist, one of them mechanical. - Over-tagging is structurally resisted (closed list + lint enforcement). - Intersection targeting is unavailable by design. -- Authors must keep role tags = role names; the linter enforces it. +- Authors must keep role tags = role names. The linter enforces the *vocabulary* (every tag must be a known role name or an approved tag); the role-tag-equals-role-name rule itself is a convention the linter does not separately check. ## Related diff --git a/scripts/check-tags.py b/scripts/check-tags.py index 74cb20b..e967f8a 100644 --- a/scripts/check-tags.py +++ b/scripts/check-tags.py @@ -52,6 +52,7 @@ def role_names(repo=REPO): def collect_tags(node): """Recursively collect every static tag string under any 'tags:' key.""" + # Matches any dict key literally named `tags`; Ansible-tag semantics assumed. tags = set() if isinstance(node, dict): for key, value in node.items(): @@ -81,7 +82,12 @@ def iter_yaml_files(repo=REPO, scan_dirs=SCAN_DIRS): if not base.is_dir(): continue for ext in ("*.yml", "*.yaml"): - yield from sorted(base.rglob(ext)) + for path in sorted(base.rglob(ext)): + # Molecule scenarios are test orchestration, not the production + # run-targeting surface this standard governs (ADR-019). Skip them. + if "molecule" in path.relative_to(base).parts: + continue + yield path def find_violations(used, allowed): diff --git a/tests/test_check_tags.py b/tests/test_check_tags.py index e25ed4e..e288cf6 100644 --- a/tests/test_check_tags.py +++ b/tests/test_check_tags.py @@ -70,3 +70,16 @@ def test_find_violations_flags_unknown_tag(): def test_find_violations_empty_when_all_allowed(): assert ct.find_violations({"base", "firewall"}, {"base", "firewall"}) == [] + + +def test_iter_yaml_files_skips_molecule(tmp_path): + role = tmp_path / "roles" / "demo" + (role / "tasks").mkdir(parents=True) + (role / "tasks" / "main.yml").write_text("---\n") + mol = role / "molecule" / "default" + mol.mkdir(parents=True) + (mol / "verify.yml").write_text("---\n") + found = list(ct.iter_yaml_files(repo=tmp_path, scan_dirs=("roles",))) + names = [p.name for p in found] + assert "main.yml" in names + assert "verify.yml" not in names