utils.roles package¶
Subpackages¶
- utils.roles.applications package
- Subpackages
- utils.roles.applications.services package
- Submodules
- utils.roles.applications.services.config module
- utils.roles.applications.services.database module
- utils.roles.applications.services.engines module
- utils.roles.applications.services.errors module
- utils.roles.applications.services.registry module
- utils.roles.applications.services.resolver module
- utils.roles.applications.services.resources module
- utils.roles.applications.services.sso module
- Module contents
- utils.roles.applications.services package
- Submodules
- utils.roles.applications.config module
- utils.roles.applications.in_group_deps module
- utils.roles.applications.mcp module
- utils.roles.applications.mounts module
- utils.roles.applications.security_headers module
- utils.roles.applications.status_codes module
- utils.roles.applications.topics module
- utils.roles.applications.variants module
- Module contents
- Subpackages
- utils.roles.entity package
- utils.roles.rbac package
- utils.roles.validation package
Submodules¶
utils.roles.categories module¶
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 categories_file() rather
than spelling it themselves, so a future move touches one line.
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).
- utils.roles.categories.categories_file(root: Path | None = None) Path¶
Absolute path of the category file below root.
- Parameters:
root – tree to resolve against; the repository root when omitted.
- utils.roles.categories.flatten_categories(tree, prefix='')¶
Flattens nested category tree to all possible category paths.
- utils.roles.categories.load_categories_tree(categories_file)¶
Return the
rolesmapping of the category file.- Parameters:
categories_file – path-like of a
meta/categories.yml.
utils.roles.credits module¶
Attribution recorded in markdown ## Credits sections.
The link that belongs to a credited person lives nowhere but the Credits
lines themselves: galaxy_info.author carries the name without a URL, and
galaxy_info.company names the project owner rather than the role’s
author, so neither can supply it. This module derives the name -> URL map
from the linked mentions already present in the repository, which keeps the
README generator and the credits-attribution lint reading the same
source instead of a hand-maintained list.
Adding a contributor therefore needs no code change: one linked credit anywhere makes the generator emit that link for every role they author.
Patterns exported here:
LINKED_RE[Name](https://url)plus the bold markers a Creditsline wraps it in.
BARE_BOLD_RE**Name**that is not itself a link’s text.PERSON_REa multi-word proper name whose parts carry no dot. Thedot is what separates people from the products and projects that also appear in Credits lines (“Infinito.Nexus Project”). A person written with an initial (“J. Smith”) therefore falls out of enforcement rather than into a false positive, which is the safe direction for a lint.
- utils.roles.credits.author_urls() dict[str, str]¶
Name -> URL for every person credited with exactly one URL repo-wide.
Names linked to conflicting URLs are omitted, so an ambiguous mapping never silently picks a winner.
- utils.roles.credits.collect_author_urls(documents: list[list[str]]) dict[str, set[str]]¶
Map every credited person to the set of URLs they are linked with.
A name with more than one URL is a conflict the caller decides about; this function reports rather than resolves it.
- Parameters:
documents – one entry per file, each the file’s lines.
- utils.roles.credits.credits_lines(lines: list[str]) list[tuple[int, str]]¶
(1-based line number, text)for every line inside a## Creditssection, ending at the next heading of any level.- Parameters:
lines – the file’s lines, without trailing newlines.
- utils.roles.credits.is_person(name: str) bool¶
- utils.roles.credits.iter_markdown_documents() Iterator[tuple[str, list[str]]]¶
Yield
(path, lines)for every tracked markdown file.Files the process cannot read are skipped rather than raised: this walk backs the README build, and an unreadable agent scratch file such as
.claude/loop.mdmust not abort generating documentation.
utils.roles.dependency_resolver module¶
- class utils.roles.dependency_resolver.RoleDependencyResolver(roles_dir: str)¶
Bases:
object- get_role_dependencies(role_name: str, *, resolve_include_role: bool = True, resolve_import_role: bool = True, resolve_dependencies: bool = True, resolve_run_after: bool = False) set[str]¶
- resolve_transitively(start_roles: Iterable[str], *, resolve_include_role: bool = True, resolve_import_role: bool = True, resolve_dependencies: bool = True, resolve_run_after: bool = False, max_depth: int | None = None) set[str]¶
utils.roles.deploy module¶
Deploy-shape helpers shared by the complexity matrix and the Cosmos builder.
Both need the same facts about a role: whether it ships its own container stack, and which deploy modes it supports. Keeping the logic here means the two callers never drift.
- utils.roles.deploy.role_deploy_modes(role_dir: Path, role_name: str | None = None) dict[str, bool]¶
Return the deploy modes a role supports, keyed by mode with its
modes.<mode>.enabledSPOT value.A stack role (ships a compose template) is offered
composeandswarm; a host-only role is offeredhost. The two are mutually exclusive: swarm needs a stack, host configures the machine instead.
- utils.roles.deploy.role_has_stack(role_dir: Path) bool¶
True iff the role renders its own container stack, i.e. ships a
templates/*compose*.yml.j2. Host-only roles (backup cron, wireguard, swapfile) and pure service-injectors carry no compose template and return False. Presence (not animagekey) is the signal so build-from-source stacks that pull no registry image still count as a stack.
utils.roles.display module¶
Codec between a role id and the display name CI names carry.
A role id is the machine handle (web-app-nextcloud); the display name is
what a human reads off a GitHub Actions job or run title
(网络应用·Nextcloud). The category path renders as the hanzi labels
meta/categories.yml declares, run together into one block, then a ·,
then the role’s own README.md heading or the part of the id the categories
do not already carry.
The name never holds a space: the whitelist and priority workflow
inputs are space-separated lists, so a display name with a space in it would
make a two-role list unparseable.
RoleDisplayName.decode() accepts a raw role id unchanged, so every
consumer can decode unconditionally and a human dispatching a run by hand
never has to type hanzi.
- class utils.roles.display.RoleDisplayName(root: Path | None = None)¶
Bases:
objectEncode role ids to display names and back, for one repository tree.
- category_labels(app_id: str) list[str]¶
The
hanzilabel of every category the role id walks through.- Parameters:
app_id – role id, e.g.
'web-app-nextcloud'.
- decode(text: str) str | None¶
The role id text names, or None when it names no role.
Accepts a display name with or without its variant suffix, and a raw role id unchanged – job names from runs that predate the display names still parse.
- Parameters:
text – a display name or a role id.
- decode_list(names: str) str¶
Space-separated display names rendered as space-separated role ids.
The axis suffix a CI selection token may carry (
#0,2@swarm+tor) is split off before the lookup and re-attached after it, so translating a list never drops what the operator pinned on it.- Parameters:
names – space-separated display names or role ids; anything that names no role passes through untouched.
- encode(app_id: str, variant: str = '') str¶
The display name for app_id, with the variant shard appended.
The last fragment is the shorter of the heading and the role id’s own section: a heading that outgrows the id it is meant to read better than has lost its point.
A token with no directory under
roles/encodes to itself: it is a sentinel like__ALL__, or a role the tree does not hold, and neither has a heading or a category to read.- Parameters:
app_id – role id, e.g.
'web-app-nextcloud'.variant – variant CSV the job covers, e.g.
'0,1'('': none).
- encode_list(app_ids: str) str¶
Space-separated role ids rendered as space-separated display names.
- Parameters:
app_ids – space-separated role ids; sentinels like
__ALL__and anything that names no role pass through untouched.
- id_fragment(app_id: str) str¶
The part of the role id the category labels do not already carry.
A role named after its own category (
update) leaves no remainder; the whole id stands in, so the fragment is never empty.- Parameters:
app_id – role id, e.g.
'web-app-nextcloud'.
- title(app_id: str) str¶
The role’s
README.mdheading as one separator-joined fragment.Whitespace inside the heading becomes a
·so a display name never holds a space and stays one token in the space-separatedwhitelistandpriorityinputs. Variation selectors left behind by an emoji in the heading are dropped.- Parameters:
app_id – role id, e.g.
'web-app-nextcloud'.
- utils.roles.display.display_names() RoleDisplayName¶
The codec for the repository the process runs in.
- utils.roles.display.main(argv: Sequence[str] | None = None) int¶
Translate one space-separated list between role ids and display names.
- Parameters:
argv –
['<display names>']to decode,['--encode', '<role ids>']to encode.
- utils.roles.display.split_axes(token: str) tuple[str, str]¶
Split a CI selection token into its name and its axis suffix.
#,@,+,%and/introduce an axis and appear in neither a role id nor a display name, so the first of them is the seam.- Parameters:
token – e.g.
'web-app-nextcloud#0,2@swarm+tor%debian/zfs'.
utils.roles.guide module¶
Which deploy row replays the role’s README instructions, and in what mode.
The instructions test used to be a job of its own that picked one random role per run. Riding along on a deploy row instead costs no extra runner and covers every role the sweep touches. The row it rides on is the role’s smallest variant – the one enabling the fewest services – because that is the shape the README’s Production block describes: the bare role, with the optional providers switched off.
The replay is a second deploy on the same runner, so the row it picks must be
one the guide can actually deploy: the README has to carry a Production block,
and the guide’s own mode (compose for a role that ships a stack, host
for one installed onto the machine) has to be a mode the role runs and tests.
- utils.roles.guide.PRODUCTION_HEADING = '### Production'¶
The README section
scripts/github/guide/compose_deploy.shreplays. A role without it has no instructions to test.
- utils.roles.guide.guide_deployable(app: str) str¶
The mode the guide would replay app in, or
''when it cannot.Independent of the variant, and cached: the answer is a property of the role’s files, and
utils.github.variant.axes.assign()asks it once per row of every chunk.
- utils.roles.guide.guide_variant(app: str, variants_per_app: Mapping[str, Sequence[Mapping[str, Any]]] | None, allowed: Collection[str]) tuple[str, str]¶
The variant of app that carries the replay, and the mode it runs in.
('', '')when the role has no instructions to replay or this sweep deploys no variant that could carry them.- Parameters:
app – the role.
variants_per_app – rendered variant configs per app; without them no variant can be told to be the smallest one.
allowed – the variants this sweep deploys, as the matrix writes them.
- utils.roles.guide.smallest_variant(variants: Sequence[Mapping[str, Any]], allowed: Collection[str] | None = None) str¶
The variant that enables the fewest services, as the matrix writes it; the lowest index wins a tie, and
''means none qualifies.- Parameters:
variants – the role’s rendered variant configs, in index order.
allowed – the variants this sweep actually deploys. A variant the sweep cuts as redundant coverage never reaches a runner, so hanging the replay on it would silently drop the role from the instructions test; the next-smallest deployed variant carries it instead.
utils.roles.lifecycle module¶
The CI-tested lifecycle envelope, read from default.env (the SPOT).
- utils.roles.lifecycle.tested_lifecycles() frozenset[str]¶
Lifecycle stages the CI test-deploy discovery exercises.
Reads the
INFINITO_LIFECYCLESvalue fromdefault.envso the envelope has a single source of truth instead of being duplicated as a hardcoded set at every consumer.
utils.roles.mapping module¶
Role mapping SPOT.
This module is the single source of truth for role-level dependencies:
the paths inside a role directory, the role types each path may
legitimately appear under, the mandatory shape of those paths, and the
marker entries that drive role-type detection itself. Both lint
tests and the role-type predicate live downstream of this file:
lint tests import
ROLE_FILESto forbid type-scoped artefacts on the wrong role type, to assert that mandatory files are present, and to assert that mandatory dotted-path entries inside those files exist;the role-type predicate lives in
utils.roles.type(seeutils.roles.type.get_role_type()) and walks themarkerflag in this file’sentriesto derive a role’s type from the values declared in its own files.
Path constants¶
Every ROLE_FILE_* constant carries the file’s path relative to the
role directory itself (roles/<role-name>/). Callers compose the
absolute path by joining with the resolved role directory:
from utils.roles.mapping import ROLE_FILE_META_SERVICES
services_path = (
PROJECT_ROOT / "roles" / "web-app-matomo" / ROLE_FILE_META_SERVICES
)
Type vocabulary and shape¶
ROLE_TYPE_* constants name the role categories the project
distinguishes. Each ROLE_FILES[<path>]['types'] value is a list of
type-scoping entries with the shape:
{
"type": <ROLE_TYPE_*>,
"mandatory": <bool>, # MUST the file/entry be set
"allowed": <bool>, # MAY the file/entry be set
"entries": [ # dotted-path facts about the file
{
"path": "application_id",
"mandatory": True,
"allowed": True,
"marker": True, # the role IS this type when set
},
{
"path": "ports.local.http",
"mandatory": False,
"allowed": True,
},
],
}
Two complementary flags control the per-type policy:
mandatory(defaultFalse): the file (or dotted-path entry) MUST be present and non-empty for a role of this type. Impliesallowed: True; settingmandatory: Truetogether withallowed: Falseis a schema error.allowed(defaultTrue): the file (or dotted-path entry) MAY be present for a role of this type. Setallowed: Falseto forbid the file/entry on roles of this type so the lint layer can surface the offence with a precise reason.
Roles whose type set does not match any explicit entry inherit the type’s policy from the wildcard fallback (see below); without a wildcard the file/entry is implicitly forbidden.
entries is a list of dotted-path facts about the file’s content.
Each entry carries the same mandatory and allowed semantics
described above and adds a marker flag (default False):
marker: True declares the path as the type marker for the
surrounding type entry. utils.roles.type.get_role_types() walks
every marker: True entry; each marker that resolves to a non-empty
value in the role’s file adds the surrounding type to the role’s type
set. application_id for application and system_service_id
for system-service are the canonical markers.
Wildcard ROLE_TYPE_ALL collapses repetition: a single entry with
"type": ROLE_TYPE_ALL applies to every concrete role type. A
concrete-type entry that appears alongside the wildcard MUST take
precedence so a contributor can spell out one per-type exception
without losing the shared default.
- utils.roles.mapping.role_is_stack(role_dir) bool¶
True when the role renders a docker stack.
- Parameters:
role_dir – path-like of the role directory (
roles/<name>).
A role is a stack role iff it ships
templates/compose.yml.j2, anytemplates/*.compose.yml.j2sibling, or atemplates/compose.*.yml.j2variant such ascompose.override.yml.j2.
utils.roles.meta_lookup module¶
Helpers for reading per-role metadata that lives on the role’s primary service entity in meta/services.yml.
Both fields used to live nested inside meta/main.yml.galaxy_info:
run_after— project-specific role-load-order list.
lifecycle— maturity marker filtered bycli meta roles lifecycle.
They live at
meta/services.yml.<primary_entity>.{run_after,lifecycle} where
<primary_entity> is the value returned by
utils.roles.entity.name.get_entity_name() for the role’s directory
name.
- Both helpers degrade gracefully:
[]/Nonewhenmeta/services.ymlis absent OR the field is absent on the primary entity.Raise a clear error only when
meta/services.ymlexists and parses into a wrong-shape document (non-dict root, non-dict primary entry).
- utils.roles.meta_lookup.DEPLOY_MODES: tuple[str, ...] = ('compose', 'swarm')¶
The stack deploy modes the CI test-deploy matrix (get_role_skip) covers.
- utils.roles.meta_lookup.MODES: tuple[str, ...] = ('compose', 'swarm', 'host')¶
Every deploy mode a role’s primary entity may toggle.
compose/swarmtarget stack roles (own container stack);hosttargets invokable roles that configure the host instead of shipping a stack.
- exception utils.roles.meta_lookup.MetaServicesShapeError¶
Bases:
ValueErrorRaised when
meta/services.ymlis malformed.
- utils.roles.meta_lookup.get_role_lifecycle(role: str | Path, *, role_name: str | None = None) str | None¶
Return the role’s
lifecyclestring (orNonewhen absent).
- utils.roles.meta_lookup.get_role_mode_enabled(role: str | Path, *, mode: str, role_name: str | None = None) bool¶
Return whether the role opts into deploy
mode(compose|swarm|host).The SPOT is
meta/services.yml.<primary_entity>.modes.<mode>.enabled. A missingmodesblock, a missing<mode>entry, or a missingenabledkey all mean the role participates in that mode (defaultTrue).
- utils.roles.meta_lookup.get_role_placement(role: str | Path, *, role_name: str | None = None) str | None¶
Return the role’s
placementstring (orNonewhen absent).
- utils.roles.meta_lookup.get_role_provides(role: str | Path, *, role_name: str | None = None) str | None¶
Return the service the role’s primary entity
provides(orNone).
- utils.roles.meta_lookup.get_role_run_after(role: str | Path, *, role_name: str | None = None) list[str]¶
Return the role’s
run_afterlist (or[]when absent).rolemay be a role name (relative toroles/) or an absolute path to a role directory. Passrole_nameexplicitly whenroleis an arbitrary path whose basename is not the canonical role name.
- utils.roles.meta_lookup.get_role_skip(role: str | Path, *, role_name: str | None = None) list[str]¶
Return the deploy modes the role is excluded from in test-deploy discovery (e.g.
[compose, swarm]), or[]when it participates in all of them.Derived from
meta/services.yml.<primary_entity>.modes.<mode>.enabled: a mode is skipped iff itsenabledflag isfalse.
- utils.roles.meta_lookup.get_role_test_skips(role: str | Path, *, role_name: str | None = None) list[str]¶
Return the deploy modes deactivated for test-deploy discovery via
meta/tests.ymlskip, or[]when absent.modesinmeta/services.ymlstates where a role RUNS;skipinmeta/tests.ymldeactivates TESTING a mode without touching that capability.
- utils.roles.meta_lookup.iter_roles_with_placement(placement: str, *, roles_dir: str | Path | None = None) list[str]¶
Sorted role names whose primary entity declares
placement: <placement>.
utils.roles.order module¶
Resolve the roles of a group into their run_after order.
The play used to consume a task file per group, generated at setup time. The order is derived from data that already lives in the roles, so it is resolved here instead and read through the group_roles lookup.
- utils.roles.order.build_dependency_graph(roles_dir: str | Path, prefixes: list[str] | None = None)¶
Return
(graph, in_degree, roles)over the roles matching prefixes.Only run_after edges pointing inside the selected set become edges, so a group resolves independently of the roles outside it.
- utils.roles.order.find_cycle(roles: dict[str, dict[str, Any]]) list[str] | None¶
Return a run_after cycle as a role list with the start repeated, or None.
- utils.roles.order.find_roles(roles_dir: str | Path, prefixes: list[str] | None = None)¶
Yield
(role_path, meta_file)for every role matching prefixes.- Parameters:
roles_dir – directory holding the role folders.
prefixes – name prefixes to keep; all roles when empty or None.
- utils.roles.order.load_application_id(role_path: str | Path) str | None¶
Return the application_id from the role’s vars/main.yml, or None.
- utils.roles.order.load_run_after(meta_file: str | Path) list[str]¶
Return the run_after list declared by the role owning meta_file.
- utils.roles.order.ordered_roles(roles_dir: str, group: str) tuple[dict[str, str], ...]¶
Return
({'role': …, 'app': …}, …)for group, in run_after order.- Parameters:
roles_dir – directory holding the role folders.
group – group name without the trailing dash, e.g.
web-app.
- Raises:
ValueError – when a role in the group declares no application_id.
- utils.roles.order.topological_sort(graph, in_degree, roles=None) list[str]¶
Return the role names in run_after order.
- Raises:
RuntimeError – when the graph does not resolve, naming the cycle and the roles left unsorted so the failure is actionable.
utils.roles.required_by_coverage module¶
Static classifiers used by the required_by lint and verifier: role invokability (per categories.yml), required_by presence in meta/services.yml, and # nocheck: <id> opt-outs.
- utils.roles.required_by_coverage.role_has_nocheck(role_id: str, check_id: str, roles_dir: str | Path | None = None) bool¶
True if roles/<role_id>/meta/services.yml contains # nocheck: <check_id> (whole-word match).
- utils.roles.required_by_coverage.role_has_required_by(role_id: str, roles_dir: str | Path | None = None) bool¶
True if any entity in roles/<role_id>/meta/services.yml declares non-empty required_by categories/roles, either flat or nested under a compose:/swarm: mode block.
- utils.roles.required_by_coverage.role_is_invokable(role_id: str, roles_dir: str | Path | None = None) bool¶
True when any node along role_id’s category path has invokable: true.
utils.roles.stage module¶
Deploy-stage lookup SPOT.
meta/categories.yml declares the ordered stages (constructor ->
workstation -> server -> destructor, matching tasks/stages/0*.yml) and a
stage per top-level category. This module resolves a role to its stage and
exposes the canonical stage order, so both the CLI (cli.meta.roles.order)
and the stage_order / role_stage lookups read one source of truth.
- utils.roles.stage.role_modes_defaults(role: str) dict¶
Mode-enabled defaults a role’s services must declare, from the deepest categories.yml category carrying a
modesmapping (parents apply when no deeper category declares one). Empty when no category declares modes.
- utils.roles.stage.role_sort_key(role: str) tuple¶
Sort key placing a role by (stage, category run_after, name). Used as the run_after tiebreak so ready roles fall out in call order.
- utils.roles.stage.role_stage(role: str) str¶
The stage a role is invoked in (deepest matching category wins).
- utils.roles.stage.stage_groups(stage: str) list[str]¶
Ordered role-group names that belong to
stage, in intra-stage call order (category run_after, then name). The SPOT the stage plays consume so group membership follows categories.yml. Bootstrap-flagged categories are excluded (they run via explicit steps).
- utils.roles.stage.stage_order() list[str]¶
Canonical deploy-stage order.
utils.roles.type module¶
Role-type detection.
Role-type detection consumes the marker: True entries declared in
utils.roles.mapping.ROLE_FILES so the schema and the predicate
share one source of truth: adding a new type marker on the SPOT side
makes the predicate recognise the new type without further code edits.
A role MAY belong to more than one type. svc-opt-ssd-hdd is the
canonical example: it declares both application_id and
system_service_id and is therefore an application AND a system
service. get_role_types() returns the full set so callers that
care about scope (e.g. lint tests asserting “this file is allowed for
applications”) can use simple set membership.
Algorithm¶
For every marker entry declared anywhere in
utils.roles.mapping.ROLE_FILES the predicate reads the role’s
file at the entry’s dotted path. Each marker that resolves to a
non-empty string adds the surrounding type to the result.
Roles without any marker fall back to:
utils.roles.mapping.ROLE_TYPE_USERfor the bareuserrole and everyuser-<…>sibling (name-based);utils.roles.mapping.ROLE_TYPE_TOOLINGotherwise.
Conformance¶
A marker value MUST be a non-empty string. If the marker key is
present but the value is of the wrong YAML type, empty, or whitespace-
only, the role’s typing is broken: get_role_types() raises
RoleTypeError so the misconfiguration surfaces at the first
call instead of silently degrading to a different type.
Caching¶
Reads route through utils.cache.yaml.load_yaml_any() (already
memoised per-path). get_role_types() caches its return per
role-directory string so repeated calls during a single test sweep
stay free.
- exception utils.roles.type.RoleTypeError¶
Bases:
ValueErrorRaised when a role’s typing markers do not conform to the schema.
- utils.roles.type.get_role_types(role_dir: Path) frozenset[str]¶
Return the set of role types declared by role_dir’s markers.
A role MAY belong to several types simultaneously (for example an application that also ships a systemd unit). The returned frozenset contains every type whose marker matches; the user / tooling fallbacks fire only when no marker resolves.
Cached: repeated calls for the same role_dir reuse the first parse. Pass an absolute path so the cache key is stable across callers that resolve relative paths differently.
- utils.roles.type.reset_cache_for_tests() None¶
Drop the per-process role-type cache. Tests that mutate
vars/main.yml(or another marker-bearing file) mid-run MUST call this between mutations.