mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`, `requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from non-Markdown source files. Comments anchored to requirement numbers rot when requirements are renumbered or rewritten and conflate present-state behaviour (which the comment should describe) with the audit trail of how the code got there (which belongs in commit messages, PR descriptions, and the requirement file itself). * Document the rule in `docs/contributing/documentation.md` under "Comments". Code MUST describe the *why* in present-state language; contract enforcement lives in tests, not folklore. * Clean 254 source files across `roles/`, `tests/`, `plugins/`, `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root `eslint.config.js`. Pure comment / docstring / log-message edits; no behavioural change. * Add `tests/lint/repository/test_no_req_references_in_code.py` to enforce the rule going forward across `.js`, `.py`, `.yml`, `.yaml`, `.j2`, and `.sh` files. The legacy `test_no_stale_requirement_refs.py` is exempt as the only file whose purpose is to detect these very tokens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.8 KiB
Python
94 lines
3.8 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def compute_application_gid(application_id, roles_dir="roles", base_gid=10000):
|
|
"""Pure-Python GID resolver — no ansible dependency.
|
|
|
|
Sorts every `<roles_dir>/<role>/meta/services.yml`-bearing role
|
|
alphabetically by `<role>` and returns ``base_gid + index`` for the
|
|
requested ``application_id``. Extracted so callers that only need
|
|
the GID computation (e.g. ``utils.cache.applications._build_variants`` on
|
|
the GitHub Actions runner host, where the runner Python ships
|
|
without ``ansible``) can import THIS function instead of the
|
|
``LookupModule`` class — that one transitively pulls
|
|
``ansible.plugins.lookup.LookupBase`` and raises
|
|
``ModuleNotFoundError`` on ansible-less hosts.
|
|
|
|
Raises ``ValueError`` (not ``AnsibleError``) for portability.
|
|
"""
|
|
if not Path(roles_dir).is_dir():
|
|
raise ValueError(f"Roles directory '{roles_dir}' not found")
|
|
|
|
# Per, an "application role" is identified by the presence of
|
|
# at least one of the project-owned `meta/<topic>.yml` files (services,
|
|
# server, rbac, volumes, schema, users). This preserves the prior
|
|
# assignment ordering: every role that previously had `meta/services.yml`
|
|
# now has at least one of these files.
|
|
application_marker_files = {
|
|
"services.yml",
|
|
"server.yml",
|
|
"rbac.yml",
|
|
"volumes.yml",
|
|
"schema.yml",
|
|
"users.yml",
|
|
}
|
|
discovered: set[str] = set()
|
|
for entry in os.listdir(roles_dir):
|
|
role_dir = str(Path(roles_dir) / entry)
|
|
meta_dir = str(Path(role_dir) / "meta")
|
|
if not Path(meta_dir).is_dir():
|
|
continue
|
|
for marker in application_marker_files:
|
|
if Path(str(Path(meta_dir) / marker)).is_file():
|
|
discovered.add(entry)
|
|
break
|
|
sorted_ids = sorted(discovered)
|
|
|
|
try:
|
|
index = sorted_ids.index(application_id)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Application ID '{application_id}' not found in any role"
|
|
) from None
|
|
|
|
return base_gid + index
|
|
|
|
|
|
# The Ansible LookupModule wrapper. We define it conditionally on
|
|
# ansible being importable: ansible's plugin loader only needs the class
|
|
# when an Ansible process actually loads this module via the lookup
|
|
# loader, and that always happens inside a process that has ansible
|
|
# installed (the playbook runner). Pure-Python importers (e.g.
|
|
# `utils.cache.applications._build_variants` running inside `cli.administration.deploy.
|
|
# development init` on the GitHub Actions runner host) want
|
|
# `compute_application_gid` only and MUST NOT pay the ansible-import
|
|
# cost — see CI run 24935979190 for the regression that motivated this
|
|
# split.
|
|
try:
|
|
from ansible.errors import AnsibleError
|
|
from ansible.plugins.lookup import LookupBase
|
|
|
|
class LookupModule(LookupBase):
|
|
def run(self, terms, variables=None, **kwargs):
|
|
application_id = terms[0]
|
|
base_gid = kwargs.get("base_gid", 10000)
|
|
roles_dir = kwargs.get("roles_dir", "roles")
|
|
|
|
try:
|
|
return [compute_application_gid(application_id, roles_dir, base_gid)]
|
|
except ValueError as exc:
|
|
raise AnsibleError(str(exc)) from exc
|
|
|
|
except ImportError: # pragma: no cover - exercised on ansible-less hosts only
|
|
# Sentinel so callers that *try* to instantiate the lookup outside
|
|
# an Ansible process get a clear, actionable error instead of a
|
|
# confusing AttributeError or NameError.
|
|
class LookupModule: # type: ignore[no-redef]
|
|
def __init__(self, *args, **kwargs):
|
|
raise RuntimeError(
|
|
"plugins.lookup.application_gid.LookupModule requires "
|
|
"ansible at runtime. Use compute_application_gid() "
|
|
"directly for ansible-less code paths."
|
|
)
|