Commit Graph
189 Commits
Author SHA1 Message Date
kevinveenbirkenbach eaec310b7a Merge branch 'main' into feature/store-i18n
# Conflicts:
#	roles/web-app-akaunting/TODO.md
#	roles/web-app-friendica/meta/volumes.yml
#	roles/web-app-fusiondirectory/TODO.md
#	roles/web-app-keycloak/TODO.md
#	roles/web-app-lam/meta/volumes.yml
#	roles/web-app-phpmyadmin/meta/volumes.yml
#	roles/web-app-pixelfed/meta/volumes.yml
#	scripts/docker/cache/package.sh
2026-10-22 07:59:04 +02: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
kevinveenbirkenbachandClaude Opus 5 ff734fe728 feat(licensing): declare the deployed software's licence per role
The project licence governs this repository's code and says nothing about the application a role installs, which is where a restriction on commercial use usually sits: a deployment cleared to use Infinito.Nexus commercially is still barred from NLLB-200's CC BY-NC weights.

Each invokable role names its licence in meta/<role>/license.yml with the url it was read from. The rights of a named licence live once in meta/licenses.yml, fifteen fields following the taxonomy choosealicense publishes. AND folds to the strictest combination, OR keeps the most favourable whole licence, and the seven contexts are derived from the rights rather than declared, so a heading cannot contradict the matrix under it.

The generated page renders one matrix per context, a lint refuses a role that declares nothing, and a Playwright spec asserts the page against the deployed docs. 21 of 22 deprecated SPDX ids were resolved against the upstream text; EPSON's remains unread because its AUR page blocks automated access.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-20 23:35:00 +02:00
kevinveenbirkenbach 15930945fe Merge branch 'main' into feature/store-i18n 2026-10-19 01:54:37 +02:00
kevinveenbirkenbachandClaude Opus 5 d3d5090e0f fix(sys-front-inj-design): keep the branding config out of the hashed script
The inline design script carries a CSP sha256 computed at render time. On an
onion request body_filter.lua rewrites the clearnet CDN base inside the whole
head payload, the script's bytes stop matching their hash, and every role
whose script held a CDN URL has its branding refused wherever script-src-elem
has no 'unsafe-inline'. web-svc-logout and web-svc-html are on nearly every
deployment, so this was not a corner case.

The config now rides in the script tag's data-design attribute, which CSP
does not hash and onionize may rewrite freely, and the first statement of the
script reads it back into INFINITO_DESIGN. The executable body is identical on
both vhosts, so one hash authorizes both.

All 35 design scripts now read the config instead of interpolating
design.urls. The 13 that still needed converting keep their render-time
guards, so a role with branding disabled still emits no script at all, exactly
as before. slots left the config object: no script has ever read it.

A new check refuses a branding URL in a script body, so the class cannot
return, and a unit test drives the config through to_json, Jinja's escape and
an HTML parser with a hostile title to prove the attribute round trip.

Not yet proven: that the hash matches on a real onion vhost. This stack runs
without tor and every role deployed here has 'unsafe-inline', so the check
needs a tor-enabled deploy of one of the seven roles that disable logout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-18 23:49:30 +02:00
kevinveenbirkenbachandClaude Opus 5 cb17cd5112 fix(web-app-phpmyadmin): select the privileges tab by its route, not by its label
With the upstream default TabsMode=both, phpMyAdmin renders each tab as an icon whose alt carries the label plus the same label as text, so the accessible name of the link is the label twice and an anchored getByRole name match cannot resolve it. Select the entry by its route instead, which is what the assertion claims to prove and what survives a translated interface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-17 12:59:38 +02:00
evangelostsak 05170355cc feat(web-app-phpmyadmin): let a deployment put its hosts into meshes 2026-10-16 03:40:19 +02:00
kevinveenbirkenbach 043f0cc390 Merge branch 'feature/designer' into feature/store-i18n 2026-10-15 03:45:29 +02:00
kevinveenbirkenbachandClaude Opus 5.5 01515cfa2d feat(web-app-phpmyadmin): replace the theme's named colors and give status buttons their own tokens
The in-house theme still carried pink, skyblue, olive, teal and lime, painted success messages in the warning tint and put on-primary text on success and danger buttons. Those now take danger, info, warning and success tokens with their subtle, border, hover and active steps; the meter drops its gradients and .bg-secondary gets a surface. When a user picks another theme in the chooser, the design script pins the light palette so no dark tokens land on that light theme. The spec asserts that every color of the theme sheet comes from a token, walks the chooser to another theme and back, and checks the collation badges and a success message in both modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 03:02:55 +02:00
kevinveenbirkenbachandClaude Opus 5 33504a3b7b Merge branch 'feature/designer' into feature/store-i18n
Resolved 35 conflicts per the designer x store-i18n checklist: store-i18n stays the single point of truth for credentials and role logic, designer's verified design work is kept.

Credential convergers: store-i18n's realign contract wins where both sides have one (mediawiki realign_admin_password.sh over converge_admin.php, pgadmin realign over administrator.yml, matomo utils/converge_superuser.yml over 09_admin_password.yml); the designer-only ones with no counterpart stay.

READMEs: test_readme.py hard-fails a committed Quick Setup or Credits block, both of which designer's side carries, so those go and only the Further Resources heading rename and the gallery links remain. SuiteCRM takes main's session fix, leaving exactly one SUITECRM_SESSION_CONFIG_HOST and dropping the Symfony cache-rebuild task the entrypoint now covers.

Two defects the rename detection hid: git paired store-i18n's web-app-meta against designer's web-app-sphinx, so sphinx's design slots would have been lost and its require(./test-design) landed in web-app-meta, whose spec has no such file. The sphinx design spec and its require now sit in web-app-docs, the role store-i18n renamed sphinx to.

Designer's eleven new Playwright design specs read env with || "" defaults, which test_unguarded_env_default.py refuses because an empty string matches every substring assertion; 35 bindings now go through requireDotenvValue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 17:26:43 +02:00
kevinveenbirkenbachandClaude Opus 5.5 13128cca67 feat(web-app-phpmyadmin): carry the design in an in-house phpMyAdmin theme
phpMyAdmin themes are directories, so the role ships one named infinito
on top of pmahomme: tasks/main.yml renders templates/theme/ (theme.json,
theme.css, theme.rtl.css, jquery-ui.css and design.css, which maps
page, navigation, results, cards, primary action and the highlighted
SQL onto the design tokens and carries the logo) when design is on, meta/volumes.yml mounts
the directory read-only and templates/compose.yml.j2 wires the role's
volumes. templates/config.user.inc.php.j2, handed over as
PMA_USER_CONFIG_BASE64, makes the theme the default and appends the
configured title to every window title; templates/design.js.j2 points
the favicon at the generated one. The emptied role stylesheet templates/style.css.j2
goes, the theme replaces it.

files/playwright/test-design.js checks that the theme is active, the
status colors of highlighted SQL in both modes, tokens, page,
navigation, results, cards and primary action, the focus stops of the
breadcrumb bar, logo, favicon and title, and captures the gallery.
README.md links the gallery; TODO.md records the warning about the
missing configuration storage.

Verified green on the compose stack (9 passed); only README.md changed
after that run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:28:51 +02:00
kevinveenbirkenbachandClaude Opus 5 19dee8cf72 test(roles): make the administrator persona prove it is an administrator
`personas/utils/interaction.js` states that every persona scenario must
drive a real role-specific interaction and that the module "deliberately
refuses to provide" a generic fallback, because a generic "click any visible
link" assertion tests nothing role-specific. All 47 roles carrying an
`adminInteraction` had reimplemented exactly that fallback: a locator whose
alternation included surfaces any signed-in user reaches, a guard that
skipped the whole body when nothing matched, a click whose failure was
swallowed, and a body assertion whose alternation included the bare product
name, which also matches that product's own 404 page. A guest-level account
passed every one of them.

Six roles are fixed here. The guard gives way to a strict `toBeVisible` on a
link only an administrator is served, the click is allowed to throw, and the
locator keeps only admin-only entries: `projects|groups` leave GitLab's,
`campaigns|subscribers|lists` leave Listmonk's, `databases|sql|export` leave
phpMyAdmin's.

The second assertion no longer reads page copy. Each one now checks the URL
the click reached, which is locale-independent and survives the heading
renames that upstream ships between minors. That splits the two questions
cleanly: the visible link proves the rights, the URL proves the navigation.
Listmonk asserts only that it stayed inside `/admin/`, because naming a
specific subpage there would trade one guess for another.

Two roles needed more than the uniform shape. EspoCRM keeps its
Administration entry in a Bootstrap dropdown at `display:none`, which
`getByRole` skips, so the callback opens `#nav-menu-dropdown` first;
without it a strict assertion fails for a reason unrelated to admin rights.
Mastodon renders the entry with `target="_target"`, so the dashboard opens
in a second tab and the old assertion was passing against the untouched
timeline; the callback now captures that tab and asserts there.

The other 41 roles are left alone on purpose. `runAdminFlow` returns at its
first two guards, so a callback behind `PERSONA_ADMINISTRATOR_BLOCKED=true`
or a template without `APP_BASE_URL` never executes: 36 roles block the
persona unconditionally. Writing a strict assertion into code nothing runs
is worse than the vacuous one it replaces.

That survey turned up three roles where the persona signs in without being
an administrator, which is a deployment question rather than a test one:
mobilizon provisions no admin yet runs the callback in its sso variants,
port-ui has no admin tier at all, and GitLab CE cannot map an OIDC group to
admin. GitLab's sharpened callback is held back until that is settled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 01:53:55 +02:00
kevinveenbirkenbachandClaude Opus 5 3c72c2002a refactor(credentials)!: give applications their own administrator account
Application roles used the host administrator account.
user-administrator/tasks/00_core.yml:17 hashes its password into the OS
user, files/administrator grants that name ALL=(ALL) ALL without
NOPASSWD, and rotate.py lists it in ROTATION_EXEMPT, so the value is
password-authenticated sudo that cannot be rotated to recover. Every role
that typed it into a web form was typing the host sudo credential, and the
staged Playwright env carrying it is written mode 0644.

roles/user-administrator/meta/users.yml now declares platform_administrator
beside administrator: no host account, no rotation exemption, mailbox and
identity so it stays SSO-capable and can receive the mail applications send
to their admin. Every use moves to it, all fields and not only the
password: leaving .email behind built accounts from both identities at
once, with the host operator's address under the platform account's login
name, and an IdP matching on e-mail resolves such an account back to the
host operator.

The account is reachable by two spellings, and both are migrated:
lookup('users', 'administrator') in its two- and three-argument forms, and
the dotted lookup('config', application_id, 'users.administrator.email')
that five roles used. administrator is now referenced nowhere outside
roles/user-administrator.

Four lints hold the boundary:

  - no role outside user-administrator may use the host administrator, in
    either spelling or as a *user_key literal
  - a role that looks a user up must declare it in its own meta/users.yml,
    because required_user_policies only pins passwords for users declared
    by a role resolved into the inventory, and those are the invokable
    roles; a user declared only by a non-invokable role keeps the
    {{ 42 | strong_password }} fallback, which re-renders per evaluation
  - a role that iterates a user or passes it as a *user_key must declare it
  - the biber and mapache test personas may be read only from the
    Playwright and CLI test surfaces

Two credentials that were derived by hashing the admin password get their
own declaration, because deriving them tied an unrelated rotation to a
password change: web-app-fider jwt_secret, which signs session and e-mail
sign-in tokens, and web-app-nextcloud integration_suitecrm_client_secret,
whose two ends must be re-registered in lockstep.

web-app-keycloak separates the two admins it has: a new meta/users.yml
declares keycloak_bootstrap for the temporary account the container creates
on first start, while the permanent account is platform_administrator. Both
resolve in vars/main.yml so the lookup runs once per role.

web-app-semaphore's break-glass admin no longer shares ansible_become_password
and no longer forwards its mail to the host administrator; tasks/02_breakglass.yml
realigns the password on every deploy, so it converges on its own.

The six roles whose meta/secrets.yml declared administrator_password as its
only credential lose the file.

tests/integration/ansible/lookups/test_declared_users_resolve.py runs the
lookup through the real plugin loader and fails on a declared user that
cannot be resolved or whose username, e-mail or password renders empty. The
two declarations reading ansible_become_password resolve to an empty string
when that variable is absent, and nothing raises on it.

Existing deployments keep the admin account their application database
already holds, under the old name. Roles that re-apply the credential every
run converge; 30 roles feed it from config with no provisioning task and do
not, so those accounts need a reset. web-app-friendica additionally guards
on "'administrator' not in stdout", which the substring platform-administrator
satisfies, and web-app-n8n initialises an owner that already exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-08 16:28:52 +02:00
kevinveenbirkenbachandClaude Opus 5 7bc12e55f3 fix(docs): drop the generated sections the designer merge put back
aceb5295ef moved Cosmos, Quick Setup and Credits out of the committed role
READMEs and into the documentation build's own scratch checkout, so a
committed copy can no longer go stale when the metadata moves. Merging
feature/designer (ebad39422c) took origin/main's side of those files, which
still carries them, and reinstated all 290 along with the cosmos make target
and its i8cos alias. tests/lint/ansible/roles/test_readme.py hard-fails on a
committed copy, and the reinstated Credits block credited one person with two
URLs, which tripped the credits-attribution lint as well.

Stripped with the repository's own fence-aware parser against
MANAGED_SECTIONS, not with a line filter: the Cosmos block holds a mermaid
fence a naive filter would cut through. The result is byte-identical to
aceb5295ef for every README that carries no prose change since.

The cosmos target wrote the forbidden section straight back into the
checkout, so it goes with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 01:50:43 +02:00
kevinveenbirkenbach ebad39422c Merge branch 'feature/designer' into feature/store-i18n 2026-10-06 00:56:17 +02:00
kevinveenbirkenbachandClaude Opus 5.5 86cefeff26 feat(web-svc-design): derive one corporate design from a single base color
web-svc-css becomes web-svc-design and owns the configuration that lived in group_vars/all/13_design.yml. Consumers switch their css service key to design, and sys-front-inj-css becomes sys-front-inj-design. javascript stays an independent key; a role's optional design.js.j2 is injected together with the design and CSP-hashed like the functional script.

An in-repo OKLCH lookup replaces colorscheme_generator. From design.colors.base alone it derives surfaces, text, borders, the primary and status fills with their on-colors, hover and active states and a dedicated dark palette; a unit test proves WCAG AA, and AAA for body text, across six base colors in both modes. Semantic --design-* tokens replace --color-NN-SS in every role stylesheet. The shared element styles sit in the cascade layer infinito-design, so an app's own design system always wins and takes the palette through a variable mapping in its role stylesheet or through its own theming mechanism.

design.logo and design.title resolve globally from web-svc-design or per role from its design entry, and false or 0 disables the replacement. The design_assets action renders the logo lockup per slot as SVG, PNG and ICO, and the legacy title keys move into the design entries.

Three roles carry the design end to end. The dashboard takes logo, favicon and title from the generator and replaces the hard-coded port-ui colors. Gitea maps its theme variables onto the tokens and mounts the generated logo and a branded start page. Nextcloud receives primary color, logo, name and slogan through occ theming and ships no stylesheet.

make design-sync re-renders the static design files of a deployed app, make design-gallery captures before and after screenshots in light, dark, desktop and mobile, and make design-queue orders the roles that are due for a design pass. The i8-designer skill and docs/agents/action/design.md describe that loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 06:04:14 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d3ca143343 docs(roles): regenerate READMEs for Stalwart as the mail provider
MAIL_PROVIDER moved to web-app-stalwart without a make docs run, so every generated README still drew Mailu as the email dependency. This is the generator output for the current tree: the root role table and each role cosmos now name Stalwart, Mailu loses its dependents, and Stalwart gains its swarm marker, the Tor dependencies and the 0..0 edges.

The hand-written cosmos of svc-net-tor-smtp gives way to the generated one, which no longer shows the SOCKS5 hop through svc-net-tor because the role does not declare that dependency.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 19:57:30 +02:00
kevinveenbirkenbachandClaude Opus 5 48b119a381 test(playwright): refuse an env value defaulted to "" as a match argument
`"".endsWith(x)`, `x.includes("")` and `x.startsWith("")` are true for every
string. A spec that binds

    const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");

and later asserts `url.includes(canonicalDomain)` therefore stops testing
anything the moment the deploy does not write that key: the assertion passes,
the job is green, and the missing value never surfaces.

The repository already refuses the list-shaped half of this. `decodeDotenvJsonList`
throws rather than return `[]` because an empty list "would pass every assertion
that iterates it". `requireDotenvValue` is the scalar counterpart and throws
while the module loads.

43 bindings across 43 files fed a matcher this way, in exactly two shapes:
`decodeDotenvQuotedValue(ENV || "")` forty times and `normalizeBaseUrl(ENV || "")`
three times. The wrapper is kept where it does work, so the URL normalisation
survives and only the core is replaced. 38 `decodeDotenvQuotedValue` imports fell
out of use and are removed; eslint reporting no unused binding is what confirms
that.

`|| ""` itself is not the defect and is not banned: it appears 400 times, and
most of those values are compared with `===`, where empty simply is not equal,
or flow into `normalizeBaseUrl`, which tolerates it. The lint pairs the empty
default with the matcher and reports only that combination, which is why the set
is 43 and not 400.

xwiki carries the instance this started from. Its OIDC spec asserted that the
IdP host ends with the last two labels of the XWiki host, so under a three-label
domain it checked `infinito.test` and would have accepted any foreign host in
the parent zone. It now checks `DOMAIN_PRIMARY`, exported for the purpose.

Verified by running it: node --check on all 48 files, eslint 0 errors,
`make lint-playwright` parses 89 specs. That last one is the load-bearing
evidence, because `requireDotenvValue` throws at module load and ce1a19d3c2 hit
exactly that wall on seaweedfs this morning; it holds here because the lint
harness stubs an unmatched key as the non-empty string "stub". The rule was also
watched failing: restoring one binding reports
`test-baseline.js:21: .includes(canonicalDomain)`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 22:14:50 +02:00
kevinveenbirkenbachandClaude Opus 5 d1b944a230 fix(web-app-phpmyadmin): take the official image, which carries arm64
The role pulled `phpmyadmin/phpmyadmin`, the organisation repository, whose
5.2.3 tag is a single-platform amd64 manifest. The Docker official image of
the same project and the same version is an index over seven platforms:

    386  amd64  arm  arm64  ppc64le  riscv64  s390x

So this role needs no `architectures` declaration at all; it needs the
repository that publishes arm64. 5.2.3 is also the newest tag, so nothing is
skipped: `latest` and `5.2.3` point at the same release.

An unqualified image name is the repository's normal form - memcached,
mariadb, redis, rabbitmq, debian and python are all pinned this way - and
`plugins/lookup/image.py` composes the reference as `image:version` without
assuming a namespace.

Worth stating because it is not a no-op: the two amd64 images have different
config digests (2c568793e85a against e66b1f5a8c58), so an existing
installation changes image content on its next deploy, not only gains an
architecture. No digest pin accompanies this, because the repository pins
images by version tag throughout and no meta/services.yml declares a digest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 01:54:00 +02:00
kevinveenbirkenbachandClaude Opus 5 aceb5295ef feat(docs): let the documentation build generate what make docs generated
Cosmos, Quick Setup and Credits follow from meta/services.yml, categories.yml
and galaxy_info.author, yet 279 role READMEs carried a copy that went stale
whenever the metadata moved and nobody re-ran make docs. The documentation
build now runs the generators itself, against its own scratch checkout, and
the committed READMEs keep only the prose a person writes.

utils.cache.PROJECT_ROOT derives from the module's location, so putting the
checkout first on PYTHONPATH makes the generator operate on the scratch tree.
i18n.py and builder.py add it; the one remaining coupling, a relative_to over
PROJECT_ROOT in a log line, is gone.

CI extracted the "### Production" block from the committed README with awk.
Stripping the section made guide_deployable() return "" for every role, which
selects no deploy row and turns nothing red: a fail-open regression. It now
asks _is_role_invokable, which matches the old condition exactly (178 roles
either way, both difference sets empty), and cli/build/docs/readme/production.py
renders the block for the two guide scripts. All 178 renders are byte-identical
to what HEAD served, and both scripts' extraction pipelines produce identical
output for a web app, a service and a desktop role.

test_readme.py inverts: the three sections are now a hard failure in a
committed README. readme_md.md documents what each is derived from.

Removed: the docs, cosmos, readme-generate, readme-index and readme-check
targets, the five aliases pointing at them, and docs from make quality.

Verified by running generate_commands() in series against a copy of the
checkout: every command exits 0, no README is left without Cosmos or Credits,
and exactly the 178 invokable roles get a Quick Setup. The ansible_roles step
needs pandoc, which only the docs image carries, so that one link of the chain
is unexercised here; it is unchanged by this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 01:09:22 +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 4986e80dd4 feat(readme): ask the reader for a domain the deployment can resolve
The Production block never set one. inventory provision writes only
ansible_connection for a local host, so DOMAIN_PRIMARY stayed at the
group_vars default of infinito.localhost and the deployment served a zone its
own resolver knows nothing about. CoreDNS templates answers for
INFINITO_DOMAIN_RE and nothing else, so every lookup of the deployment's own
name came back NXDOMAIN.

Mastodon is where that surfaced. tootctl accounts create validates the address
through EmailMxValidator, which resolves MX and then, for the domain itself,
A and AAAA. All six queries returned NXDOMAIN in run 33574056480, the
validator recorded email :unreachable, and the admin account was never created.
Only the guide rows were affected: the matrix rows write
DOMAIN_PRIMARY: "{{ lookup('env', 'INFINITO_DOMAIN') }}" into their host_vars
and resolve fine.

The template now carries DOMAIN=<your-domain> in both the host and the
container form and threads it into --vars, with the container form passing it
through the docker run boundary as well. The two guide runners substitute it
the way they already substitute the SSH key, so the replay deploys under
INFINITO_DOMAIN like every other CI deploy. compose_deploy.sh moves its env
load ahead of the substitutions to have the value; host_deploy.sh had no env
load at all.

A production reader is asked for a domain now instead of silently inheriting
infinito.localhost, which no real deployment wants either. The shipped default
keeps its value: sys-svc-nss documents that *.localhost resolves through the
myhostname NSS module without any zone, and host-side tasks lean on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 18:30:25 +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 acc9e25c0f refactor(volumes): one canonical mount surface, context-wired lookups
The mount pipeline was split across two layers: compose_volumes and
container_volumes lived as filter modules that ansible's filter loader
rejected on every start ("has no attribute 'FilterModule'", two warnings
per run), while the same-named lookups wrapped them thinly. The filter
bodies now live inside their lookups (the tor_socks precedent), the
filter files are gone, and the render tests moved next to the lookup
tests.

As lookups they see the templating context, so the application_id term
every call site passed back to its own role is gone too: both lookups
read application_id from context and fail loudly when it is missing.
107 templates and nfs_prep.yml drop the term; the calls collapse to
lookup('compose_volumes') and lookup('container_volumes', service_name).

The container_volumes call is now canonical - four leading spaces,
service_name in the service position, | indent(4) - enforced by a new
lint (test_container_volumes_call_shape). Normalizing the 85 literal
call sites surfaced that erpnext keyed its meta/volumes.yml mounts by
container name instead of compose service key, the only role to do so;
its meta now uses the service keys. wordpress's column-0 call, which
rendered its first line at top level, is indented like every other.

Template logic that only restated meta/volumes.yml is deleted: mailu fed
compose_volumes an extra_volumes dict whose eight names were looked up
from the very meta file compose_volumes reads itself; zammad's init and
railsserver extra mounts were the volumes.yml entries minus the two
services, which the meta now lists (the when-gated OIDC binds included).
Conditionality that meta cannot express stays in code, but in role-local
lookup plugins instead of template loops: matrix_bridge_mounts renders
the per-bridge registration mounts, nextcloud_extra_volumes the
feature-gated whiteboard/recording volumes (type: volume entries are
declared unconditionally by compose_volumes, so gating must happen
before it).

Parity is proven, not assumed: every mechanically rewritten template is
byte-identical to HEAD after the transformation regexes (95/95), and the
semantic roles render identically through the real functions against
HEAD's meta - zammad across six sso/tls polarities, all seven erpnext
services, mailu's top level. make test-lint passes complete.

Also: suitecrm's compose template pins container_extra_hosts with
host_alias (the SOCKS proxy URL host.docker.internal never resolved in
compose+tor), and the five disabled mautrix bridge addons use the
empty-default config lookup so templating the applications dict stops
emitting 125 errors per batch in every CI job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:28:16 +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 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
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 642055a20a style(e2e): drop the timeout argument isVisible discards
Playwright declares the timeout option on locator.isVisible() and isHidden() as deprecated and ignored: both return immediately and never wait. Passing resolveTimeout(10_000) to one therefore changes nothing at runtime and only tells the reader a wait happened that never did. That reading is what let 7538b8a0b conclude two genuine call sites were fine, and what hid the false negative b97cf8888 had to fix.

Sixty call sites across fifty-eight role-local specs lose the argument and nothing else. Every added line in this commit is a bare isVisible(); a grep for added lines that are not one returns zero, so a behaviour change cannot have entered here. Files whose diff also alters waiting semantics are deliberately not part of this commit.

A repository lint added in b97cf8888 keeps the construct out. It matches isVisible and isHidden only, because isChecked, isDisabled, isEditable and isEnabled take a real timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:49:18 +02:00
kevinveenbirkenbachandClaude Opus 5 f713ba8350 refactor(credentials): address them at secrets.credentials, like every other meta file
meta/schema.yml became meta/secrets.yml, but its content still landed at
applications.<app>.credentials — the one meta file whose payload did not sit
under a root named after the file. Every other topic obeys that rule:
meta/volumes.yml goes to volumes, meta/rbac.yml to rbac. Credentials now go to
secrets.credentials.

The path exists exactly once, in utils/manager/credential_key.py, and
OVERRIDE_SECTION is derived from its two segments rather than spelled beside
them, so the dotted form used by --set and the nested form used by the
inventory cannot drift. Six consumers read it: the loader, the generator, the
CLI override parser, the two emit helpers, and the four lookup validators.

--set now accepts exactly one key shape,
applications.<app>.secrets.credentials.<key>. The three short forms are
rejected instead of guessed at: the same short key names different credentials
on different apps. That also retired primary_app_id, which only existed to
disambiguate them.

Three defects surfaced while wiring this up. _apply_one_role_schema still wrote
to the old root, so an app's own credentials would have moved while its
providers' stayed behind. web-app-pgadmin reads through
lookup('applications', ...).credentials.<key>, an access path no regex over
lookup('config', ...) could see, and would have deployed with an empty postgres
password. And _META_NON_GENERIC_TOPICS still listed 'schema' — a bare topic
name, invisible to any path search — so the generic file-root loop assigned the
raw schema under secrets and the follow-up write landed inside the cached
document, truncating keycloak's four credentials to one for every later
reader.

tests/integration/ansible/lookups/config/test_credentials_roundtrip.py pins both
directions of the seam and the cache invariant that last defect broke.

Existing inventories still carry the pre-rename root; migrating them is
deliberately out of scope here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:51:56 +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 b1d9b56d42 fix(playwright): block the personas the sso-off variants cannot serve, and stop three claims that were false
The 21 env templates added two commits ago left the persona flags off entirely
in most roles. biber.js has no native-login branch at all -- with
SSO_SERVICE_ENABLED false it never authenticates and then hard-fails at :201
rather than skipping -- and admin.js gates its native fallback on !oidcEnabled.
CI deploys every matrix variant, and variant 1 of these roles pins
services.sso.enabled false, so each of them was a guaranteed red job that had
never been observed because the specs had never run.

Nine flags are static true: chess, confluence and jira pin sso.enabled false in
meta/services.yml with no variant override and provision no native admin, so
there is no login surface in any variant. Eleven follow services.sso.enabled in
the ternary web-app-baserow already uses, for the roles that do have an
SSO-enabled variant where the personas should run. pgadmin and phpldapadmin
lose the credential keys that a permanently blocked persona can never read.

Eleven nocheck rationales cited "the sys-svc-mail integration tests" as the
coverage that justifies not gating on EMAIL_SERVICE_ENABLED. No such tests
exist: roles/sys-svc-mail* contain no tests at all, and
tests/integration/infrastructure/services/test_mailu_dependency.py is a static
YAML parity check. The real delivery coverage is web-app-mailu's own spec,
playwright.spec.js:154. Each is now a fact checkable inside its own role.

Eight more claimed the CSS injector's origin is asserted by the shared CSP
baseline. personas/utils/csp.js:96 bails with `if (!host) continue`, and
injectorBaseUrl("css") reads CSS_BASE_URL, which no consumer role renders --
so the assertion is a guaranteed no-op everywhere. Rewritten to the
mechanically true reason: assertCspInjections resolves the injector through a
computed loop variable, never a literal gate argument, so test_spec_env_gates'
literal-only regex cannot see it.

The CSP hole itself is left standing: rendering CSS_BASE_URL would arm the
injection assertion for every consumer role at once, which is a change of a
different size than correcting a comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:41:19 +02:00
kevinveenbirkenbachandClaude Opus 5 92faba5d2d feat(playwright): make the env template mandatory so specs actually run
test_has_spec.py forces every roles/web-app-* to ship a Playwright spec and
calls a missing one a hard error, because the persona contract must never be
silently absent from the deploy capstone. Nothing forced the other half. The
runner discovers roles by rglob('templates/playwright.env.j2')
(discover_playwright_roles.py:53), so a spec without that file is staged
nowhere and executed never.

21 of 76 web-app roles were in exactly that state. Verified rather than
inferred: running the discovery filter directly returns 61 roles and none of
the 21 is among them, `make compose-playwright` refuses without a rendered
.env from a prior deploy, and `git log --all` on those paths shows the file
never existed on any branch -- the single hit is web-app-gitlab on a fork
branch. Their specs have never run once. 15 of the 21 deploy in CI today,
reporting green while their only functional verification never fires.

The new lint mirrors test_has_spec and carries a per-role opt-out that is
used nowhere: uniform beats exceptions.

Every one of the 21 now has a template written against its own
meta/services.yml rather than a shared skeleton -- espocrm splits
ADMIN_PASSWORD from ADMIN_NATIVE_PASSWORD because OIDC_ALLOW_ADMIN_USER
leaves both login paths open, the four permanently auth-less roles declare
their personas blocked instead of shipping a knowingly-red spec, and
web-app-mastodon gained a real test-seaweedfs.js because no honest
suppression existed for a role that does route Paperclip to S3.

web-app-magento moves pre-alpha -> alpha here: it now has a spec that runs,
which is what the lifecycle gate is about.

Six meta/services.yml files change only where adding a template falsified an
existing suppression -- gitlab and shopware both claimed the role 'ships no
templates/playwright.env.j2', which stopped being true in this commit.

Not resolved here, and the adversarial review says so plainly: the specs are
enabled, not proven. None has executed against a live stack. The review found
three classes of defect that land in follow-up commits rather than being
smuggled in silently -- persona blocks that are missing or should be tied to
sso.enabled, eleven suppressions naming a sys-svc-mail test suite that does
not exist, and eight claiming a CSP assertion that cannot fire because no
consumer role renders CSS_BASE_URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:17:55 +02:00
kevinveenbirkenbachandClaude Opus 5 946b3769cb feat(svc-net-tor): let CI deploy the role in compose and swarm
modes.compose.enabled and modes.swarm.enabled were both false, and a
stack role is forced host=false, so all three test_<mode> columns were
false and the CI discovery filter (test_<mode> == true) dropped the role
from every matrix. It was resolved into the priority whitelist as a new
role and then silently filtered out again — svc-net-tor was only ever
exercised indirectly, as a dependency other roles pull in.

The flag is read solely by the discovery model and the README generator;
nothing in the deploy path consults it, so this has no runtime effect.

meta/categories.yml pins the whole net category to compose/swarm false,
which fits svc-net-wireguard-{core,plain,firewalled}: none of them ships
a compose template. svc-net-tor does, and is the only role in the
category that does, so it takes the category-modes exemption the lint
provides rather than bending the category default for three host roles.

Regenerated READMEs follow: svc-net-tor now carries its mode symbols in
every cosmos diagram that names it as a dependency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:02:24 +02:00
kevinveenbirkenbach 342de43111 Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
# Conflicts:
#	roles/svc-ai-ollama/tasks/01_manager_ops.yml
#	roles/web-app-decidim/templates/compose.yml.j2
#	roles/web-app-joomla/templates/compose.yml.j2
#	roles/web-app-matrix/files/flavor/ansible/runner/bootstrap.service
#	roles/web-app-matrix/templates/flavor/compose/services.yml.j2
#	roles/web-app-nextcloud/files/playwright/_shared.js
#	roles/web-svc-cdn/tasks/01_assets.yml
#	scripts/tests/deploy/swarm/utils/trigger_units.sh
#	scripts/tests/deploy/utils/filesystem/docker_dataroot.sh
2026-08-05 02:32:58 +02:00
kevinveenbirkenbachandClaude Opus 5 e3e81e816f refactor(healthcheck): declare probes in services.yml, render them from one lookup
Seven include templates under sys-svc-container decided how a container was
probed, and they read their inputs from template-local `{% set %}` scope.
That scope leaks: a value set for one service silently reached every service
below it, which is how taiga's front and gateway ended up probing a port
declared for a different service, and how bluesky's frontend probed a PDS
endpoint.

Probes now live in utils/docker/healthcheck.py, one class per flavor behind a
PROBES registry, and each service declares its own:

    healthcheck:
      flavor: curl
      path: health/ready
      start_period: 20m

`port_key` picks another entry under ports.internal for the services that
expose their probe elsewhere (keycloak on management, nextcloud talk on
signaling). Without a flavor the entry must carry an explicit test argv, so a
missing declaration fails loudly instead of rendering a probe against nothing.

Rendering HEAD's templates against the new output confirms 49 of 56 call
sites are byte-identical; the remaining seven are covered by an Ansible run.
Three probes change on purpose: erpnext's backend gains a check it never had,
bluesky's frontend drops the leaked PDS path, and msmtp_curl moves from a
hardcoded localhost:80 to the service's own port on 127.0.0.1 like every
other flavor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 04:05:38 +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 62a46e472f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Resolves the runner Dockerfile to the lint-standard Acquire::Retries=3
and keeps the collabora TODO wording; hardens the merged tree for the
stricter lints (apt retries in the tor Dockerfile, shell-or-true
markers, comment cleanup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:27:25 +02:00
kevinveenbirkenbachandClaude Fable 5 3125c5ac03 docs: regenerate role catalogs with the tor onion wiring
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:49:50 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ea9ebe0de3 refactor(inventory): rename prod inventory to production
Standardize the private inventory dir on the fully spelled "production" (matching inventories/development). Update the role README template (SPOT) and regenerate 153 role READMEs plus the admin deploy guide; add inventories/production/ to .gitignore, propagated to .dockerignore via make fix-dockerignore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 19:08:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9ad8fa2971 feat(guide): default self_signed and test the whole README block
The guide deploys to a .localhost target, where letsencrypt cannot issue (Cloudflare DNS-01 needs a real domain). Default TLS_MODE=self_signed in the generated Production block so the guide uses the working self-signed provider. Document every deploy parameter as a top-level shell var (APP, HOST, TLS_MODE, SSH_PUBLIC_KEY) instead of an inline placeholder.

The compose guide now extracts and runs the whole README block, sourcing params from it (no more -e injection); HOST is forced to localhost. Skip checkout/login under ACT so the guide workflow is iterable locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:23:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 972596a758 fix(guide): document the administrator SSH key step and seed it in CI via --vars
The README Production block went provision -> deploy with no admin-key step, so user-administrator failed early (authorized_keys empty) for both the Guide CI jobs and any real user copying the README. Add a visible 'provision --vars' step that sets users.administrator.authorized_keys (merged into host_vars) in both Production variants of the role README template, regenerate all role READMEs, and replace the invisible CI payload splice with a visible substitution of the documented <your-ssh-public-key> placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 05:45:26 +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 Opus 4.8 4c247b61ec test(e2e): give every standalone Playwright request an onion-aware timeout
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 04:45:03 +02:00
kevinveenbirkenbachandClaude Fable 5 17cd53609a feat(readme): merge the Production deploy into one docker command
The role README Production section now runs provision and deploy in a
single docker call: APP and HOST are shell variables passed via -e,
INVENTORY is the in-container SPOT for the duplicated paths, and the
redundant --vars-file (optional, default None) and --id (the inventory
is already narrowed by --include) flags are gone. All 153 role READMEs
regenerated from the template; test-run.yml executes exactly this block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:03:25 +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