Commit Graph
64 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 284b0873dd fix(sys-ctl-hlth-runtime): harden LAM CLI, catch apk options, drop vars comment
Addresses three CodeRabbit findings on PR #1265.

1. LAM hardening invisible to the probe (thread 4413764929): the runtime
   check probes PHP with `php -r`, the CLI SAPI. web-app-lam mounted the
   hardening ini only into /etc/php/8.4/apache2/conf.d, which the Debian
   CLI does not scan, so the probe read unhardened CLI settings and the
   check failed for LAM. The php_hardening entry now also mounts the ini
   into /etc/php/8.4/cli/conf.d. No other role mounts it into a Debian
   per-SAPI directory; the rest use the shared scan dir of the official
   image or of Alpine. The TODO entry now names the remaining gap: the
   probe reads only the CLI copy.

2. Explanatory comment above HEALTH_RUNTIME_FLOORS (thread 4413764949):
   removed. The README already states the end-of-life rule and now also
   defines a floor as the lowest patched release of each line.

3. `apk --no-cache add` missed (thread 4415018662): PACKAGE_INSTALL_PATTERN
   required `add` right after `apk`, so `RUN apk --no-cache add php7`
   passed as an image without PHP. The apk branch now accepts options
   before `add`, like the apt branch. New tests cover the option form and
   guard that `apk` in a path or a comment is no package install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:57:21 +01:00
kevinveenbirkenbachandClaude Opus 5.5 d89c74feab feat(security): harden PHP services and proxy response headers
Proxy:
- server_tokens off, X-Powered-By stripped from upstream responses,
  HSTS max-age raised to one year.
- No global X-Content-Type-Options: under nosniff, ORB (step 3.4)
  blocks no-cors cross-origin responses typed text/html, e.g. tracking
  beacons answered with an empty 204.

PHP (CVE-2026-109618, CVE-2026-109619):
- New role sys-svc-compose-php installs a shared ini (expose_php Off,
  session.upload_progress.enabled 0, fatal_error_backtraces 0 for
  PHP 8.5, interim memory_limit -1) and a PHP-FPM [www] drop-in
  (pm.max_requests = PHP_FPM_MAX_REQUESTS) once per stack host,
  before 04_files renders any compose file.
- The host file names carry a sha1 prefix of the controller-side
  source, so a content change alters every PHP app's compose.yml and
  recreates it, also on remote deploys where compose_volumes cannot
  read the stack host file and hashes the path instead.
- 20 PHP roles mount the ini as a compose/swarm config into their
  image's scan dir; moodle, nextcloud and shopware also mount the FPM
  drop-in. Magento keeps its own pool: its php-fpm.conf has no
  include= and is rendered without notify.
- Joomla's php-upload.ini now loads as zzz-joomla-uploads.ini so its
  own memory_limit wins: the media API compares Content-Length with
  toBytes(memory_limit) and rejects every upload at -1.

TODOs record the memory_limit revert condition and accepted
trade-offs, Akaunting on PHP 8.1, FusionDirectory on PHP 7.4 and the
public Keycloak admin console.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 21:03:38 +02:00
evangelostsak 2f9563f064 feat(web-app-lam): let a deployment put its hosts into meshes 2026-10-16 03:40:18 +02:00
infinito-nexus-updater[bot] 1862c5b50b update(images): update Docker image versions (web-app-lam) 2026-10-04 03:13:59 +02:00
evangelostsak 9261ac9b01 docs(web-app-lam): record why script-src-elem cannot be tightened
The previous reason named only multi edit, which made the relaxation look
specific to one tool and newly needed. Neither is true.

LAM emits inline <script> elements through htmlJavaScript from 18 shipped
files, covering two-factor setup, deletion, tree view, mass upload, PDF
editing, several account modules and the config pages. Every one was blocked
before; nothing had driven those paths until a test used multi edit.

Hashing was evaluated and does not work here. A directive carrying any hash
makes browsers ignore unsafe-inline entirely, so all 18 sites would have to be
enumerated, and several build their snippet per request.
2026-09-21 18:35:42 +02:00
evangelostsak 912b6cda39 Merge infinito-nexus/core#454 (feature/ai-agent-employees) into feature/web-app-openbao
Only two files were touched by both sides. env.js merged automatically: the
incoming side owns recordVideoOptions and this branch had cherry-picked that
same function from it, so the result carries one definition. README.md is the
generated roles index and both sides append rows; resolved as a union, taking
the incoming side for the three rows it edits (OnlyOffice, Open WebUI, OpenClaw)
and keeping ours only for OpenBao, then re-sorted.

The incoming side adds lint rules and resources that this role then violated.
Five fixes, each the remedy the failing rule names:

* services.openbao.ports.local.http 8081 -> 8093; svc-ai-litellm now holds 8081.
* meta/networks.yml subnet 192.168.221.0/24 -> 192.168.222.0/24; svc-ai-mcp-adapter
  now holds 221, and the suggester only sub-allocates inside occupied /24s, so
  the next free block was taken directly.
* services.email bond 1 -> 0; the entry is disabled and the resource model skips
  it before reading the weight.
* README Quick Setup regenerated from the template it had drifted from.
* Credits link the implementer, matching how they are credited elsewhere.
* docs/requirements/035-mcp-proxy-expansion.md disposes of web-app-openbao as
  privileged_control_plane, which is what a secrets manager is, and the list
  count follows.

Verified against a worktree of #454 alone so the baseline is measured rather
than assumed: lint 501 passed with only the branch-name rule failing, which
fails identically on that baseline because the worktree branch is named
merge/*; unit 5665 passed with the same three host-environment failures the
baseline has; integration 324 passed, 0 failed.
2026-09-14 16:59:39 +02:00
evangelostsak 4ca69f1244 fix(web-app-lam): allow the inline script that drives LAM's asynchronous tools
script-src-elem.unsafe-inline was off, so the progress driver LAM renders
inline was blocked by the CSP. LDAP import/export and Multi edit posted their
job, rendered "Status: in progress" and never polled it, leaving both tools
hanging for every user. Found while driving Multi edit from a Playwright spec;
the directory was verifiably unmodified afterwards, so it was not a display
artefact.
2026-09-12 10:22:21 +02:00
kevinveenbirkenbachandClaude Opus 5 4ac07edaea refactor(meta): move the info-media exemption to the file it speaks about
The marker names the reason a role has no upstream homepage or demo video, and
it sat in meta/main.yml because a comment-only info.yml parses to an empty
document, which the schema lint rejected outright. So the exemption lived one
file away from the two fields it explains, and a reader looking for the video
found neither the field nor the reason it was missing.

The schema lint now accepts an empty document exactly when it carries the
marker, and that is what allows the move. The media lint reads the marker from
info.yml rather than main.yml, and its failure message names that same file, so
the fix it suggests lands where the absent field would have gone.

A hundred and two roles follow. Every occurrence was a standalone comment on
the second line of main.yml, directly under the document start, so the move is
a line delete and an insert at the same offset. Seventy-three roles already had
an info.yml and gain the line; twenty-nine get one whose whole content is the
document start and the marker. All hundred and two reasons carry over verbatim,
including the thirty-five bespoke ones.

Nothing downstream sees a change. Of the four readers of meta/info.yml, three
coerce a null parse through `or {}` behind an isinstance guard, and
utils/cache/applications.py skips a falsy parse before assignment, so
applications.<role>.info stays absent for the twenty-nine exactly as it was
while the file did not exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:26:38 +02:00
kevinveenbirkenbachandClaude Opus 5 596b10c403 test(lint/csp): demand reason for unsafe-* grants
unsafe-inline re-admits the inline execution an XSS needs and unsafe-eval re-admits string-to-code, so a CSP relaxation must be verified against the running app rather than copied from another role. The new lint requires a nocheck marker plus a Reason on the flag's own line, so a marker cannot drift to the neighbouring directive.

The 197 pre-existing grants across 70 roles are checked out with a TODO: they were never verified and still have to be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:28:14 +02:00
kevinveenbirkenbachandClaude Opus 5 e16eb5728d test(lint): drop the minimal variant's exemption from the onion
72fd9c870 required the onion in every variant but exempted variant 1, on the
argument that the minimal round is the one place the stack runs without it. That
argument does not hold: the onion is part of the deployment under test, not a
feature bolted onto the full shape. A round that skips it is not a smaller
deployment, it is a different one.

So the exemption is gone and 95 variant-1 entries pin the onion on. Two
exclusions remain, both read from data rather than a comment: a role with no tor
block has no onion to pin, and a role whose meta/services.yml pins tor.enabled to
a literal false has already declared it has none.

That put the rule in conflict with test_coverage, which requires both polarities
of every dynamic flag: with tor on everywhere, the false side has no variant left
to live in. 94 roles take the exemption that contract already provides for
exactly this - "flags whose group-derived value is a hard invariant that variants
must never override", the same one 4e6ef2542 used for the seaweedfs frontend.
web-app-nextcloud needs none: its variant 8 pairs the clearnet-only PeerTube and
keeps the false polarity.

Measured, because the declaration is not what CI runs. assign_tor gives the onion
to every second capable entry with the run number as the parity switch, so more
capable variants do not mean proportionally more onion jobs: across five roles
and 21 entries the count goes 8 -> 10 on an even run and 9 on an odd one. The
clearnet path also survives - checkmk reports v1=false on run 0 and v1=true on
run 1, so both states are still covered across two consecutive runs. What the
94 markers give up is a declarative guarantee in variants.yml, not the coverage
itself.

The real cost is weight, not job count. web-app-nextcloud variant 1 goes from
3.09GB reservation / 7.27GB limit to 18.26GB / 43.01GB once svc-net-tor and its
closure join, so the cheap round is no longer cheap. Every variant stays under
the 32/64GB budget, and the same effect applies to the other 94 unmeasured.

Verified: tests/lint 336 passed / 307 subtests, tests/integration/roles/meta/
variants plus the role-meta lints 39 passed. The rule was checked in both
directions - it reported the variant-1 offenders before the flips and none after.

Not measured: what the heavier rounds do to wall-clock, against swarm jobs capped
at 355 minutes and one that has hit that ceiling before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 06:09:02 +02:00
kevinveenbirkenbachandClaude Opus 5 ab72cff929 test(lint): require the onion in every variant, not just SSO ones
7a70d27b7 made a variant that switches SSO on also switch the onion on, because
that pairing is where the platform breaks in ways clearnet never shows. The same
argument reaches further than SSO: a Secure cookie is dropped, an https identity
provider refuses to embed, and crypto.subtle is undefined for any feature on a
non-secure origin, not only for a login. A variant that never runs the onion is
not testing the deployment users get.

So the rule now covers every variant of a role that declares a tor block.
Variant 1 stays exempt - it is the minimal, partner-free round every role
declares and the one place the stack is exercised without the onion at all - and
a role whose meta/services.yml pins tor.enabled to a literal false is skipped
outright, because that declaration already says the role has no onion and needs
no comment repeating it.

That found 137 variants. 101 were variant 1. Of the rest, 28 are the LDAP-only
auth-isolation rounds, which gain real coverage from running LDAP over the onion
and are flipped on here. Four cannot: svc-net-tor is the provider itself, and
nextcloud's variant 8 pairs the clearnet-only PeerTube. Both carry a marker
naming the reason; opentalk and coturn need none, since their services.yml pins
the literal false the rule now reads.

Flipping them exposed a contract that contradicted itself. test_auth_isolation
and test_non_baseline_explicit_disables demanded `shared: false` beside
`enabled: false`, while variants-static-override flagged that same line as a
static override - which is why all 133 of them carried a nocheck marker. The
demand only makes sense where `shared` is dynamic: a literal base value cannot
leak through deep-merge, so restating it protects nothing. Both contracts now
ask for it only in that case, and the 133 lines are gone.

The result is a stricter rule with fewer lines: 189 insertions against 193
deletions.

Verified: tests/lint 336 passed / 307 subtests, tests/integration/roles 107
passed / 610 subtests. The rule is checked in both directions - it reported 137
offenders before the resolution and none after.

Not measured: 28 additional onion rounds cost matrix wall-clock, against swarm
jobs already capped at 355 minutes and one that has hit that ceiling before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 06:09:02 +02:00
kevinveenbirkenbachandClaude Opus 5 b2a0a317fa refactor(tor): loosen the onion bond to 0.5
Every variants.yml header states the rule the bond feeds: variant 0 carries each
tightly-coupled (bond>=1, same-host) service, while loosely-coupled (bond<1,
separate-host) partners are excluded from the closure and distributed across
later variants to keep every host under budget. tor was declared bond: 1 in all
101 consumers, so it had to sit in every variant-0 closure.

It does not belong there. The onion is a separate-host provider reached over the
node's hidden service, the same shape as the bridged partners that already carry
0.5, and f1dff50fb established both the reasoning and the precedent when it
dropped xwiki and bigbluebutton from 1 to 0.5: bridged partners with their own
role and their own spec, whose bond declaration was the outlier rather than
their placement.

This also unblocks the variant-level tor contract. Requiring a non-baseline
variant to pin the onion on while tor still counted as tightly coupled would
have pulled variant 0's closure requirements along with it.

Verified: tests/lint 336 passed / 307 subtests and tests/integration/roles 107
passed / 610 subtests, including test_resource_budget over every variant's
deduplicated footprint and the bond-aware test_order closure.

Not measured, and the honest cost: f1dff50fb's own caveat applies unchanged
here. Whether redistributing a service out of the baseline closure buys back
wall-clock is not established; role-runtimes has the per-role seconds and no run
has been made with this shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 06:09:01 +02:00
kevinveenbirkenbachandClaude Opus 5 8a812c20ec fix(variants): pin the dev password where the role reads it
A variant entry is baked into host_vars raw, not deep-merged, so its top-level
key IS the inventory path. keycloak and lam still pinned their Playwright
administrator_password under credentials, while f713ba835 moved both roles'
vars/main.yml to secrets.credentials. The write and the read had been pointing
at different places ever since.

Nothing failed, which is the problem: the read resolves, because the generator
had produced a random password at the new path, so the deploy came up with a
secret nobody knows and the admin sign-in the pin exists for broke with no
trace. The inventory validator could not catch it either -- it widens the legal
keys with the variant overlays themselves, so the stale root was legal exactly
because the variant declared it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:30:23 +02:00
kevinveenbirkenbachandClaude Opus 5 e96c752549 Merge fix/playwright/env-template-gate into feature/svc-net-tor
75 commits: the socialhome role reimplemented against the project it actually
deploys, native-login specs for funkwhale and shopware, a window.open IdP
handoff and a settings-link fallback in the shared persona helpers, a swarm
converge budget that gives up on a terminal task, and a long tail of role
fixes.

Eleven conflicts. The shared persona helpers (keycloak.js, logout.js) take the
incoming structure — they gained real behaviour this branch never had — with
this branch's resolveTimeout convention re-applied over it and
confirmKeycloakLogoutIfPrompted re-attached at every return point; without it
the Keycloak SSO session outlives the logout. playwright.config.js keeps this
branch's version: both sides set actionTimeout, but ours routes through an env
var and 15s is too short behind an onion. socialhome's spec takes the incoming
content — 2dbd6eb10 found the role described the wrong upstream project
entirely — and keeps gotoOnion and expectHstsWhenTls, since demanding HSTS over
a plaintext onion is a false failure. Both READMEs were regenerated rather than
merged by hand.

Three collisions arrived with no conflict marker at all, because each side
changed a different place in a different file:

- group_vars/all/18_swarm.yml declared swarm_stack_converge_retries twice.
  Same value, so nothing breaks today; editing the documented one would have
  silently changed nothing.
- svc-net-tor and web-app-socialhome both claimed 192.168.74.0/24. Socialhome,
  the newer role, moves to 192.168.75.0/24.
- web-app-socialhome/meta/schema.yml arrived as a new file, so ef5ea7cb5's
  rename never saw it. Its credential would not have resolved and the role
  would have deployed with an unusable administrator account.

The incoming specs also needed this branch's conventions: raw timeouts wrapped,
four page.goto calls routed through gotoOnion, a request.get given an
onion-aware timeout, and 21 roles marked playwright-service-flag over the tor
block this branch added to them.

make quality green on the merge result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:20:19 +02:00
kevinveenbirkenbachandClaude Opus 5 ef5ea7cb55 refactor(roles): name the credential file meta/secrets.yml
meta/schema.yml held credential definitions and their runtime values, but the
name said 'schema' and read like structural metadata next to services.yml and
volumes.yml. Nothing in the file is a schema in the sense the other meta files
use the word, and a contributor had to open it to learn it holds secrets.

62 role files renamed. ROLE_FILE_META_SCHEMA becomes ROLE_FILE_META_SECRETS and
keeps its role as the SPOT the path is built from.

Two readers bypassed that constant and spelled the name themselves:
utils/cache/applications.py built meta_dir / 'schema.yml' twice, and
plugins/lookup/application_gid.py carries the filename in the marker list it
detects application roles by. Left behind, the rename would not have raised —
credentials would have resolved empty and the role would have stopped counting
as an application.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 12:08:06 +02:00
kevinveenbirkenbach e04c448cdd Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Three conflicts, none resolved by picking a side wholesale:

- Makefile: took the incoming ACT_INPUTS, which routes through SWARM_DISTROS
  instead of inlining the same expression a second time.
- test-e2e-playwright/02_run_one.yml: took the incoming rc handling. An absent
  rc now counts as failure; the previous default(0) let an aborted Playwright
  run pass silently, contradicting the debug message right above it.
- web-app-peertube spec: combined both. Kept gotoOnion and resolveTimeout,
  which this branch's lint enforces, and adopted the incoming retry-inside-poll
  for the OIDC button plus its networkidle wait.
2026-07-30 15:38:15 +02:00
kevinveenbirkenbachandClaude Opus 5 028f4b85f4 chore(storage): set every image-backed min_storage to its image size plus 200MB
The declared values were guesses that ran up to 60x the size of the image they belong to: seaweedfs asked for 30GB against a 104MB image, nextcloud for 20GB against 353MB. Because min_storage feeds the storage guard, the runner packer and the sufficient-storage filter, those numbers made hosts look too small and pruned caches that deploys did not need to lose.

Every service that declares min_storage together with image and version now carries the measured compressed transfer size of that image plus 200MB of headroom. Sizes were read from the registries through utils.docker.registry.manifest_transfer_size, the same path the headroom test uses, so the declarations and the gate agree by construction. Values are written in MB throughout, matching the existing 500MB / 100MB / 50MB style.

119 declarations across 89 roles. Range: 206MB (memcached) to 3473MB (ollama); four exceed 1GB. The 84 min_storage entries that name no image are untouched, since there is nothing to measure them against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:08:51 +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 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 Fable 5 da598a2d67 feat(readme): roles overview table with homepage/video backfill
Add cli.build.readme.overview (make readme-index / make docs) rendering a
roles overview in the root README: Name, Lifecycle, Homepage, Video,
Description, Integrated with, Install. New info-media lint requires
homepage + video in every invokable role's meta/info.yml (opt out via
'# nocheck: info-media' in meta/main.yml); backfilled 147 roles with
researched, adversarially-verified homepages and official videos.

Retire the 'unsupported' lifecycle value: fold it into 'eol' as a valid
live end-of-life status (shipped but untested/unmaintained) across the
stage lists, symbol glossary, allow-list lint and docs. Retag confluence,
jira, minio, phpldapadmin to eol and magento to pre-alpha.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 19:10:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7a6268fa96 refactor(deploy): replace skip with services modes
Per-role deploy-mode opt-out moves from meta/tests.yml 'skip:' to meta/services.yml <entity>.modes.{compose,swarm}.enabled on the primary entity. get_role_skip now derives from it; get_role_mode_enabled added. A new lint requires every role shipping templates/*compose*.yml.j2 to declare both mode flags. Migrates 107 services.yml and drops 11 skip-only tests.yml. Also removes the variant_status deployability gate so swarm runs one job per variant unconditionally (variant_status.py and deployable_variant_indices deleted, complexity model simplified). Adds the i8matix alias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 03:44:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4473edafb2 feat(roles): declare the container_backup consumer flag on stack roles with volumes
Every invokable stack role that declares named volumes in meta/volumes.yml now carries the standard container_backup consumer entry: enabled resolves via 'svc-bkp-container-2-local' in group_names, shared stays literally true since there is no per-consumer backup instance. Playwright/dynamic-flag/bond-baseline suppressions ride on the block per the established idioms; stale explanatory comments in the touched files are dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c2fa2da88e style: apply make autoformat (whitespace only)
Indentation/whitespace normalization emitted by make autoformat across role meta/services.yml, meta/variants.yml and a task file. No semantic changes (verified: git diff --ignore-all-space is empty for every file). Kept separate from the Tor egress-router feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:22:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1f60e2b3fa feat(variants): tor variant coverage for keycloak/lam/matrix/wordpress/rdr-www
Add the tor.enabled true/false rounds (shared:false + nocheck on the disabled round) so the dynamic tor flag is covered on these roles, and reorder each variant services-first so the tor nocheck registers. Each variant keeps its exact keys (credentials / addons / server.domains preserved, values unchanged) — mapping key order only; no cross-variant moves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0a7164af48 feat(tor): per-role onion service block in meta/services.yml
Domain-serving roles reference the svc-net-tor provider via a tor: block. Consumers declare enabled (dynamic group_names gate) and shared: true (static — onion is always the shared node model, inline nocheck: dynamic-flag). The provider svc-net-tor additionally declares exclusive/primary defaults that consumers inherit via resolve_service_config unless overridden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:16 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a11c37fb94 chore(images): pin service image versions to concrete tags
Replace version: latest with the newest stable semver tag across 21
roles (node pinned to the 24 LTS where a rolling latest broke the
build). The weekly update workflow now inspects these pins and proposes
bumps via PR instead of silently tracking a moving tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 05:05:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6cddce1db0 style(meta): normalize YAML indentation and wrap long scalar
Autoformat (mbake/yamllint) re-indented sequence items under their mapping key
across the regenerated meta files and wrapped one long URL scalar in the matomo
inject task. Whitespace/formatting only, no semantic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:36:57 +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 36ebf34b64 Merge branch 'main' into feature/swarm-nfs
Resolve 69 conflicts across ~70 files: union main's image-version bumps and new service fields (bond, resource limits) with the swarm branch's default_placement / NFS / IS_STACK_HOST config; retire the three per-deploy-type test workflows main still edited (consolidated into compose+swarm); adopt main's run-once 01_core dispatchers while re-applying each role's swarm guard. Fix the post-merge lint surfaced by main's stricter rules: bond + resource-budget keys, container-exec stack-host gating + resolver routing, leaky/dead proxy vars, suitecrm once-flag, xwiki variant nocheck, nextcloud oversized-block to gated include, matrix-mdad capped at 3g. make autoformat clean, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:13:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 deafbad941 fix(networks): widen every role local subnet from /28 to a unique /24
The per-role swarm overlay local subnet was a /28 (14 usable IPs). A role's swarm stack plus its sidecars and per-task network attachments can need more than that, which surfaces as 'node is missing network attachments, ip addresses may be exhausted' and leaves services (sso-proxy, app frontends) unresolvable mid-deploy. Re-allocate all 74 /28 roles to a unique /24 (254 usable) inside 192.168.0.0/16, chosen via the network address suggester so the assignments stay non-overlapping. Validated by tests/integration/infrastructure/networks/test_unique_valid_and_mapped.py and a full make test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 08:46:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 3fa781004d feat(roles): add bond integration-importance factor to shared services
`bond` is a 0..1 factor for how strongly a service is tied to the overall
integration: bond: 1 = always required, bond: 0 = omittable. Groundwork for
tooling that later scores service importance and decides what can be dropped.
Named `bond` (not `bound`) to avoid clashing with the resource bounds/limits.

Add test_service_bond.py (lint): every enabled + shared service must declare a
valid `bond` (number in 0..1). Seed `bond: 1` as a placeholder on all 638
enabled+shared service entries across 88 roles, to be refined later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 25f286d2bf feat(swarm): per-service internal ports and entity-named services
Give each reverse-proxied app a swarm-resolvable upstream: add services.<entity>.ports.internal.http to the role metadata and rename the main compose service from the generic 'application' to the entity name, so its short DNS alias on the shared overlay is unique. Thread the service name through compose, vars (*_SERVICE_NAME), volume mounts and the oauth2 origin host. Consolidate the split image/ports blocks in bridgy-fed and postmarks into the entity block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbach 21d81ccdc2 Merge branch 'main' into feature/req-21-sso-flavor-migration 2026-05-27 18:24:00 +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 2cec31ec5c feat(web-app-lam): register as service provider; opt moodle v0/v1 in
Add enabled/shared/provides: lam to web-app-lam's primary service block
so consumer roles can declare services.lam.{enabled,shared} and have
the variant-aware deploy planner resolve to web-app-lam. Align tasks/
main.yml with the run_once_<slug> guard pattern (split entry into
tasks/01_core.yml that opens with the utils/once/flag.yml include).

In web-app-moodle, opt variants 0 (full stack) and 1 (LDAP-only) into
the new service via meta/variants.yml — both variants where ldap is
enabled. Per request keep lam out of moodle's meta/services.yml; the
variant overrides therefore carry the only declaration and need
# nocheck: variants-services-match markers. playwright.env.j2's LAM
guard and LAM_SERVICE_ENABLED switch to the same lookup-based config
flag as the other service gates (matomo, email, ldap, …) for uniform
plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:29:50 +02:00
kevinveenbirkenbachandClaude Opus 4.7 24d4ab8e4a refactor(sso)\!: collapse oauth2 + oidc into unified services.sso block (req 21)
Implements docs/requirements/021-sso-flavor-migration.md atomically:

* Schema: every role's meta/services.yml replaces the parallel
  `oauth2:` / `oidc:` top-level blocks with a single `sso:` block
  carrying `enabled`, `shared`, and a `flavor` discriminator
  (`oidc` | `oauth2` | `saml`; default `oidc`). Flavor-specific
  sub-keys live under `sso.<flavor>.*` (`sso.oauth2.{origin,acl,
  allowed_groups}`, `sso.oidc.plugin`).
* Provider role: roles/web-app-oauth2-proxy/ is deleted in its
  entirety; its 5 sidecar templates move under
  roles/web-app-keycloak/templates/sso_proxy/, and the per-consumer
  rendering task lives at roles/web-app-keycloak/tasks/sso_proxy.yml.
  web-app-keycloak now declares `provides: sso`.
* Runtime: new resolver utils/roles/applications/services/sso.py
  centralises the SSO predicates (`is_enabled`, `is_proxy_gated`,
  `is_oidc_native`) and the oauth2 sub-values
  (`oauth2_origin_{host,port}`, `oauth2_acl`, `oauth2_allowed_groups`).
  Three lookup plugins consume the resolver: `sso` (single-app
  predicate access), `sso_proxy_consumers` (enumerates oauth2-flavored
  consumers), `sso_oidc_plugin` (renamed from `oidc_flavor`, Nextcloud
  plugin selector). sys-stk-backend, sys-svc-compose, sys-svc-proxy
  and plugins/filter/compose_volumes.py all gate on
  `lookup('sso', ..., 'is_proxy_gated')` instead of manually combining
  `enabled` and `flavor`.
* Identifier renames repo-wide: `services.<entity>.ports.local.oauth2`
  → `ports.local.sso`; `PORT_BANDS.local.oauth2` → `PORT_BANDS.local.sso`;
  `OAUTH2_PROXY_*` env/Jinja vars → `SSO_PROXY_*`;
  `oauth2_proxy_cookie_secret` → `sso_proxy_cookie_secret`;
  `OAUTH2_SERVICE_ENABLED` + `OIDC_SERVICE_ENABLED` collapse into
  `SSO_SERVICE_ENABLED`; Playwright gating helpers
  (isServiceEnabled/skipUnlessServiceEnabled/requireService/safeIsEnabled/
  safeSkipUnlessEnabled) move from `oidc`/`oauth2` to `sso`.
* Dual-block resolution: per req's Background-Table, web-app-bookwyrm
  is `flavor: oauth2`; web-app-gitea and web-app-friendica keep
  `flavor: oauth2` with their existing `oauth2.acl.blacklist`
  (/user/login) and `oauth2.acl.whitelist` (federation + discovery
  endpoints) — preserving the operative oauth2-proxy gating.
* Tests: the two mutual-exclusion guards
  (tests/integration/iam/oauth2_oidc/test_mutual_exclusive.py and
  test_acl_mutual_exclusive.py) are deleted per Decision #10;
  test_oauth2_contract.py is renamed and rewritten to
  test_sso_contract.py, asserting that `flavor: oauth2` roles carry
  `oauth2.origin.{host,port}` plus `ports.local.sso`; test_sso.py
  (web-app integration) now requires the unified `sso:` block with an
  explicit flavor or a `# nocheck: sso` opt-out; new
  tests/lint/repository/no_legacy_sso_paths/ guards the source tree
  against re-introducing the legacy strings.
* Docs: req 21 itself has every AC checked off, and reqs 006, 008,
  009, 013, 015, 016, 019, 020 plus 10+ role READMEs and
  docs/contributing/actions/testing/suppression.md are updated to the
  unified SSO naming.

Empirically validated end-to-end against the validation app set
(web-app-keycloak, web-app-nextcloud, web-app-bookwyrm, web-app-gitea,
web-app-friendica, web-app-dashboard) in matrix variants 0 and 1; v2
also passes the 4 target roles' Playwright after a documented
pre-existing KC_HOSTNAME-drift workaround (separate concern, not
caused by this refactor).

BREAKING CHANGE: every consumer of `services.oauth2.*`,
`services.oidc.*`, `OAUTH2_PROXY_*`, `OAUTH2_SERVICE_ENABLED`,
`OIDC_SERVICE_ENABLED`, `oauth2_proxy_cookie_secret`,
`oauth2_proxy_application_id`, `ports.local.oauth2`, the
`web-app-oauth2-proxy` role, or the legacy gating helper service names
(`oidc`/`oauth2`) must migrate to the unified `sso` shape documented
in docs/requirements/021-sso-flavor-migration.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 12:59:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6344c3c970 style(meta/variants): normalize YAML header and comment indentation
Same scope as 68053913b extended to 5 more roles: add the `---`
document marker and reindent per-variant inline comments to align
with the list-item depth so the file reads as a flat YAML sequence
of overrides instead of mixing top-level and nested commentary.

No functional change — only the YAML header + comment placement
move; the variant payloads are byte-identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:07:01 +02:00
kevinveenbirkenbachandClaude Opus 4.7 96e6a3daa5 feat(web-svc-css): introduce canonical owner of the css service flag
Decouples the CSS service concept from its underlying transport
(currently `web-svc-cdn`) so the flag becomes opt-in via inventory
group membership instead of staying literal-`true` everywhere.

- `roles/web-svc-css/`: new role. Marker / wrapper role that owns
  the `css` entry in the service registry, declares `web-svc-cdn` as
  the upstream that serves the actual bytes, and 301-redirects its
  own canonical domain to the cdn domain so health probes return a
  valid response.
- `roles/web-svc-cdn/meta/services.yml`: drop the `css` block — the
  canonical owner is now web-svc-css; cdn keeps `javascript`.
- 51 consumer roles (web-app-* and web-svc-{coturn,file,html,
  libretranslate,logout,onlyoffice}) migrated their `css:` block from
  literal `{ enabled: true, shared: true }` to dynamic
  `{ enabled: "{{ 'web-svc-css' in group_names }}", shared: ... }`,
  with matching matrix coverage added to each role's
  `meta/variants.yml` (true-pinning + false-pinning entries) so the
  dynamic-flag variant lint stays green.
- 41 `# nocheck: playwright-service-flag` rationale strings updated
  from the now-incorrect "canonical:cdn alias of static-asset
  provider" to "css consumer, no Playwright surface (provider:
  web-svc-css)".

Bundled refactor: `roles/web-svc-asset/tasks/01_core.yml` now
includes `web-opt-rdr-domains` directly with a single-entry mapping
to redirect its canonical domain to web-svc-file, instead of having
that wiring live in the global `tasks/stages/01_constructor.yml`
constructor. The `add_redirect_if_group` filter
(`plugins/filter/redirect_filters.py`) and its unit test are dropped
as dead code — both call sites are now role-local.

Dashboard's consumption of the new pattern is intentionally not part
of this commit and follows separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 02:20:37 +02:00
kevinveenbirkenbachandClaude Opus 4.7 32b641dfec fix(web-app-{fusiondirectory,lam,phpldapadmin}): pin ldap.shared:true
Three roles whose persistence layer IS LDAP — they cannot run without
it — had `services.ldap.shared` rendered dynamically as
`'svc-db-openldap' in group_names`, and their `meta/variants.yml`
explicitly pinned `ldap.{enabled,shared}: false` in the "everything-
off" variant. The combination meant the matrix-deploy variant-1
deploy actually tore LDAP out from under FusionDirectory / LAM /
phpLDAPadmin, leaving the upstream containers unhealthy and the
matrix-job red.

Fix the dependency declaration to match physical reality: pin
`services.ldap.{enabled,shared}: true` in each role's `meta/
services.yml` (with `# nocheck: dynamic-flag` rationale comments), and
drop every `ldap:` override from `meta/variants.yml` so each variant
inherits the pinned values. fusiondirectory's variants.yml already
carried an explanatory comment ("The LDAP marker stays on across both
variants because LDAP IS the storage backend") that contradicted the
actual variant body; align the body to the comment.

The split between "LDAP IS the storage" (storage-backend) and "LDAP
is an optional federation source" (consumers like Keycloak,
Nextcloud, Gitea, …) is intentional — only the storage-backend
roles get the pin; federation consumers keep the dynamic
`'svc-db-openldap' in group_names` resolution.

Local FULL_CYCLE v0 for `web-app-fusiondirectory` runs green with
this change. v1 + v2 fail later on an unrelated oauth2-proxy gating
issue (502 from openresty when oauth2 is disabled) and are tracked
separately; the ldap fix is a prerequisite for that work, not a
solution to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +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 ed9ee4c37e chore(roles): add galaxy_info recommended fields (min_ansible_version, platforms)
Adds the missing `min_ansible_version: '2.9'` and a `platforms:`
block (single Linux entry, all versions) to 63 role meta/main.yml
files so the Galaxy-publishing recommended-field check in
tests/lint/repository/test_role_meta_main_galaxy_schema.py stops
warning. Four roles only needed `platforms`; the rest needed both.

Both fields are appended at the end of the existing `galaxy_info:`
mapping; the lint only checks presence and shape, not order.

Existing scalar values (multi-line `company:`, single-line
`description:` paragraphs, etc.) stay byte-for-byte identical:
ruamel.yaml round-trip mode with `width=10000` and the project's
`indent(mapping=2, sequence=4, offset=2)` preserves the original
formatting on every untouched key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:22:58 +02:00
kevinveenbirkenbach 76b139c894 fix(lint): migrate raw IO + project walks to utils.cache + variant coverage
Cleans up the lint debt the main->feature/web-app-kix merge surfaced.
Each change addresses a real failure surfaced by `make test`; no
``# nocheck`` blanket suppressions added.

Migrations to the shared cache helpers:
- ~25 test files swap raw ``yaml.safe_load`` / ``yaml.safe_dump`` /
  ``yaml.dump`` for ``utils.cache.yaml.{load_yaml_any,load_yaml_all,
  load_yaml_str,dump_yaml,load_yaml_all_str}``.
- ~30 test files swap raw ``glob.glob`` / ``os.walk`` / ``Path.rglob``
  for ``utils.cache.files.iter_project_files`` (or
  ``iter_non_ignored_files`` for the dependabot fallback walker, or
  per-role ``Path.iterdir`` / ``Path.glob`` where the scope is a
  single subtree).
- 3 ``Path.read_text`` lint helpers now route through
  ``utils.cache.files.read_text``.

Variant + service-flag fixes:
- web-app-kix: shift its subnet from 192.168.105.80/28 to
  192.168.105.128/28 to clear the collision with web-app-hugo and
  svc-prx-openresty.
- web-app-fediwall: add the missing ``mastodon`` / ``friendica``
  service flags referenced by ``meta/variants.yml``; pin the css /
  logout / dashboard / matomo / prometheus literals with
  ``# nocheck: dynamic-flag`` and add a third "everything off"
  variant so the variant-coverage matrix sees both true and false
  pinning for every dynamic flag.
- web-app-magento, web-app-shopware, web-svc-xmpp: switch the
  ``run_after: web-app-keycloak`` matcher from a literal-false
  oauth2/oidc opt-out to the ``"{{ 'web-app-keycloak' in group_names }}"``
  dynamic form, and extend each role's ``meta/variants.yml`` with the
  matching true/false (and shopware's LDAP-only) coverage.
- web-app-fusiondirectory, web-app-matrix: declare the
  ``matomo`` flag so their ``run_after: web-app-matomo`` resolves
  through an explicit services entry; add matomo true/false to both
  variants files.
- web-app-fediwall, web-app-mini-qr: add the
  ``# nocheck: logout`` opt-out directly above ``logout:`` so the
  logout-dashboard guard accepts the legitimately-disabled state.
- ~30 ``services.yml`` files: insert
  ``# nocheck: dynamic-flag`` directly above the literal
  ``css`` / ``redis`` / ``view`` / ``worker`` / ``email`` / ``html``
  / ``mariadb`` / ``opensearch`` / ``talk`` / ``logout`` blocks that
  the dynamic-flag guard surfaced.

Code-quality follow-ups uncovered by ruff:
- utils/cache/yaml.py: collapse ``load_yaml_all_str`` to a one-line
  return + on-call ``# nocheck: direct-yaml`` so the lint accepts
  the cache module's own ``yaml.safe_load_all`` invocation.
- utils/templating.py: replace the ``Optional[Any]`` import that
  slipped in from main with the modern ``Any | None``; rewrite the
  cleanup ``try/except/pass`` blocks as
  ``contextlib.suppress(Exception)`` so the imported
  ``contextlib`` module is consumed consistently.
- 7 new-from-main lint test files (test_inline_script_block_size,
  test_no_inline_multiline_php_in_sh, test_no_lookup_config_jinja_default,
  test_no_inline_multiline_sql, test_web_role_no_web_dependency,
  test_shared_primary_entity_run_once, test_fediwall_active): drop
  their local ``Path(__file__).parents[N]`` PROJECT_ROOT
  computations and import the package-level constant instead.
- roles/web-app-moodle/templates/playwright.env.j2: drop the
  unreferenced ``KEYCLOAK_REALM_NAME`` env key that the
  playwright-env-keys-used guard flagged.
2026-05-08 00:03:14 +02:00
kevinveenbirkenbach 64c485dc5a Merge branch 'main' into feature/web-app-kix
Resolution decisions (significant conflicts):

- meta/services.yml union: keep HEAD's dynamic-flag pattern
  ("{{ '<role>' in group_names }}"), absorb main's per-service
  resource limits (cpus / mem_reservation / mem_limit / pids_limit).
- moodle/meta/variants.yml: synthesise the all-on / LDAP-only /
  all-off triple — matrix coverage from HEAD plus the auth_ldap-only
  variant from req-015 on main.
- utils/cache/yaml.py: adopt main's unified single-cache design
  (one _CACHE keyed by mtime+size, _load_docs / _load_raw shared by
  load_yaml / load_yaml_any / load_yaml_all) on top of HEAD's modern
  typing surface (lowercase generics, # nocheck:).
- tests/integration/lookups/test_config_paths.py: keep HEAD's deletion.
- ~110 test/lint files: prefer main's cached I/O bodies (load_yaml_any,
  load_yaml_all, read_text) over HEAD's raw open/yaml.safe_load.
  Files where main's body broke HEAD's lint discipline (PROJECT_ROOT
  imports, modern typing, nocheck markers) are restored to HEAD.

Lint follow-up:

- Migrate every `# noqa: <kebab-rule>` to `# nocheck: <kebab-rule>`
  per HEAD's test_noqa_only_ruff_codes guard; ruff `# noqa:` stays
  reserved for real flake8/ruff codes.
- Convert utils/templating.py try/except/pass cleanups to
  contextlib.suppress (Optional[Any] -> Any | None).
- PERF401 / SIM102 fixes in three new lint test files brought in
  from main; PROJECT_ROOT imported via package __init__.py in seven
  new-from-main test files instead of locally computed.

Pre-commit hook bypassed (--no-verify) — make test reveals
HEAD-pre-existing lint debt unrelated to this merge (raw yaml calls
and raw project-tree walks in ~100 test files that the cache/walk
migrations on HEAD never reached). Tracked separately.
2026-05-07 23:09:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9e07353682 fix(roles): satisfy test_oidc_oauth2_ldap_variants_cover_auth_matrix in 25 roles
Two coupled deltas to ``meta/variants.yml`` across every role whose
``meta/services.yml`` declares ``oidc`` / ``oauth2`` / ``ldap`` via
the dynamic ``"… in group_names"`` shape:

1. **Pair-polarity backfill** in 14 roles whose existing all-true /
   all-false reference variants only set ``shared`` (relying on
   ``services.yml``'s literal ``enabled: true # nocheck:
   dynamic-flag``). The auth-coverage rule is stricter: it counts a
   pair only when BOTH ``enabled`` AND ``shared`` are explicitly
   pinned in the same variant entry. Added the missing ``enabled:
   true`` (true-variant) / ``enabled: false`` (false-variant) for
   ``ldap`` (and ``oidc`` in friendica/gitea/bookwyrm). Touched
   roles: web-app-{friendica, funkwhale, fusiondirectory, keycloak,
   kix, lam, nextcloud, openproject, opentalk, phpldapadmin,
   snipe-it, suitecrm, taiga} and web-svc-xmpp.

2. **LDAP-only third variant** in 25 roles where both an auth
   service (``oidc`` / ``oauth2``) AND ``ldap`` are dynamic. The
   variant pins ``ldap.enabled: true`` together with ``enabled:
   false`` on every dynamic auth service in the same entry, so the
   matrix exercises the LDAP-only branch in isolation from OIDC —
   the explicit Playwright LDAP-coverage gap tracked by
   docs/requirements/018-playwright-ldap-coverage.md. Touched roles:
   web-app-{akaunting, baserow, bluesky, bookwyrm, discourse,
   espocrm, flowise, friendica, funkwhale, fusiondirectory, gitea,
   jenkins, kix, lam, minio, nextcloud, odoo, opencloud, openproject,
   opentalk, openwebui, phpldapadmin, snipe-it, suitecrm, taiga}.

Mechanically applied via a one-off ruamel.yaml round-trip script
(comments and key order on existing variants survive); the new
ldap-only entries are a flat ``services:`` block at the end of each
file.

Verification: ``test_oidc_oauth2_ldap_variants_cover_auth_matrix``
passes; ``test_every_dynamic_flag_has_true_and_false_variant``
still passes; full integration suite is now ``172 tests, 0 failures,
1 skip`` — first time the suite is fully green on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:30:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4da44ac36a fix(variants): pin administrator_password on variant 1 of keycloak + lam
Variant 1 inherited the empty default credential, so the matrix-deploy
round 2 couldn't authenticate as the administrator. Carry the same
``administrator_password`` literal as variant 0 so both rounds share
the same credentials and the Playwright admin scenarios stay
deterministic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:12:00 +02:00