mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
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>
92 lines
3.4 KiB
Python
Executable File
92 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils.cache.files import iter_project_files, repository_files
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
|
|
class TestPythonShebangs(unittest.TestCase):
|
|
EXPECTED_PYTHON_SHEBANG = "#!/usr/bin/env python3"
|
|
EXPECTED_ANSIBLE_MODULE_SHEBANG = "#!/usr/bin/python"
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.repo_root = PROJECT_ROOT
|
|
|
|
def _tracked_files(self) -> list[Path]:
|
|
try:
|
|
return [self.repo_root / p for p in repository_files(untracked=True)]
|
|
except Exception:
|
|
return [Path(p) for p in iter_project_files()]
|
|
|
|
@staticmethod
|
|
def _first_line(path: Path) -> str:
|
|
with path.open("rb") as fh:
|
|
line = fh.readline(4096)
|
|
return line.decode("utf-8", errors="replace").strip()
|
|
|
|
@staticmethod
|
|
def _is_ansible_custom_module(path: Path, repo_root: Path) -> bool:
|
|
rel = path.relative_to(repo_root)
|
|
parts = rel.parts
|
|
if parts[0] == "library" or "library" in parts[1:]:
|
|
return True
|
|
return len(parts) >= 2 and parts[0] == "plugins" and parts[1] == "modules"
|
|
|
|
def test_python_shebangs_are_portable(self):
|
|
portable_violations: list[str] = []
|
|
ansible_module_violations: list[str] = []
|
|
|
|
for path in self._tracked_files():
|
|
if not path.is_file():
|
|
continue
|
|
|
|
first_line = self._first_line(path)
|
|
if not first_line.startswith("#!"):
|
|
continue
|
|
|
|
if "python" not in first_line.lower():
|
|
continue
|
|
|
|
rel = path.relative_to(self.repo_root).as_posix()
|
|
if self._is_ansible_custom_module(path, self.repo_root):
|
|
if first_line != self.EXPECTED_ANSIBLE_MODULE_SHEBANG:
|
|
ansible_module_violations.append(f"{rel}: {first_line}")
|
|
elif first_line != self.EXPECTED_PYTHON_SHEBANG:
|
|
portable_violations.append(f"{rel}: {first_line}")
|
|
|
|
if portable_violations or ansible_module_violations:
|
|
self.fail(
|
|
"Found invalid Python shebangs.\n\n"
|
|
"Why this is necessary:\n"
|
|
"- For normal Python scripts use '#!/usr/bin/env python3' so the interpreter "
|
|
"is resolved via PATH and remains portable across distros/images.\n"
|
|
"- For Ansible custom modules in any `library/` directory keep "
|
|
"'#!/usr/bin/python': Ansible rewrites this interpreter path to the configured "
|
|
"host interpreter.\n"
|
|
"- Using '#!/usr/bin/env python3' in Ansible modules can break execution with "
|
|
"errors like: module interpreter '/usr/bin/env python3' was not found.\n\n"
|
|
+ (
|
|
"Expected '#!/usr/bin/env python3' (non-library Python scripts), but found:\n- "
|
|
+ "\n- ".join(portable_violations)
|
|
+ "\n\n"
|
|
if portable_violations
|
|
else ""
|
|
)
|
|
+ (
|
|
"Expected '#!/usr/bin/python' (Ansible custom modules in library/), but found:\n- "
|
|
+ "\n- ".join(ansible_module_violations)
|
|
if ansible_module_violations
|
|
else ""
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|