mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 19:32:17 +00:00
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
"""Category-tree SPOT.
|
|
|
|
``meta/categories.yml`` at the repository root declares the nested
|
|
category tree every role name is derived from: the dash-joined path of a
|
|
node is the role's category prefix (``web`` -> ``app`` yields
|
|
``web-app-*``), and nodes carry the ``invokable``, ``stage``, ``modes``
|
|
and ``run_after`` policy the dispatcher, the stage lookup and the
|
|
inventory generator read.
|
|
|
|
This module owns the file's location and the tree-walking primitives.
|
|
Consumers MUST resolve the path through :func:`categories_file` rather
|
|
than spelling it themselves, so a future move touches one line.
|
|
|
|
:data:`FILE_META_CATEGORIES` stays repository-root *relative* and is not
|
|
pre-joined onto ``PROJECT_ROOT`` at import time on purpose: the root is
|
|
not fixed. ``utils.roles.validation.invokable`` is exercised against a
|
|
patched ``PROJECT_ROOT`` and test fixtures build the file inside a
|
|
temporary tree. A module level absolute constant would freeze the root at
|
|
import time and silently ignore both.
|
|
|
|
The file is not optional. It lives beside ``roles/``, which the packaging
|
|
config excludes from the wheel just as it excludes ``meta/``, so any
|
|
context that can resolve a role name also carries this file. A missing
|
|
file is a broken checkout and raises rather than degrading to an empty
|
|
category tree, which would silently strip every category prefix off every
|
|
role name.
|
|
|
|
Keep this module free of Ansible imports and cheap to import: it is
|
|
pulled in by ``plugins/filter/invokable_paths.py``, which runs in
|
|
contexts where Ansible is not installed (see
|
|
``tests/unit/python/utils/cache/test_data.py``).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.cache import PROJECT_ROOT
|
|
from utils.cache.yaml import load_yaml
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
FILE_META_CATEGORIES: str = "meta/categories.yml"
|
|
|
|
|
|
def categories_file(root: Path | None = None) -> Path:
|
|
"""Absolute path of the category file below *root*.
|
|
|
|
Args:
|
|
root: tree to resolve against; the repository root when omitted.
|
|
"""
|
|
return (PROJECT_ROOT if root is None else root) / FILE_META_CATEGORIES
|
|
|
|
|
|
def load_categories_tree(categories_file):
|
|
"""Return the ``roles`` mapping of the category file.
|
|
|
|
Args:
|
|
categories_file: path-like of a ``meta/categories.yml``.
|
|
"""
|
|
return load_yaml(categories_file)["roles"]
|
|
|
|
|
|
def flatten_categories(tree, prefix=""):
|
|
"""Flattens nested category tree to all possible category paths."""
|
|
result = []
|
|
for k, v in tree.items():
|
|
current = f"{prefix}-{k}" if prefix else k
|
|
result.append(current)
|
|
if isinstance(v, dict):
|
|
for sk, sv in v.items():
|
|
if isinstance(sv, dict):
|
|
result.extend(flatten_categories({sk: sv}, current))
|
|
return result
|