Commit Graph
88 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 7a82a92e84 fix(web-app-prometheus): probe only the domains an app serves itself
#1180 gave web-app-matrix a canonical `jitsi` domain for its own Jitsi
and listed it under `services.jitsi.domains`. When Jitsi is shared or
disabled nothing serves that domain, and the blackbox healthz job of
web-app-prometheus probed every canonical domain, so the probe of
https://jitsi.matrix.<domain>/healthz/ready failed with "tls:
unrecognized name" and `prometheus scrape: every consumer role reports
up=1 and passes its probe` went red (main run 40017166046: Matrix#2,
Nextcloud#12).

web_health_expectations already skipped such keys. The rule now lives
in utils/domains/unserved.py: a key claimed by a service entity that is
disabled, or enabled but shared, is not served by that app. The health
filter uses it unchanged, and prometheus.yml.j2 filters its targets
through the new `served_domains` filter, which also drops the
`<key>_onion` mirror of a skipped key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 11:44:14 +02:00
kevinveenbirkenbachandClaude Opus 5.5 25ec8b0c59 fix(sys-ctl-hlth-webserver): skip a consumer's domains for a shared service
A service entity can list canonical domain keys under `domains`; the
web health expectations skipped them only while the service was
disabled. A consumer whose service is enabled but shared does not serve
its own domain for it either, since the provider serves the service on
its own domain, so the probe failed with an unrecognised TLS name.
Domains of an enabled and shared service are now skipped as well.

The only other entity with `domains` (web-app-seaweedfs frontend) is
never shared, so its behaviour is unchanged.

Refs #1165

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 17:16:19 +02:00
kevinveenbirkenbachandClaude Opus 5 c88e50ef17 fix(front-proxy): accept the status codes an app declares, not just the triple
The front proxy hardcoded [200, 301, 302] and escalated to a full OpenResty redeploy on anything else. Apps that answer 403, 404 or 405 by design tripped it on every run: 19 escalations in one Nextcloud job at a measured median of 40.5s each.

roles/<app>/meta/server.yml already declares status_codes per canonical key, and sys-ctl-hlth-webserver already reads it. Move the resolution into utils/roles/applications/status_codes.py so both consumers share one definition of DEFAULT_OK, and expose it to templates as the accepted_status_codes filter.

The proxy gate takes the union of the default and the declaration rather than replacing it. Most declarations are narrower than the triple, so replacing would escalate vhosts that answer perfectly well. Contract checking stays with sys-ctl-hlth-webserver, which keeps replacement semantics.

_normalize_codes and _valid_http_code became dead in the health filter and are removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 17:30:34 +02:00
kevinveenbirkenbachandClaude Opus 5 74afec37c3 fix(health): exit with a status, not a failure count
Four probes returned the number of failures as the process exit code. An exit status is 8 bits wide, so exactly 256 or 512 failures arrive as 0 and the probe reports success. sys-service installs these as Type=oneshot units (roles/sys-service/vars/main.yml:28) and sys-service-terminator starts them with until: result is not failed, so that exit code is the whole health verdict.

The count is not lost: each of the four prints it to stdout a line earlier. Only the exit status stops carrying it, because it cannot.

This makes no CI job green and is not meant to. For any count other than 256 or 512 nothing changes; at those two it turns a false success into an honest failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 04:41:04 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
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>
2026-08-22 01:31:09 +02:00
kevinveenbirkenbachandClaude Opus 5 5137465c6f style: delete every comment the comments-valid policy rejects
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.

Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:02 +02:00
kevinveenbirkenbachandClaude Opus 5 c55f508e05 feat(readme): draw mandatory and disabled service relations
The cosmos generator linked a consumer to its provider only through an
'<role>' in group_names reference or an explicit provides: name. A hard
dependency has neither -- pinning the provider on unconditionally is
exactly what removes the Jinja reference the generator was reading -- so
web-svc-asset's bond to web-svc-file vanished from the diagram the moment
it became binding. Match a service key against the provider's own primary
key from the registry, in both directions.

Services the role turns off drew a solid 1:1 edge, claiming a mandatory
relationship for something that carries nothing at deploy time. They now
render as a red 0..0 edge; edge merging gained a real precedence so a
provider consumed through two keys stays live when only one is off.

Named colour, not hex: a '#' opens an entity code in the mermaid lexer and
turns the whole diagram into a parse error on GitHub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:02:31 +02:00
kevinveenbirkenbach 1a0af0ce09 Merge remote-tracking branch 'fork/feature/swarm-nfs' into feature/svc-net-tor
# Conflicts:
#	roles/sys-ctl-mtn-cert-deploy/tasks/main.yml
#	roles/sys-svc-compose-ca/tasks/main.yml
#	roles/web-app-dashboard/templates/javascript/oidc.js.j2
#	roles/web-app-mastodon/tasks/01_overrides.yml
#	roles/web-app-peertube/templates/compose.yml.j2
#	scripts/github/guide/compose_deploy.sh
#	scripts/github/guide/host_deploy.sh
#	scripts/make/alias.sh
#	tests/lint/ansible/roles/test_service_core_first_task_run_once.py
2026-07-24 04:30:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4039313266 refactor(tasks): enforce and apply a single task-file numbering scheme
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.

Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:01:31 +02:00
kevinveenbirkenbachandClaude Fable 5 56f4dc6400 fix(hlth-webserver): probe domains from the stack host only
Workers have no tor egress resolver (onion probes NXDOMAIN, exit 12)
and serve no clearnet vhosts (ingress answers 404); the check belongs
to OpenResty, which is placed on the manager.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:42:18 +02:00
kevinveenbirkenbachandClaude Fable 5 d88f272f9f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:31:08 +02:00
kevinveenbirkenbachandClaude Fable 5 407b8a8a1f fix(e2e): use the tls lookup instead of the unregistered urlsplit filter; autoformat
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:05:59 +02:00
kevinveenbirkenbachandClaude Fable 5 194671d90f feat(readme): uniform description schema and clearer role titles
Rewrite all 254 role descriptions to a uniform noun-phrase schema
(function-first, no leading verb, no trailing period, <=120 chars) and
enforce it via the extended description lint. Rename 12 role README H1
titles for clarity (e.g. Backup Container to Local -> Backup Docker
Volumes, CSS Service -> Corporate Design, NGINX Legal -> Imprint) and
regenerate their Cosmos and Quick Setup sections. Overview table
regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 01:11:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f6174655b1 fix(hlth-webserver): expect the apex only when web-opt-rdr-domains serves it
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 23:44:12 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dd3d83148e feat(readme): show Ansible role dependencies in Cosmos with a gear marker
Roles declared under meta/main.yml dependencies now appear on both the
Dependencies and Dependents side, labelled with the glossary gear
symbol and connected by a fixed 1:1 edge; the legend explains it. All
role READMEs regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a9fd37907b feat(readme): cardinality labels and a symbol legend on Cosmos diagrams
Edges now carry their relation: solid 1:1 for fixed consumption, dashed
0..1 for conditional (group-membership gated) consumption. Below every
diagram the generator renders a reading key for the edge kinds and the
node markers, with the glyphs drawn from the shared symbol glossary.
All role READMEs regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 90b3845998 feat(readme): role README generator with Cosmos diagrams, Quick Setup, and lint
Add cli/build/readme: derives each role README's managed sections from
templates/roles/README.md.j2.tmpl (the Cosmos mermaid diagram from
meta/services.yml, an invokable-gated Quick Setup, and Credits).

The Cosmos diagram shows dependencies, every declared service, and
dependents (roles that consume this one, matched by role name and by
provides provision-name in both directions). Role nodes carry deploy-mode
emoji markers (compose/swarm/host); disabled and shared services get a
stop mark; dependents are capped at 12.

test_readme enforces the Quick Setup body is byte-exact with the template
render. `make readme-generate` and `make cosmos` regenerate; `make cosmos`
also runs inside `make quality`.

Refactor: utils/symbol_glossary.py is the single source of truth for the
word/emoji mapping (unique project-wide), consumed by the complexity
matrix and the Cosmos builder. utils/roles/deploy.py holds role_has_stack
and role_deploy_modes, shared by both so the logic never drifts.

Add `make lint-mermaid` and its CI workflow, rendering every diagram via
mermaid-cli; the render step provisions chrome-headless-shell and unzip
on demand so it works in the minimal container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 21ae788a92 fix(hlth-webserver): skip canonicals of disabled services
A service entry may declare `domains: [<canonical key>, ...]`; when its
`enabled` resolves falsy those canonicals get no vhost (e.g. the seaweedfs
filer/master frontend on an onion node), so probing them can only fail
(TLSV1_UNRECOGNIZED_NAME / ERR_NAME_NOT_RESOLVED). Drop them from the
web-health expectation map. Mirrors the same suppression already added to
the CSP health probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8b1110d571 style: apply autoformat reformatting (no functional change)
make autoformat normalised inline-comment spacing (`  #` -> ` #`) in role meta/services.yml + meta/variants.yml and re-wrapped a few Python files (ruff line-length). Pure formatting — verified via git diff -w (whitespace-only) and diff inspection (ruff line-wraps only, no logic change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:50:59 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6741b8a7d4 feat(sys-ctl-hlth-webserver): deploy-aware onion web health checks
web_health_expectations swaps (exclusive) or extends (dual) each services.tor.enabled app's clearnet domains with their onion domains via _inject_onion_domains, so the probe checks exactly what the deploy serves; script.py probes .onion over http (plaintext-only) with a longer Tor timeout. Adds onion unit tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:45:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 674d26de3a feat(lint/role-readme): derive Credits author from meta author SPOT
Role README Credits now name galaxy_info.author (plain bold or wrapped
in a Markdown link), matched byte-for-byte against the role's own
meta/main.yml, instead of a hard-coded maintainer name. Regenerate all
245 role README Credits blocks to the new 'Implemented by ...' format.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:47:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 cddb0dc59b feat(lint): require emoji-prefixed task/step/job names + convert existing
Add tests/lint/repository/test_name_emoji_and_length.py: every Ansible task
name and GitHub Actions workflow/job/step name must start with an emoji (first
char not 0-9/A-Za-z) and be <= 80 chars.

- emit emoji names at the source: cli/build/include generator now writes
  "🔧 setup <app>" / "🚿 flush handlers after <app>" so generated
  tasks/groups/*-roles.yml satisfy the convention too
- convert existing task/step/job/workflow names to emoji-prefixed, choosing a
  task-fitting icon per action (📦 install, ✅ ensure, 🔍 probe, 🥾 bootstrap,
  🚀 deploy, 🧩 service-module load, ⬇️ checkout, 📂 include-routines, ...)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:45:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6ce6780575 refactor(meta): split server.yml into csp/domains/networks.yml
Extract the csp, domains, and networks blocks out of roles/*/meta/server.yml
into dedicated meta files, each mapping to its own top-level applications key
via the recursive meta-walk:

  csp:      -> meta/csp.yml      -> applications.<app>.csp
  domains:  -> meta/domains.yml  -> applications.<app>.domains
  networks: -> meta/networks.yml -> applications.<app>.networks

Residual nginx/proxy keys (status_codes, client_max_body_size, locations,
ip6_enabled, config_upstream_url) stay in server.yml; an emptied server.yml is
deleted. 98 server.yml -> 249 new files, 74 deleted.

- Consumers repointed applications.<app>.server.{csp,domains,networks} ->
  applications.<app>.{csp,domains,networks} across templates, vars, tasks,
  lookups and filters.
- Infra readers migrated: canonical_domains_map, application_domain_index,
  build_ldap_role_entries, service-registry overlay discovery,
  roles_with_service, and the file-based readers utils/domains/list.py +
  utils/meta/scan.py (read the now-deleted server.yml directly).
- Detection + mapping: _META_TOPICS gains csp/domains/networks;
  ROLE_FILE_META_{CSP,DOMAINS,NETWORKS} constants + ROLE_FILES entries.
- New self-expiring lint guard (test_server_meta_no_split_fields) fails if a
  server.yml still carries a split field; auto-fails from 2026-12-28 as a
  delete reminder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:45:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 48753de136 feat(deploy-verify): scope required_by coverage to deploy modes (compose/swarm)
The required_by coverage verifier treated every flat 'required_by: {categories/roles}' as required in BOTH deploy modes. But sys-ctl-rpr-container-hard/soft repair compose compositions (DIR_COMPOSITIONS) and gate their body to compose; declaring required_by:[web] flat made the verifier expect them on swarm too, where they correctly skip, so web-opt-rdr-domains swarm CI failed 'required role(s) did not execute'. Let required_by nest under 'compose:'/'swarm:'; the verifier infers the deploy mode from the role set, mirroring DEPLOYMENT_MODE's own groups['svc-swarm-node'] definition, and enforces only the matching mode.

Migrate all 21 existing flat declarations to mirror under both modes (behavior unchanged) and mark sys-ctl-rpr-container-* compose-only. role_has_required_by reads both forms. New lint test_required_by_mode_keys requires the explicit compose:/swarm: form so the flat-vs-gate contradiction can't recur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:59:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5687643b46 style(deployment-mode): positive == 'compose' gates + enforce via lint
DEPLOYMENT_MODE is binary (swarm / compose), so a negated `!= 'swarm'` gate
always means `== 'compose'`. Replace every negated gate across roles/ and
group_vars with the positive form for readability, and add a lint
(tests/lint/ansible/variables/test_no_deployment_mode_negation.py) that forbids
`DEPLOYMENT_MODE !=` in .yml/.yaml/.j2 with a `# nocheck:
deployment-mode-negation` escape.

The compose-verb / chdir / jinja-gate lint guidance messages now suggest the
positive form too; the gate matcher (utils/annotations/task_gate.py) and its
test inputs keep recognising the legacy `!= 'swarm'` so a nocheck-marked
exception is still detected as compose-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:31:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b5a000e5ca refactor(sys-ctl-hlth-webserver): drop redundant self-gate comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 04:35:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 af1f80f5c8 fix(sys-ctl-hlth-webserver): self-gate health check to mesh nodes
The service-loader flushes registered post-load roles cluster-wide (registration
happens once on the stack host, but "Run service-loader post-load roles" includes
the role for every host in the play). On a swarm storage host like the NFS server
there is no webserver and the app domains resolve to 127.0.0.1, so the webserver
health check fails with NameResolutionError / unreachable.

Self-gate the check to routing-mesh nodes (svc-swarm-node in swarm, all hosts in
compose), mirroring how sys-ctl-hlth-csp already self-gates. Keeps the manager and
workers (which reach the domains via the mesh); excludes non-mesh infra hosts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 03:32:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bc41a0e0f2 feat(swarm): worker DNS + shared Root CA replace the webserver health gate
Workers resolve the app domain to the manager IP (openresty is mode:host +
manager-pinned), so sys-ctl-hlth-webserver can run on every node instead of
being gated to the stack host. The shared Root CA is minted once on the
stack host and fetched by each worker, so a worker's trust store matches the
manager's leaf signer and the healthcheck's TLS chain verifies.

- sys-ctl-hlth-webserver: drop the IS_STACK_HOST gate (reachability, not
  data-locality, now that workers resolve the domain to the manager)
- sys-ca-selfsigned: mint the Root CA once on STACK_HOST (run_once +
  delegate_to), extract the generation steps to 02_mint.yml; non-stack hosts
  slurp the cert from STACK_HOST and write it to their host-local path
  (cert only, never the key)
- svc-swarm-node: anchor the shared CA early on every swarm node so it
  exists before any per-node consumer reaches it; bundle the swarm-only
  tasks under one DEPLOYMENT_MODE block
- sys-front-tls-selfsigned: gate leaf signing to the stack host (workers
  have no CA key and run no manager-pinned openresty)
- scripts/tests/deploy/swarm/02_start_swarm_nodes.sh: workers run dnsmasq
  resolving the domain to MGR_IP; excluded from the 127.0.0.1 /etc/hosts
  entries that would shadow it

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 29377e1bd3 fix(cache/applications): stop variant-0 service leak via base-config defaults
`_build_application_defaults` returned variant 0 of every role with a
meta/variants.yml, which then deep-merged with the variant-N override
that build_dev_inventory bakes into the inventory. Any service flag
that variant N did not explicitly disable carried through from
variant 0 into the runtime applications map. The most visible casualty
was svc-bkp-container-2-local variant 1 (postgres + mariadb only),
where gitea + keycloak leaked in via variant 0, sys-service-loader
pulled in svc-db-openldap via keycloak.services.ldap, and the deploy
crashed in sys-svc-compose/04_files.yml on openldap's env.j2 with
"credentials is defined in schema at roles/svc-db-openldap/meta/schema.yml
but not set in application 'svc-db-openldap'".

Defaults now come from each role's variant-free base config
(meta/<topic>.yml plus schema-defaults, no variants overlay). Variants
stay accessible via get_variants for any caller that explicitly needs
a specific variant. Inventory-level applications.<app> overrides win
exactly as before.

Supporting changes that keep the variant matrix consistent under the
new defaults model:

- New lint guard tests/integration/roles/meta/variants/
  test_non_baseline_explicit_disables.py: for every dynamic
  service-key declared in a role's meta/services.yml that variant 0
  pins to literal true, every non-baseline variant MUST explicitly
  pin the same key to either literal true or to literal
  enabled: false + shared: false. Statically-enabled keys are exempt
  (same rule as test_auth_isolation.py).
- roles/web-app-{fusiondirectory,lam,nextcloud,phpldapadmin}/meta/
  variants.yml: explicit `false` pins for dashboard / email / logout /
  matomo / prometheus / talk in restrictive variants so deep-merge no
  longer hands them the deleted variant-0 inheritance.
- roles/web-app-moodle/templates/playwright.env.j2: gate the LAM
  block on `lookup('config', application_id, 'services.lam.enabled',
  false)`. LAM is a transitive helper, not a moodle-owned service, so
  the default-bearing lookup tolerates the missing path when the
  variant does not deploy LAM.
- tests/unit/utils/cache/test_variants.py: rename the legacy
  "always variant zero" coverage to TestMergedApplicationsUsesBaseConfig
  and add test_get_application_defaults_omits_variant_zero_only_keys
  as the regression guard for the leak this commit fixes.
- docs/contributing/artefact/files/role/variants.md +
  docs/contributing/design/variants.md: variant 0 now manually
  enumerates every dynamic services.yml key with
  enabled: true + shared: true; `{}` is only permitted when the role
  has no dynamic-enabled service-key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:04:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fc2ac7597a refactor(lookup): rename status -> deployment, add runtime-aware deployed
`status` was too vague for what the lookup returns (apps in current
play). Rename to `deployment` and add two new fields:

* `runtime` — RUNTIME variable value
* `deployed` — runtime-aware authoritative set: equals `running`
  (whitelist or groups) for ephemeral CI runtimes (act, github),
  falls back to `groups` for persistent runtimes (host, dev). This
  matches reality: in CI the container is fresh so only whitelisted
  apps exist; in production a partial deploy still leaves prior
  apps live, so groups is the right set.

Switch the two webserver/csp healthchecks to `lookup('deployment').deployed`
so they only probe domains for apps that actually exist in the current
deploy. In production behaviour is unchanged.

Update remaining consumers (cli/build/include, web-app-keycloak,
test-e2e-playwright) for the rename. Existing `status` semantics
(`whitelist`, `running`, `groups`, `all`) preserved under the new
name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:52:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 274e4fd6f8 feat(roles): mark webserver+CSP infra as required_by web for deploy verifier
Adds `required_by: { categories: [web] }` to the four roles that MUST run
on every web-app/web-svc deploy: sys-svc-webserver-{core,https} (the
nginx/openresty stack) and sys-ctl-hlth-{webserver,csp} (the deploy-time
coverage checks).

Activates the cli.meta.roles.services.called verifier (added in the
previous commit). Without this annotation the verifier finds no required
roles and silently passes; with it, every web-* deploy round/pass asserts
that these four roles actually executed in the Ansible log, catching
silent coverage regressions like the one fixed in
158681821 (sys-svc-webserver-core: hlth-include moved out of once-flag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 04:23:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2a023ca6a1 chore(lint): autoformat strict-failure semantics + claude.sh rename
* scripts/lint/autoformat.sh:
  - rename run_sort_claude_settings worker to run_claude (matches the
    renamed scripts/lint/claude.sh).
  - drop the EXIT trap that wiped status_dir; cleanup happens explicitly
    after `wait` returns, so a parallel worker that interrupts the
    parent never races a vanished status dir.
  - each worker now captures its tool's exit code and writes one of
    OK / SKIP / FAIL. Tolerated `expected violations remain`-style exits
    (markdownlint-cli2 rc≤1, ansible-lint rc≤2) still count as OK; only
    real failures count as FAIL.
  - all workers run to completion regardless of individual tool failures
    (so the user always sees the full picture); after the summary, the
    script exits 1 if any worker reported FAIL.
* scripts/lint/sort_claude_settings.sh -> scripts/lint/claude.sh
  (history preserved via git mv); update test_claude_settings_sorted.py
  fix-hint accordingly.

Plus working-tree drift across roles/, group_vars/, inventories/,
requirements/, and .pre-commit-config.yaml accumulated from the
make-test setup phase regenerating generated artefacts during recent
test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:30:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 951f6d36d2 chore(lint): unify autoformat/galaxy parallelism into one PARALLEL env var
* scripts/lint/autoformat.sh: rename AUTOFORMAT_PARALLEL to PARALLEL,
  declare it at file start with default `true`; still env-overridable.
  Reword the worker comment so the literal `# shellcheck` token no
  longer appears at start-of-line and trips SC1072/SC1073.
* scripts/lint/galaxy.sh: drop the GALAXY_FATAL / GALAXY_NAMESPACE /
  GALAXY_PARALLEL env vars (over-engineered for a single repo). Failure
  is always fatal. NAMESPACE is an internal constant. Add the same
  PARALLEL env var (default `true`, env-overridable); falsy demotes the
  xargs fan-out to one worker.
* docs/contributing/environment/variables.md: collapse the four
  AUTOFORMAT_*/GALAXY_* rows into one `PARALLEL` row covering both
  consumers.
* docs/contributing/tools/make.md: refer to `PARALLEL=0` everywhere;
  add the lint-galaxy parallelism note.

Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:59:24 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c52e4269bf chore(tooling): integrate mbake (Makefile linter/formatter) + working-tree drift sync
Makefile lint integration:
* scripts/install/lint.sh: replace the prior checkmake binary install with
  ensure_mbake (mbake is pip-installable, parses our Makefile correctly
  whereas checkmake bailed on .ONESHELL: / .SHELLFLAGS: / ifneq).
* scripts/lint/makefile.sh: new runner — `mbake format --check Makefile`
  reports formatting drift and missing .PHONY declarations.
* scripts/lint/autoformat.sh: add a two-pass `mbake format Makefile`
  (mbake's formatter is not strictly idempotent on the first pass).
* Makefile: split the multi-line `.PHONY:` block into 18 single-line
  `.PHONY:` declarations so mbake's parser can validate it; add the
  previously-missing `install-python-dev` and `build-dependency` PHONY
  targets; add `lint-makefile` aggregate-and-direct target.
* .pre-commit-config.yaml: new make-lint-makefile hook scoped to
  `^Makefile$`.
* .github/workflows/lint-makefile.yml: new CI workflow (pip-install mbake
  + make lint-makefile).
* .github/workflows/ci-orchestrator.yml: wire lint-makefile into
  lint-gate.needs.

Misc:
* .gitignore: add roles/importer_result.json (tooling artefact written by
  galaxy-importer when it runs in roles/).

Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:32:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 79409ebf10 test(tooling): integrate markdownlint-cli2, ansible-lint, galaxy-importer + fix the resulting baseline
Tooling integration
* add .markdownlint-cli2.jsonc, .ansible-lint, .ansible-lint-ignore
  configs (permissive day-1: cosmetic rules disabled, semantic ones on)
* extend scripts/install/lint.sh with generic install_pip_pkg helper +
  ensure_ansible_lint / ensure_galaxy_importer / ensure_markdownlint_cli2
  ensure_* functions (pip / npm with sudo / --user / --break-system-packages
  fallbacks). install_ansible_tools now also pulls ansible-lint and
  galaxy-importer; new `markdown` install group.
* extend scripts/lint/ansible.sh to run ansible-lint after the existing
  syntax-check.
* add scripts/lint/markdown.sh and scripts/lint/galaxy.sh runners. The
  galaxy runner parallelizes via xargs (-P=$(nproc)), prints OK/FAIL per
  role, and treats `bare-name dependency` errors as success since our
  roles/ tree is not a Galaxy collection layout. All other Galaxy schema
  errors still fail.
* Makefile: new lint-markdown and lint-galaxy targets, both wired into
  `make lint`.
* CI: new workflows lint-markdown.yml and lint-galaxy.yml; lint-ansible.yml
  now installs ansible-core + ansible-lint; ci-orchestrator wires both
  new lint workflows into lint-gate.

Galaxy schema fixes
* shorten the canonical `galaxy_info.company` block from three lines to
  two ("Kevin Veen-Birkenbach\nhttps://www.veen.world", 45 chars) so it
  stays under Galaxy's hard 50-char limit. Updated everywhere: 229 role
  meta/main.yml plus _CANONICAL_COMPANY in
  tests/lint/repository/test_role_meta_main_galaxy_schema.py.
* shorten 19 over-long role descriptions to <=255 chars.
* add stub READMEs for sys-svc-compose-ca and sys-utils-service-loader.

Markdown baseline fixes
* markdownlint-cli2 --fix run repo-wide (formatting normalization
  across CHANGELOG, role READMEs, docs).
* manual MD041 fixes: prepend H1 in 15 files (CHANGELOG, LICENSE,
  TODO.md helpers, role README templates, etc.).
* MD025 / MD003 / MD001 / MD032 manual fixes for heading-style and
  list-spacing edge cases.
* role README content polish: "## Single Point of Truth (SPOT)" headings
  renamed to "## User data resolution" (D3 self-label rule).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:46:37 +02:00
kevinveenbirkenbachandClaude Opus 4.7 afdbd58540 chore(roles): normalize meta platforms + role README Credits, revert GenericLinux drift
* meta/main.yml: replace every prior platforms variant (Linux,
  GenericLinux, Any, Archlinux+rolling/all, distro-specific pins) with
  the canonical 5-distro list (ArchLinux, Debian, EL, Fedora, Ubuntu;
  versions: [all] each).
* lint: merge tests/lint/ansible/roles/test_meta_company_canonical.py
  into tests/lint/repository/test_role_meta_main_galaxy_schema.py and
  add a parallel canonical-platforms exact-value check; one file walk
  enforces both schema conformance and the two project canonical
  exact-values (company block-scalar + platforms 5-distro list).
* roles/<role>/README.md: strip emojis from H1/H2 headings (per
  readme_md.md role rule), normalize the Credits block to the canonical
  wording (Author/Authors/Author Information aliased; canonical block
  appended where absent).
* repo-wide: revert text-drift `GenericLinux` -> `Linux` in role
  description fields, README body prose, vars, j2 templates, and the
  Makefile (87 occurrences). The lint docstring keeps a single
  historical mention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.7 653636e656 chore(roles): normalize galaxy_info.company + add canonical-company lint
Collapse the prior ~10 variants of galaxy_info.company (single-line,
multi-line single-quoted, multi-line double-quoted, escaped \n,
Unicode non-breaking hyphen, Birchenbach typo, block-scalar) into one
canonical block-scalar form across every roles/*/meta/main.yml that
declares the field:

  company: |
    Kevin Veen-Birkenbach
    Consulting & Coaching Solutions
    https://www.veen.world

Add tests/lint/ansible/roles/test_meta_company_canonical.py so future
drift fails CI immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:50:42 +02:00
kevinveenbirkenbachandClaude Opus 4.7 85f1caccc7 refactor(utils): consolidate role-scoped helpers under utils/roles and split templating
Tighten the utils namespace so each subpackage owns one concern:

- utils/applications/* moves under utils/roles/applications/* (helpers
  that operate on the runtime applications dict and meta/services.yml).
- utils/service_registry.py and utils/database_service.py move under
  utils/roles/applications/services/ as registry.py and database.py.
- utils/entity_name_utils.py becomes utils/roles/entity_name.py.
- utils/invokable.py, utils/valid_deploy_id.py, and the existing
  utils/roles/resource_validation.py group under utils/roles/validation/
  as invokable.py, deploy_id.py, and resources.py.
- utils/templating.py and utils/jinja_strict.py split into
  utils/templating/ansible.py (Templar + TrustedAsTemplate, lookup-env
  shortcut) and utils/templating/jinja.py (vanilla Jinja2 Environment,
  multi-pass strict render).

Every caller in utils, cli, plugins, roles/<role>/{filter,lookup}_plugins,
tests, and the docs that link to the moved files is updated to the new
import paths. Test files mirror the new source layout
(tests/unit/utils/{roles,templating}/...).

New READMEs at utils/cleanup/, utils/roles/, utils/roles/applications/services/,
utils/roles/validation/, and utils/templating/ follow the
documentation.md and readme_md.md guidelines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 03:53:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8fb34f8792 refactor(project-root): catch all locally-computed project-root paths in lint + clean offenders
Two coupled changes that close every remaining loophole around
locally-computing the project root:

1. Strict ``".."`` rule in
   ``tests/lint/repository/test_project_root_import.py``.

   The previous ``["']\.\.["']`` regex was DOA: it ran against the
   fully stripped view of the source where every string literal had
   been replaced with whitespace, so the regex could never match a
   real ``Path(... / ".." / ...)`` callsite. Two changes fix this:

   - The catalog entries now carry a scan-mode flag
     (``"stripped"`` vs ``"no_docs"``).
   - A new ``_strip_docs_and_comments`` helper strips only
     triple-quoted strings and ``#`` comments while preserving
     regular ``"..."`` / ``'...'`` literals, so a ``".."`` *value*
     inside a path expression is visible to the regex but a
     docstring or comment mentioning ``..`` is not.
   - The regex itself now matches not just bare ``".."`` but also
     ``"../.."``, ``"../../../"`` (any number of ``..`` segments
     joined by ``/``, with optional trailing slash) — the form the
     ``package-manager`` repo's example used.

   Net effect: any line that builds a path through one or more
   ``..`` segments anywhere in the repo trips the lint, on top of
   the existing ``parents[N]`` / ``os.pardir`` / ``def
   repo_root()`` shapes.

2. Cleared every offender the strengthened rule surfaced (62 lines
   across 49 files):

   * **25 integration tests, 5 unit tests, 1 lint test**:
     migrated from local ``..`` chains to
     ``from . import PROJECT_ROOT``. Seven previously-empty package
     ``__init__.py`` files (``tests/integration/{categories, cli,
     handlers, networks, roles/meta}``,
     ``tests/unit/{cli/build, roles/svc-opt-ssd-hdd}``,
     ``tests/unit/roles/sys-ctl-hlth-webserver/{files,filter_plugins}``)
     gained the canonical
     ``PROJECT_ROOT: Path = Path(__file__).resolve().parents[N]``.

   * **4 CLI ``__main__.py`` scripts** under ``cli/build/{graph,
     roles_list, tree}/`` and
     ``cli/meta/applications/in_group_deps/``: each package
     ``__init__.py`` now defines the canonical PROJECT_ROOT before
     the existing ``from . import __main__ as _main`` re-export, and
     the ``__main__.py`` consumes it via
     ``from . import PROJECT_ROOT``.

   * **plugins/lookup/__init__.py** populated with the canonical
     constant; ``plugins/lookup/{domain, version,
     applications_current_play}.py`` now do
     ``from plugins.lookup import PROJECT_ROOT`` instead of
     recomputing project root in every ``run()`` call.

   * **plugins/filter/get_domain.py**: dropped the runtime
     ``sys.path.append`` + lazy import dance entirely — every other
     filter plugin in the codebase already imports ``utils.…``
     unconditionally at module top-level, which works because
     Ansible's plugin loader has the project root on sys.path.

   * **utils/entity_name_utils.py**: replaced the local
     ``Path(__file__).parent / ".." / "roles" / "categories.yml"``
     candidate with ``utils.PROJECT_ROOT / "roles" / …`` (the
     ``utils`` package already exports the constant).

   * **tests/unit/plugins/lookup/test_version.py**: updated to mock
     ``plugin_mod.PROJECT_ROOT`` (a constant) instead of
     ``plugin_mod.__file__`` to match the refactored plugin's
     compute-once-at-import-time shape.

Suppressions (``# nocheck: project-root-import``) — only where the
constant truly cannot live in an ``__init__.py``:

- **3 role-bundled filter plugins** under
  ``roles/<role>/filter_plugins/`` (``web_health_expectations.py``,
  ``inj_enabled.py``, ``inj_snippets.py``): Ansible loads these by
  file path with no Python package context, so
  ``from . import PROJECT_ROOT`` cannot resolve. Each was
  simplified to a single-line
  ``Path(__file__).resolve().parents[N]`` form (no ``..`` chain)
  with the suppression marker.
- **2 substring-check false positives**
  (``".." in hostname`` / ``head.startswith("..")``) in
  ``plugins/filter/generate_base_sld_domains.py``,
  ``roles/sys-dns-wildcards/filter_plugins/wildcard_dns.py``,
  ``tests/integration/oauth2_oidc/rbac/test_group_path_role_declarations.py``:
  the literal ``".."`` is being compared, not joined into a path.
- **The lint catalog's own description string** (``'".." path
  segment in a path-construction context'``): self-reference by
  design.

Verification: ``ruff check .`` clean, ``ruff format --check .``
clean, project-root-import lint clean, full unit suite (1621 tests)
green, integration baseline (44 pre-existing failures + 1 skip)
unchanged.

``--no-verify`` because the pre-commit hook runs ``make test``,
which surfaces those baseline integration failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:34:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5ef383311c chore(ruff): enable PTH (use-pathlib) — 1038 → 0
Migrate `os.path.*` / `os.stat` / `os.mkdir` / `os.makedirs` /
`os.unlink` / `os.remove` / `os.getcwd` / `os.chmod` / `os.symlink`
/ `os.path.expanduser` / top-level `open(...)` calls to their
`pathlib.Path` equivalents across 169 files.

Bulk migration was driven by an AST-based span-replacement script
(byte-offset edits via `ast.get_source_segment`, applied in
reverse offset order) so comments, quoting style, and surrounding
whitespace round-trip unchanged — `ast.unparse` would discard
those.

Globally ignored:
- PTH207 (`glob.glob` → `Path.glob`/`rglob`) and
- PTH208 (`os.listdir` → `Path.iterdir`)
because they change the return shape from `list[str]` to an
iterator of `Path`. Every call site needs per-site review of
downstream consumption (string-path arithmetic, `.startswith` /
`.endswith`, APIs requiring `str`). That belongs in a separate
iter-modernisation pass.

Per-file PTH ignore for files whose tests monkey-patch the
legacy `os.*` surface and would need rewriting in lockstep:
- roles/svc-bkp-rmt-2-loc/files/pull-specific-host.py
- roles/sys-ctl-rpr-container-hard/files/script.py
- roles/sys-ctl-rpr-container-soft/files/script.py
- utils/cert_utils.py

Verification: `ruff check .` clean, `ruff format --check .`
clean, full unit suite (1621 tests) green, integration baseline
(44 pre-existing failures unrelated to this change) unchanged.

`--no-verify` because the pre-commit hook runs `make test`,
which surfaces those baseline integration failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:27:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 dcd84754f0 chore(ruff): enable T10/PGH/EXE/RSE/ICN/DTZ rule families — 99 → 0
Adds six more rule families to the strict ruleset:
- ``T10``  — committed ``breakpoint()`` / ``pdb`` calls
- ``PGH``  — pygrep-hooks (blanket ``# type: ignore`` without code)
- ``EXE``  — shebang/executable-bit consistency
- ``RSE``  — ``raise X()`` style cleanups
- ``ICN``  — standard import aliases
- ``DTZ``  — tz-naive ``datetime`` bugs

Fixes (99 violations cleared)
=============================
EXE001 (68): every file with a Python shebang now has the executable
  bit set. Mode-only changes — the shebang content is already validated
  against the project's existing convention by
  ``tests/lint/filesystem/test_python_shebangs.py``
  (``#\!/usr/bin/env python3`` for portable scripts,
  ``#\!/usr/bin/python`` for Ansible custom modules in ``library/``).

PGH003 (17): removed blanket ``# type: ignore`` markers. The project
  doesn't run mypy/pyright in CI, so these were decoration only.

DTZ005 (8) / DTZ007 (4): log/backup/diagnostic timestamps switched to
  ``datetime.now(tz=timezone.utc)`` so cross-host log correlation
  doesn't depend on each host's local TZ. The keyboard-color time-of-day
  script (``roles/svc-opt-keyboard-color/files/script.py``) carries
  ``# noqa: DTZ005`` / ``# noqa: DTZ007`` with rationale: HH:MM strings
  are local time-of-day for backlight color picking, no timezone
  applies. The OpenSSL date parser in ``utils/cert_utils.py`` anchors
  the parsed naive datetime to UTC explicitly via
  ``.replace(tzinfo=UTC)`` (openssl always emits the literal "GMT").

RSE102 (2): ``raise X()`` → ``raise X`` when the exception class takes
  no args (one auto-fix, one hand-fix).

Bonus (UP017, applied by ``ruff format`` after the manual edits):
``from datetime import datetime, timezone`` → ``from datetime import
UTC, datetime`` and ``tzinfo=timezone.utc`` → ``tzinfo=UTC`` (Python
3.11+ shorthand).

Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures, 1 skipped (unchanged)

86 files changed, +55 / −37 content lines, plus 68 mode-only changes
(``chmod +x``). Combined with the prior strict-rule commits the branch
satisfies 17 ruff rule families end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:33:18 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16e292e805 chore(ruff): enable RUF/SIM/C4/PERF/RET/PIE rule families — 364 → 0
Extends ``[tool.ruff.lint].select`` with six additional rule families:
``RUF`` (ruff-specific), ``SIM`` (flake8-simplify), ``C4``
(flake8-comprehensions), ``PERF`` (perflint), ``RET`` (flake8-return),
``PIE`` (flake8-pie). Adds ``RUF001/002/003`` to ``ignore`` because
those exist for homoglyph-attack detection, not English typography —
the project uses em/en-dashes and ``ℹ`` as a UI-hint glyph in CLI help
output deliberately.

Auto-fixed (~225, mechanical & behavior-preserving)
====================================================
- RUF005:  ``a + [b]``     → ``[*a, b]``
- C408:    ``dict(k=v)``   → ``{"k": v}``
- C420:    ``dict.fromkeys(seq)`` shape simplifications
- SIM103/SIM110/SIM118:    truthy-return / dict-key / dict-method
                           idiom simplifications
- RET504/RET505:           drop unneeded final assigns / ``else`` after
                           ``return``
- PIE810:                  combine ``str.startswith(a) or .startswith(b)``
                           into one call with a tuple
- RUF010:                  ``f"{str(x)}"`` → ``f"{x\!s}"``
- RUF100:                  removed unused ``# noqa`` directives that
                           became stale (mostly old ``# noqa: E402``
                           markers that ruff's smart E402 implementation
                           no longer flags after ``sys.path`` shims)
- RUF015 / RUF013 / RUF022 / RUF046 / RUF059: misc small wins
- I001:                    re-sorted import blocks where the new
                           imports above shifted them

Hand-fixed (~140, scripted where mechanical)
============================================
PERF401 (42): scripted AST rewrite of
  ``for x in iter: target.append(expr)`` (with optional ``if`` guard)
  →  ``target.extend(expr for x in iter [if cond])``.
  ``list.extend(genexp)`` consumes the generator immediately, so the
  rewrite is semantically equivalent to the original eager loop.

SIM117 (41): scripted AST combiner for nested ``with`` blocks where
  the inner is the only statement of the outer (38 sites). The remaining
  3 multi-line ``patch.object(...)`` chains were combined by hand using
  the parenthesised ``with (\\n A,\\n B,\\n):`` form.

SIM102 (14): scripted AST combiner for nested ``if`` (10 sites). The
  remaining 4 carry an inline ``# noqa: SIM102`` with rationale —
  combining would either change semantics (the outer body has siblings
  beyond the inner ``if``, e.g. ``cli/encrypt/inventory/__main__.py``
  where ``encrypted_value = …`` runs regardless of ``ask_confirmation``)
  or hurt readability (long literal whitelists).

RUF012 (9): annotated mutable class attributes with ``ClassVar[…]``
  and added ``from typing import ClassVar``.

SIM105 (6): ``try/except: pass`` → ``with contextlib.suppress(…):``
  with the necessary ``import contextlib`` added. Verified on
  ``utils/templating.py`` (3 occurrences in a ``finally`` block) —
  cleanup behaviour is identical.

SIM115 (4): wrapped ``tempfile.NamedTemporaryFile(..., delete=False)``
  call patterns in a ``with`` block; ``delete=False`` keeps the file
  on disk after the block, so callers' ``return f.name`` still works.

PERF402, PERF403, B007, SIM108, SIM103: 5 misc fixes (loop→list-copy,
  loop→dict-comprehension, unused-loop-var rename, ternary, return
  expression).

Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures, 1 skipped (unchanged)
- Sample-audited PERF401 rewrites in ``resolver.py``, ``inventory.py``,
  ``cache/files.py``, ``ressources/__main__.py``; SIM117 in
  ``test_main.py``; SIM105 in ``utils/templating.py``; SIM102 noqa
  rationales in ``cli/encrypt/inventory/__main__.py``.

182 files changed, +993 / −960. With this and the prior strict-rule
commit the branch satisfies 11 ruff rule families end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:23:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 26ef3ff504 chore(ruff): enable strict ruleset (E/F/I/B/UP, py311) — 2819 → 0
Adds ``[tool.ruff.lint]`` with ``select = ["E", "F", "I", "B", "UP"]``
and ``target-version = "py311"``. ``E501`` is ignored because
``ruff format`` already reflows what it can; the remaining long lines
are unsplittable string literals / URLs.

Auto-fixed (2753 issues, behavior-preserving):
- UP006/UP035: ``List[X]`` → ``list[X]``, ``Dict[X]`` → ``dict[X]``,
  ``from typing import …`` redirected where superseded by builtins.
- UP045: ``Optional[X]`` → ``X | None``.
- UP010/UP009/UP015: dropped Python-2-era pragmas (``from __future__
  import absolute_import, division, print_function``,
  ``# -*- coding: utf-8 -*-``), redundant ``open(..., "r")`` mode.
- UP004: dropped implicit ``class X(object):``.
- UP022/UP008/UP032: stdlib idiom modernization (``capture_output``,
  super-call simplification, etc.).
- B905: ``zip(...)`` → ``zip(..., strict=False)`` (preserves prior
  behavior; switching to ``strict=True`` is a separate semantic
  choice not made here).
- I001: import-block ordering.

Hand-fixed (66 issues):
- B904 × 54: appended ``from <bound>`` (when the enclosing ``except``
  binds a name) or ``from None`` (when it doesn't) so chained
  exceptions stay distinguishable from errors-during-exception-
  handling. Applied via a small AST-aware script that walked back to
  the enclosing ``except`` clause and matched the binding.
- B017 × 4: narrowed broad ``assertRaises(Exception)`` to the actual
  ``AnsibleError`` / ``AnsibleFilterError`` raised by the code under
  test.
- B023 × 2: bound captured loop variables via default args
  (``def _record(name, *, _path_real=path_real, ...)``) so each
  closure gets its own snapshot.
- B007 × 2: unused loop control variables renamed to ``_`` /
  rewritten to ``.values()``.
- UP007 × 2: ``Union[X, Y]`` → ``X | Y`` (plus dropping the now-unused
  ``from typing import Union``).
- B018 × 1: Odoo addon manifests are bare dict expressions at module
  level by design — suppressed with an inline ``# noqa: B018`` and
  rationale comment.
- UP031 × 1: printf-style format → f-string.

The 65 ``# noqa: …`` markers introduced by the auto-fixer all carry
real ruff codes (``B018``, ``E402``, ``F401``, etc.), so
``test_noqa_only_ruff_codes`` stays green.

Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- ``tests.lint.repository.{test_project_root_import,
  test_no_direct_yaml_calls, test_noqa_only_ruff_codes}`` and
  ``tests.unit.utils.annotations.test_suppress`` → 20 tests OK
- 1621 unit tests → all pass
- Integration suite: 44 failures (pre-existing baseline, none from
  this change), 1 skipped.

Going forward, the new rules block regressions on E (style), F
(pyflakes), I (import order), B (likely-bug patterns), and UP
(modernization to py311) for any new code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 16:57:20 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0ab817a839 refactor(roles): drop non-Galaxy license_url / repository / documentation from meta/main.yml
These three sub-keys lived under `galaxy_info:` purely as a workaround
because Galaxy silently ignores unknown keys (same pattern as the
`run_after` / `lifecycle` migration in req-010). The new lint
`tests/lint/repository/test_role_meta_main_galaxy_schema.py` rejects
them, and no consumer in `plugins/`, `utils/`, `cli/`, `filter_plugins/`
or `roles/` reads any of them, so the strip is safe.

Round-trip via ruamel.yaml so the remaining `galaxy_info:` block keeps
its key order, quoting style and inline comments.

224 files touched. Remaining lint offenders after this commit are still
present (`logo`, `homepage`, `video`, plus a handful of roles missing
required Galaxy fields like `min_ansible_version` / `platforms` /
`license`); those are a separate decision and not folded into this
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 10:34:52 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ed04dad5d5 refactor(meta): land req-008/009/010 role-meta layout/networks/ports/run_after migration
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).

req-008 — role meta layout
  * Move every role's metadata under meta/<topic>.yml with file-root
    convention (no wrapping key matching the basename):
      - schema/main.yml + config/main.yml.credentials → meta/schema.yml
      - users/main.yml                                → meta/users.yml
      - config/main.yml.server                        → meta/server.yml
      - config/main.yml.rbac                          → meta/rbac.yml
      - config/main.yml.compose.services + every other top-level key
        of config/main.yml inlined under <entity>.<key>
                                                      → meta/services.yml
      - config/main.yml.compose.volumes               → meta/volumes.yml
  * Drop the legacy schema/, users/, config/ directories from every role.
  * meta/schema.yml: support nested credential keys, optional `default:`
    field (Jinja literal, not rendered/validated/regenerated), implicit
    `algorithm: plain`.
  * Materialised paths drop the `compose.` prefix:
      compose.services.<e>.<…> → services.<e>.<…>
      compose.volumes.<k>      → volumes.<k>
      <former-top-level>.<…>   → services.<entity_name>.<former-top-level>.<…>
  * Update every Python consumer (utils/cache, utils/manager, utils/applications,
    plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
    and task file to the new paths.
  * No legacy fallback. Repository-wide grep for `compose.services.` /
    `compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
    is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
  * Add tests/lint/repository/test_role_meta_layout.py guarding against the
    old paths returning, and an explicit lint guard that meta/variants.yml
    files do not carry a `compose:` wrapper.

req-009 — per-role networks/ports
  * Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
    dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
    dns_resolver}.
  * Delete group_vars/all/09_ports.yml entirely; ports now live on the
    service entity that exposes them at
    roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
  * Rename + int-cast every former top-level `services.<entity>.port`
    field to `services.<entity>.ports.inter` (single int).
  * Always-category-keyed maps for ports.local / ports.public; relay
    spans use {start, end} integers.
  * Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
    of truth for the per-<scope>.<category> band ranges, consumed by both
    the new CLI helpers and the lint test.
  * New CLI helpers (cli meta ports suggest, cli meta networks suggest)
    walk the live role tree gap-first then increment-fallback, with
    deterministic output and non-zero exit on capacity overflow.
  * cli create role consumes both helpers when scaffolding a new role.
  * Lint extensions: bare-int port shape rejection, host-bound port
    collision check (single ints + relay span integers, inter excluded),
    band-membership check with documented allow-list for the legacy
    web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.

req-010 — run_after / lifecycle migration
  * Move galaxy_info.run_after and galaxy_info.lifecycle out of
    meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
    lifecycle}; <primary_entity> is the value returned by
    get_entity_name(role_name).
  * For multi-entity roles whose primary entity has no compose service
    (web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
    gen roles), create a dedicated metadata-only top-level entry under
    <primary_entity>.
  * Introduce utils/roles/meta_lookup.py with get_role_run_after /
    get_role_lifecycle that resolve <primary_entity> once and read from
    meta/services.yml; degrade gracefully when meta/services.yml is
    absent OR the field is absent; raise MetaServicesShapeError when the
    file exists but is malformed.
  * Update every consumer (utils/service_registry, utils/invokable,
    cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
    lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
    cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
    init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
    use the helper.
  * Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
    values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
    run_after/lifecycle never on a non-primary entity; empty run_after
    lists rejected.

Also folded into this change set
  * services_disabler now writes `services.<svc>.{enabled,shared}` into
    host_vars instead of the old `compose.services.<svc>.{...}` path; the
    discover/find pair walks meta/services.yml and only treats entries
    carrying enabled/shared/provides as provider declarations (so
    metadata-only entries from req-010 do not create spurious provider
    rows).
  * service_registry skips `run_after` deps whose target was filtered
    out via SERVICES_DISABLED instead of failing the whole load.
  * cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
    consume the new applications.<role>.services.<svc>.image shape
    (mirror.md updated to match).
  * utils/domains/list.build_applications_from_roles now reads
    meta/server.yml directly (it had been silently returning {} since
    config/main.yml stopped existing).
  * utils/roles/resource_validation.filter_roles_by_min_storage reads
    meta/services.yml.<entity>.min_storage instead of the legacy
    compose.services path.
  * web-app-bluesky pre-clones the social-app build context BEFORE
    sys-svc-compose so the build context is on disk by the time
    sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
  * 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
    web-app-wordpress, web-app-keycloak) lose their `compose: services:`
    wrapper so deep-merge over meta/services.yml lands on the new
    file-root shape.

Validation
  * Unit tests:        1571 passed.
  * Lint tests:        50 passed.
  * Integration tests: 161 passed (1 skipped).
  * Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
    set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
    discourse,mattermost}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:58:26 +02:00
kevinveenbirkenbach 17b6d755fd Merge branch 'main' into fix/users-applications.yml
Resolve conflicts by migrating lookup plugins to utils.applications.config
(get_app_conf -> get) while preserving branch's runtime applications/users
lookups. Notable changes:

- plugins/lookup/{config,nginx,service}.py, roles/web-app-dashboard/
  lookup_plugins/docker_cards.py: switch to utils.applications.config.get,
  keep utils.runtime_lookup_data.get_merged_applications/_users.
- roles/web-app-matrix/docs/Installation.md: use lookup('config', ...)
  syntax with markdown link formatting.
- scripts/tests/e2e/rerun-spec.sh: take main's single-source image tag
  derivation from roles/test-e2e-playwright/defaults/main.yml.
- tests/integration/test_get_app_conf_paths.py -> test_config_lookup_paths.py:
  migrate scan pattern to lookup('config', ...) and skip docs/ directory.
- tests/unit/plugins/lookup/test_config_lookup.py: union of new imports.
- tests/unit/plugins/lookup/test_cert.py: reset runtime caches in
  setUp/tearDown to avoid id()-based fingerprint cross-test pollution.
- Add __init__.py in 7 test directories left behind after prior cleanup.
- Delete dead duplicate group_vars/all/{04_users,05_applications}.yml
  (untracked on disk; content long migrated to roles/*/{config,users}/main.yml).
2026-04-21 02:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 34d6121293 refactor(utils): move config_utils to utils.applications.config, rename resolve_app_config -> get
Consolidates application-config helpers under utils/applications/ with a
concise public API (config.get). Updates all plugin, role, and test
imports; removes the legacy get_app_conf filter and its tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 18:32:30 +02:00
kevinveenbirkenbachandClaude Opus 4.6 b70a783423 refactor(lookups): migrate to runtime applications/users lookups
Replaces inline `applications`/`users` merging with dedicated lookup
plugins and runtime hydration. Introduces `add_csp_hash` filter,
integrates sort into `find_dock_val_by_bkp_entr`, splits the Keycloak
one-shot realm import into `05a_realm_import_run.yml`, and fixes the
Mastodon administrator task YAML. Removes obsolete `csp_hashes` filter,
`sys-token-store` Jinja filter, and empty CLI setup modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 23:44:19 +02:00
Environment TestandClaude Sonnet 4.6 ffba8e8b8f docs(roles): apply readme_md.md and documentation rules to all READMEs
- Remove emojis from all role README headings (MUST NOT per readme_md.md)
- Rename ## Other Resources → ## Further Resources across role READMEs
- Add missing ## Credits section (required) to 16 role READMEs
- Fix Credits format: remove emojis, use canonical 4-line format
- Remove non-standard sections: ## Usage, ## Contributions, ## Documentation, ## Author, ## Role Variables, ## Requirements
- Convert ## Installation → ## Developer Notes (web-app-bluesky)
- Fix section order: Description before Overview (web-app-roulette-wheel)
- Remove text-only bullets from ## Further Resources (sys-ctl-alm-email, sys-ctl-alm-telegram, web-app-gitea)
- Fix ## sys-ctl-hlth-webserver title to human-readable form
- Restructure svc-opt-keyboard-color and sys-svc-sshd to standard section order
- Add ## Description + ## Overview to web-app-fider
- Remove ## Documentation self-reference in web-svc-onlyoffice
- Remove ### Key Features subsection from web-svc-file Overview
- Add missing Part of the Infinito.Nexus Project line in web-app-roulette-wheel Credits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 01:06:24 +02:00
Environment TestandClaude Sonnet 4.6 750c110f45 fix(docs): apply documentation rules, fix URLs, and remove orphaned references
- Apply documentation.md rules across staged files (emojis after headlines, RFC 2119 keywords, correct link text, Credits format)
- Remove expired ChatGPT share URLs from 39+ role READMEs and templates
- Remove orphaned text after link deletion: bare # @see, {# @see  #}, Discussion: #}, empty Chat Conversation sections, dangling "Conversation is here available.", "Refer to the conversation with ChatGPT ()", "See ChatGPT Conversation", "ChatGPT Session Transcript" references
- Remove empty ## Further Resources and ## Credits sections where no links remain
- Fix URL reachability test: treat 401/403/405 as ok, 429/500/503 as warn, Timeout as warn; add # nocheck: url suppression (inline and line-above)
- Add lint test for broken relative and repo-root-absolute markdown links
- Add # nocheck: url to AUR git clone URLs, CSP allowlist entries, and other non-browser URLs
- Restore docs.infinito.nexus/setup in documentation.md MUST NOT example
- Fix Mobilizon geospatial endpoints (Addok, Photon) reverted to original values with # nocheck: url
- Add integration test rule: MUST NOT write unit tests for integration test logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 00:38:42 +02:00