feat(roles): pull custom roles from inventory-declared sources

An inventory group web-app-cst-<name> with applications.<id>.custom: true and
a pinned source {url, version, path} is pulled into roles/ before the play,
read raw (Jinja, Ansible tags and credential URLs refused), placed through a
staging directory under an exclusive lock, and owned by a marker that names
its own directory, so a pull or roles-clean never replaces or removes a
directory it did not place and a failed pull leaves roles/ as it was.
Directory inventories, host lists and non-UTF-8 files behave as Ansible
reads them; generated test deploys pass --skip-pull. make roles-pull,
roles-clean and roles list, INFINITO_EXTRA_MOUNTS for local sources, the
web-app-cst category, the reserved prefix lint and a central
repository_files() that also yields the gitignored custom roles let every
lint scan them like in-tree roles. Supersedes the uncommitted 045 External
Roles draft and reuses its fetch, sync and mount design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-29 00:02:58 +01:00
1 parent cb4d340de9
commit d92375922d
101 files changed
+3731 -248

No files matched your search

@@ -2,11 +2,10 @@
from __future__ import annotations
import subprocess
import unittest
from pathlib import Path
from utils.cache.files import iter_project_files
from utils.cache.files import iter_project_files, repository_files
from . import PROJECT_ROOT
@@ -21,14 +20,7 @@ class TestPythonShebangs(unittest.TestCase):
def _tracked_files(self) -> list[Path]:
try:
out = subprocess.check_output(
["git", "-C", str(self.repo_root), "ls-files", "-z"],
stderr=subprocess.STDOUT,
)
rel_paths = [
p for p in out.decode("utf-8", errors="replace").split("\0") if p
]
return [self.repo_root / p for p in rel_paths]
return [self.repo_root / p for p in repository_files(untracked=True)]
except Exception:
return [Path(p) for p in iter_project_files()]