Commit Graph
331 Commits
Author SHA1 Message Date
kevinveenbirkenbach db677f81ac Merge branch 'main' into feature/store-i18n
# Conflicts:
#	roles/web-app-pixelfed/meta/services.yml
2026-10-21 20:10:55 +02:00
infinito-nexus-updater[bot] def84701c6 update(images): update Docker image versions (web-app-gitlab) 2026-10-21 15:29:24 +00: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 faadf5b850 refactor(design): compose the per-app sheet and script from one SPOT
sys-front-inj-design now renders templates/css/app.css.j2 ahead of the
role's stylesheet and templates/js/design.js.j2 ahead of its script. The
branding URLs, the slot dimensions and the logo guard live there instead of
being repeated per role: the guard that stood in 41 sheets is one condition
in the composing template, and a sheet reads branding through
--design-url-* / --design-slot-* custom properties.

29 stylesheets and 43 design scripts lose their Jinja and move to static
files. Roles whose sheet carries app-specific URLs declare them as
design_css_urls and read them as --design-url-app-*.

The script prelude is prepended only when the role script references it,
and it carries urls only while a logo is configured, so every role that
needs neither is emitted byte for byte as before.

Five checks keep the mechanism honest: a composed sheet may only read
design custom properties that app.css.j2 declares for its role, a template
may not carry Jinja expression text outside its delimiters, a design script
may not quote its config object, and the injected-script parse check now
covers the design channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-18 22:36:32 +02:00
kevinveenbirkenbach 923c629838 Merge branch 'feature/designer' into feature/store-i18n 2026-10-17 15:12:44 +02:00
kevinveenbirkenbachandClaude Opus 5.5 bac3308f75 test(web-app-gitlab): assert the palette on the admin dashboard and users list
The design spec reached the admin area only to prove root can sign in
there; nothing checked that the admin pages take the palette. The new
test opens /admin as root and asserts in light and dark mode that the
frame, the panel and the dashboard cards take their surface tokens, the
card titles --design-text and the card dividers --design-border, then
checks the cards against light and dark and the headings, titles, links
and dividers for contrast. On /admin/users the New user action must
take --design-primary on --design-on-primary and stay readable.

The selectors follow the CNG v19.5.0 markup (.admin-dashboard,
.crud-body, data-testid crud-title), so a GitLab bump that renames them
fails loudly instead of passing on nothing.

Verified on the designer stack: deploy spec 12 passed 5 skipped with the
new test green; with the role stylesheet aborted through page.route it
fails on the admin chrome; closing sync 12 passed 4 skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 09:49:50 +02:00
kevinveenbirkenbachandClaude Opus 5 1fd5d27ce9 fix(playwright): poll the two conditions that were sampled once
Both specs read a value that only settles asynchronously and asserted on the
first sample, so they failed whenever the state had not arrived yet.

web-app-espocrm navigated on with `gotoOnion` as soon as the OIDC popup chain
returned. The opener leaves before the callback has written the session, and
the next page load lands unauthenticated. It now polls `localStorage` for the
key ending in `user-auth` before navigating.

web-app-gitlab read `image.naturalWidth` directly after the locator resolved.
That property is 0 while the image is still decoding, and unlike the
comparable assertion in web-app-joomla this one had no preceding
`toHaveJSProperty("complete", true)` to establish the precondition. Both logo
checks now poll `image.complete && image.naturalWidth > 0`, which folds that
precondition into the predicate instead of asserting a proxy value.

Polling on this property is the pattern the repository already uses in
web-app-magento, web-app-snipe-it, web-app-opencloud, web-app-peertube and
web-app-bookwyrm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-16 23:40:20 +02:00
kevinveenbirkenbachandClaude Opus 5 16dd0e2333 feat(swarm): pin a single service without pinning its role
A role-level `placement: manager` pins every service of the role and keeps
every volume node-local, so a role needing one node-bound service had to give
up NFS for all of them. GitLab's gitaly is that case: its repositories volume
is node-local (`nfs: false`, gitaly does not support NFS) and a reschedule
left the data behind on the old node.

`deploy.yml.j2` now resolves `services.<service>.placement` first and keeps the
role-wide list as the fallback, so gitaly carries the constraint alone while
webservice, sidekiq, workhorse, shell, migrations and gitlabmcp stay
schedulable and `gitlab_shared`, `gitlab_uploads` and `gitlab_builds` keep
their NFS backing. Role placement still comes from the primary entry, which is
what drives that backing.

Two things had to come with it. The swarm drill decided that a role is
manager-pinned by grepping the whole `meta/services.yml`, so a per-service pin
would have skipped GitLab's drain and reschedule steps while six of its
services still run on workers; it now reads the primary entry. And
`resource_filter` returns a present-but-null value rather than the hard
default, which rendered `node.role == None`, a constraint no node satisfies.

`test_no_placement_manager.py` now inspects every `placement` value instead of
only `manager`, requires the justification for each, and rejects a value that
is not a swarm node role, since a misspelled one hangs the converge wait
rather than failing the deploy. Requirement 023 records the shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-16 23:29:50 +02:00
evangelostsak c35a94fc48 feat(web-app-gitlab): let a deployment put its hosts into meshes 2026-10-16 03:40:18 +02:00
kevinveenbirkenbachandClaude Opus 5 5b69b5c7c6 Merge branch 'feature/designer' into feature/store-i18n
Six conflicts, all from git pairing store-i18n's renamed roles with their designer originals.

web-app-meta/meta/services.yml and web-app-sphinx/vars/main.yml: store-i18n renamed web-app-sphinx to web-app-meta and dropped its custom build, so the role keeps the prebuilt ghcr.io/infinito-nexus/meta image and the old sphinx role stays deleted.

svc-ai-tika/templates/compose.yml.j2: a bogus rename pairing dropped sphinx build lines into the tika service. Rejected; the template is byte-identical to its pre-merge state again.

web-app-docs/templates/Dockerfile.j2: the designer file renders sphinx under the design title and reads SPHINX_IMAGE, SPHINX_OUTPUT_DIR and SPHINX_EXEC_DIR, none of which exist in web-app-docs, which builds from files/Dockerfile. Rejected.

web-app-docs/templates/playwright.env.j2 and files/playwright/test-design.js: both sides add independently, so both are kept. Of the designer's three new env reads only designTitle survives; iconUrl and faviconUrl are shadowed inside the test by stricter requireDotenvValue reads and were dead at module scope.

plugins/lookup/email.py merges clean toward designer, which is intended rather than a loss: 08ef66d8de (2026-10-08) replaces the submission_via_relay port-25 rule of 233f15f3e0 (2026-07-01) with per-bot app passwords under SSO, and carries the matching 04_manage_user/app_password tasks.

Checked after resolving: no duplicate YAML keys across the 17 merged YAML files, and no nocheck line dropped above a renamed key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-15 22:21:50 +02:00
kevinveenbirkenbach 63eede9137 Merge branch 'main' into feature/designer 2026-10-15 20:25:34 +02:00
infinito-nexus-updater[bot] 702cc2bde2 update(images): update Docker image versions (web-app-gitlab) 2026-10-15 15:58:42 +00:00
kevinveenbirkenbachandClaude Opus 5 7c5c7a31fc fix(web-app-gitlab): mount the shell secret where Rails looks for it
gitlab.yml.j2:100-103 splits the two paths: gitlab_shell.path is /home/git/gitlab-shell/, which lives in the container's ephemeral layer, while secret_file points at the read-only mounted secret under /var/opt. lib/gitlab/shell.rb:45 returns early only when path/.gitlab_shell_secret exists, so on every boot of every fresh container that guard is false and generate_and_link_secret_token runs. Its own protection is a TOCTOU: ln_s(force: true) swallows the error from remove_file and then calls File.symlink, so two Rails processes in the same container both get past the check and the loser dies.

Observed in run 38673950436, job 116229890606, at the role's last task: Errno::EEXIST from symlink on /home/git/gitlab-shell/.gitlab_shell_secret, raised at lib/gitlab/shell.rb:90 in generate_and_link_secret_token, reached from lib/gitlab/shell.rb:47 in ensure_secret_token!. Frame 47 proves the line-45 guard did not fire.

The same secret is now mounted at that path as well, for webservice, sidekiq and migrations, which share one gitlab.yml. The guard is true from PID 1, ln_sf is never reached, and the race is removed rather than narrowed. Nothing reads the new mount: secret_token still resolves through secret_file, so the added path only flips one boolean.

It lands as its own shell_path_secret entry rather than three more mounts on shell_secret. One volume entry mounted twice into the same service has no precedent anywhere in the repository, and container_volumes.py would emit two secrets: refs sharing one source into a single service; the separate entry keeps one ref per service, the shape the other 27 entries in this file already deploy. compose_volumes.py names it gitlab_shell_path_secret_<sha8>, distinct from gitlab_shell_secret_<sha8> but hashed from the same source, so both still rotate together.

5fdaba6af2 timed around the race from inside 03_design.yml. That wait keeps its value, since the MCP flush recreates the containers and the appearance step then drives webservice, so it moves to 00_core.yml directly after the step that destroys readiness and widens to the role's five-service 02_wait.yml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-15 12:19:41 +02:00
kevinveenbirkenbachandClaude Opus 5 5fdaba6af2 fix(web-app-gitlab): wait for the webservice before converging the appearance
The appearance task starts a rails runner while the container is still booting, so a second Rails process races the first into Gitlab::Shell#generate_and_link_secret_token and dies on symlink: File exists @ /home/git/gitlab-shell/.gitlab_shell_secret. That is the whole failure of job 116229890606 in run 38673950436.

It is not leftover state: meta/volumes.yml carries no mount for /home/git, so the file lives in the webservice container's own writable layer and cannot survive a run. The health precondition the task relied on was lost when utils/mcp.yml's handler flush moved ahead of it; this restores it through the role's own 02_wait/wait_one.yml, the same helper 02_wait.yml already includes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-15 10:59:56 +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 e422181d88 docs(roles): keep two TODO notes true after main's version bumps
The main merge 8c82e5262e brings the updater bot's pins: GitLab's CNG images move from v19.4.1 to v19.4.2 and n8n from 2.42.3 to 2.44.0. roles/web-app-gitlab/TODO.md said meta/services.yml pins v19.4.1, and roles/web-app-n8n/TODO.md named the request /api/versions/2.42.3; both now state the fact without a version that the next bump would make false again, and the n8n note keeps 2.42.3 as the version it was measured on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:28:29 +02:00
kevinveenbirkenbach 8c82e5262e Merge branch 'main' into feature/designer
# Conflicts:
#	roles/web-app-suitecrm/files/session.yaml
#	roles/web-app-suitecrm/meta/volumes.yml
2026-10-13 15:27:59 +02:00
kevinveenbirkenbachandClaude Opus 5 1e64d3da9c feat(web-app-gitlab): make the platform administrator a real GitLab admin
The role declared `platform_administrator` as its administrator and GitLab
disagreed. Community Edition maps no OIDC group onto admin, templates/config/
gitlab.yml.j2 sets no `admin_groups`, and nothing promoted the account after
its first sign-in, so `/admin` answered 404 to the one identity the
deployment calls the administrator. The rights lived on `root` instead,
which carries that same person's password, so one secret opened two doors
with different privileges and an audit of "who administers this" named an
account that belongs to nobody.

tasks/utils/platform_admin.yml provisions the account instead of deriving
it: files/ruby/ensure_platform_admin.rb creates it when absent, sets the
admin flag when missing, and realigns the password, reporting [CHANGED],
[UNCHANGED] or [FAIL] the way the root realign beside it already does.

Three details decide whether this works, and each is read off the role
rather than assumed. `skip_confirmation: true` is what lets
`auto_link_user: ["openid_connect"]`, already set in gitlab.yml.j2, bind the
first OIDC sign-in to this account by e-mail instead of creating a second
one; the role's own MCP provisioning uses that flag for the same reason.
Clearing `password_automatically_set` is what keeps an account OmniAuth
created earlier from rejecting every password, a trap the root script next
door documents. And the account is looked up by username or by any e-mail,
so a pre-existing OIDC identity is promoted rather than duplicated.

`root` stays the break-glass account and keeps its realign. Giving the
declared administrator its own rights is what lets root stop being the
everyday admin; blocking it, as the designer branch proposes, would lock out
the recovery path this branch relies on.

The sharpened persona check from 989bf86374 should go green with this, which
is the point: it was red because the deployment was wrong, not the test.

mobilizon and port-ui share the shape and are handled separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 15:01:08 +02:00
kevinveenbirkenbach de3127683f Merge branch 'feature/designer' into feature/store-i18n 2026-10-13 12:56:06 +02:00
kevinveenbirkenbachandClaude Opus 5 7c13e96c96 Merge branch 'main' into feature/store-i18n
Twelve files conflicted; most resolutions keep both sides rather than pick
one.

cli env.py and compose.py: both branches extend the same spots. The tools
override is appended before write_override, because that call filters the
mirror override to the compose files it is handed. compose.py keeps this
branch's CACHE_ROLE_SHELL_DIR, since the cache scripts moved into a role and
main's literal scripts/docker/cache path is their old home, and takes main's
mirrored init image alongside it.

utils/docker/image/discovery.py: this branch's version_variants expansion
plus main's whole runner-image block, which runner_mirror imports.

sys-svc-compose network create: main's side wins on substance and this
branch's on structure. main carries eda6249d7b, the fix for a probe that
matched an id prefix and let a sibling shadow the exact name; this branch had
refactored the task into create/overlay.yml and create/bridge.yml but brought
back the docker_network_info probe that fix replaced. The subfiles stay and
now probe through the exact-name lookup. main's half also read
SWARM_CLUSTER_AVAILABLE, which this branch removed in favour of
IS_SWARM_MODE, as the merge base confirms, so the gates follow this branch.
The lookup's import follows this branch's get_entity_name to entity_name
rename.

web-app-pixelfed: the custom entrypoints stay removed, since the new image
brings its own and the variables behind them no longer exist, and main's
host_alias=true comes along.

web-app-suitecrm: main's session.yaml and its config/packages path, because
only that version wires framework.session and only that directory is read by
Symfony.

web-app-odoo: main's onionOptions import is required by the test.use below
it; this branch's test-seaweedfs registration stays.

Four defects surfaced that no conflict marker shows.

The auto-merge of web-app-suitecrm/vars/main.yml duplicated
SUITECRM_SESSION_CONFIG_HOST, which ansible-lint rejects. Removing it exposed
the second: SUITECRM_SESSION_CONFIG_DOCKER still named config/session.yaml
while the mount had moved to config/packages, so the freshness comparison in
tasks/01_manager_ops.yml would have tested a path nothing writes.

svc-db-elasticsearch carried environment: on two include_tasks, which Ansible
accepts on include_role only. ELASTIC_PW now sits on the five tasks that read
it, which is also the tighter scope for a credential.

The openclaw templates render NETWORK_TRUSTED_PROXY_CIDRS while their test
renderer passes only its explicit kwargs, so seven tests raised Undefined.
That predates the merge: the template line is on this branch and not on main,
and both sides share the same test file.

test_runner_mirror neutralises four environment variables under clear=False
and missed three, so the gpu and tools overrides leaked in from the container
and the assertion on the layered list broke. It now neutralises all seven.

make quality-high passes both phases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 10:57:22 +02:00
kevinveenbirkenbachandClaude Opus 5 989bf86374 test(web-app-gitlab): assert the admin area instead of the word GitLab
The administrator persona's interaction accepted `projects`, `groups` and
`settings` as its target, which any signed-in user reaches, skipped its
whole body when no link matched, swallowed the click's failure, and then
asserted the page contained one of `project|group|merge request|issue|admin
area|gitlab`. The last alternative is the defect: GitLab prints its own name
on the 404 page, so the assertion passed precisely when the admin area was
not reached. The locator now accepts only the admin entry, across the label
spellings GitLab has shipped, the click is allowed to throw, and the landing
is checked by URL rather than by page copy, which survives a renamed heading
and does not depend on the interface language.

This is expected to fail while it runs. The persona signs in as
`platform_administrator`, and nothing makes that account a GitLab admin:
the image is `gitlab-webservice-ce` (meta/services.yml), Community Edition
maps no OIDC group to admin, templates/config/gitlab.yml.j2 sets no
`admin_groups`, and the role performs no promotion after the first sign-in.
`/admin` therefore answers 404 and the entry is never rendered. The old
assertion hid exactly that. The callback runs in the two of three variants
that enable sso.

The fix belongs in the role, not in the spec: provision the account natively
with admin rights and let `auto_link_user: ["openid_connect"]`, already set
in gitlab.yml.j2, bind the OIDC identity to it. That covers both sso states
with one identity, where a promotion after the first login would leave a
fresh deployment without an administrator under the declared name until
someone signs in. Until then `root` holds the admin rights while carrying
`platform_administrator`'s password, so one secret opens two doors with
different privileges.

mobilizon and port-ui share the shape: both run the persona without an
administrator behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 01:57:42 +02:00
kevinveenbirkenbachandClaude Opus 5 463053e061 fix(roles): keep credentials out of the argv list form too
`container exec` was handed 25 live secrets across 10 roles through an
`argv:` list, where `-e` sits on one entry and `NAME={{ secret }}` on the
next. Each value is an argv element of the host-side process, so `ps` shows
it to every local account while the call runs, and `no_log` masks Ansible's
output rather than the process table. All 25 now travel as `-e NAME` with
the value in the task's `environment:`.

The three container lints added earlier this week all missed this, because
they scan line by line and the two halves of the flag never share a line.
`container-exec-env-value` therefore reads `argv:` lists out of the parsed
YAML as well.

Widening it exposed the name heuristic as too blunt: `_TOKEN_NAME`,
`_API_KEY_NAME` and `_LIFETIME_DAYS` match `TOKEN` and `KEY` while carrying
nothing secret. Nine `nocheck` markers would have papered over that, so the
rule now disqualifies a name whose last segment describes a credential
instead of holding one. A secret reached through such a name is still caught
by its source expression.

Counting by variable name hid one site: discourse carries the OIDC client
secret in three tasks, and deduplicating by name collapsed them to one. The
sweep is verified by the raw occurrence count instead, which is why this is
25 rather than the 24 first reported.

Roles touched: bigbluebutton, checkmk, discourse, gitlab, jellyfin, n8n,
nextcloud, openproject, openwebui, snipe-it.

With `command` and `shell` the value still appears once on the module
wrapper's line, which `Shell.env_prefix` builds, so the exposure shrinks to
a single process rather than vanishing.

feature/designer fixes the same bigbluebutton defect in 90d491b5a7 and goes
further: it drops the base64 encoding, which only ever existed to survive
argv quoting and is pointless once the value skips the shell, and it repairs
an administrator created unverified. That side wins at the merge, except for
its `lookup('users', 'administrator')`, which this branch forbids outside
user-administrator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 01:47:23 +02:00
kevinveenbirkenbachandClaude Opus 5.5 c4764235d1 feat(web-app-gitlab): carry the corporate design into GitLab's appearance
GitLab reads its logos, favicon, PWA icon and title from the Appearance
model, so tasks/03_design.yml renders the logo set and runs
files/ruby/appearance.rb through rails runner after the MCP surface.
The script uploads logo, header logo, favicon and PWA icon, sets the
title, PWA name and short name, and removes only the fields the role
set earlier when design is off or a field is missing.
templates/style.css.j2 maps GitLab's colors onto the design tokens,
templates/design.js.j2 mirrors GitLab's own color mode, and
meta/services.yml declares the logo slots.

files/playwright/test-design.js signs in natively as GitLab's root
account with ADMIN_PASSWORD, which holds root's password on this branch
and on feature/store-i18n alike, checks the tokens in both modes, a
color mode picked in the preferences, the sign-in page, the signed-in
interface, top-bar focus, logos and title, and root's admin area, and
captures the gallery. The pass's own administrator provisioning stays out of
this commit: role logic is owned by feature/store-i18n, which keeps
root as its break-glass account.

TODO.md records the open self sign-up, the MCP owner password in argv,
Gravatar and the stale MCP section of the README.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 00:48:18 +02:00
kevinveenbirkenbachandClaude Opus 5 23e642a3f3 fix(roles): realign the admin password on every deploy, not only at install
3c72c2002a and 5366788ab1 moved application roles from the host account
administrator onto platform_administrator. administrator never rotates: it
declares ansible_become_password in roles/user-administrator/meta/users.yml, so
the generator pins nothing into host_vars, and rotate.py:12 exempts it anyway.
platform_administrator rotates with every --users pass, between PASS 1 and
PASS 2 of a matrix-deploy round. The roles below wrote the password only when
they created the account, so from the second pass on the instance kept the
previous value while the deploy rendered the new one, and the sign-in failed.

Each one now re-applies the declared password through its own handle. baserow
drops the 'if created' guard and compares with check_password. gitea and
mattermost gain a change-password call beside the create they already had.
gitlab realigns its native root break-glass through the Rails runner and clears
password_automatically_set, which would otherwise make valid_password? return
false whatever the password is. magento and snipe-it get a script because
neither ships a CLI for an existing account; magento's runs under AREA_GLOBAL so
the adminhtml-only admin_user_save_after observer never loads, at the price of
the admin_passwords history row. matomo moves its realign out from behind the
token gate a rotation never opens. The matrix ansible flavor gets the realign
its compose flavor already had. bluesky resets the native PDS account through
the admin API when the create reports the handle taken.

Every new path follows the contract web-app-nextcloud and sys-svc-rdbms already
use: [CHANGED] / [UNCHANGED] / [FAIL] on the wire, failed_when on rc or [FAIL],
and an absent account is a failure rather than a silent zero-row success. The
password reaches the container through container exec -e NAME plus a task
environment, never the host-side argv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-12 21:21:57 +02:00
infinito-nexus-updater[bot] a9aec3305d update(images): update Docker image versions (web-app-gitlab) 2026-10-12 07:34:06 +00: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 671dd2f192 test(lint): require a block from two repeated when runs, and read the list form
c61c68e903 set the threshold at three and matched only the inline "when: X"
form. A guard written as a list escaped it entirely, although its first element
carries the same shared condition, so a copy-pasted guard passed whenever the
task also carried a probe-dependent clause. _first_list_item reports that
element.

Lowering the threshold to two turns 72 runs across 68 role task files into
blocks, and moves the runs above three linked repetitions into include files of
their own.

test_credential_rotation.py walked the openbao task tree and compared guards as
strings. The blocks put tasks one level deeper with accumulated guards, so it
now collects them through leaf_tasks_of and guarded_blocks_of and compares
against lists.

Equivalence is measured rather than argued: expanding both the committed and
the new tree through include_tasks and diffing the effective when of every task
leaves no difference across 73 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 14:36:03 +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 e2948332fd fix(merge): repair what the main merge left broken
The merge brought in main's new roles and carried this branch's cross-cutting
renames and lints past them, so files that never conflicted were left
inconsistent. make quality-high is green again: 4 test targets, 15 lint targets.

a05071fe78 renamed get_entity_name to entity_name. Three files main added still
used the old name and merged cleanly because this branch never touched them:
plugins/lookup/email.py raised ImportError on import, which also took down
sys-ctl-hlth-msmtp's mail_recipient lookup; web-app-stalwart/tasks/00_core.yml
applied `| get_entity_name`, a filter that does not exist (the registration in
plugins/filter/get/entity_name.py is `entity_name`), so a compose deploy of
stalwart would have failed while rendering sys-stk-front-proxy's vars; and
test_database.py patched a name the module no longer binds.

web-app-stalwart and web-app-mailu spell DEPLOYMENT_MODE out in 15 places. The
named booleans in group_vars/all/18_swarm.yml are the SPOT and the bare form is
the idiom: 177 existing uses, and web-app-listmonk/tasks/utils/mcp.yml:42,49
drives both as mutually exclusive `when:` conditions, which a truthy string
would break.

svc-net-tor-smtp pinned aiosmtpd and PySocks inside its Dockerfile, where
Dependabot cannot see them. The pins move to files/requirements.txt; since
sys-svc-compose stages only the Dockerfile, 00_core.yml now stages the
requirements file into the build context too.

stalwart's webui_version had no upstream declared. Its three inline
`environment:` blocks stay, with a nocheck each: the role sets no
docker_compose_env, so one templates/env.j2 would also be the env_file of the
sibling containers, and clamav would receive the recovery admin credential and
the OIDC client secret.

The 13 bindings in stalwart's playwright env.js defaulted to "" and were
consumed unconditionally. Every key is emitted unconditionally by
playwright.env.j2 from a lookup that fails rather than yields empty, and
OIDC_ISSUER_URL always concatenates the realm path, so requireDotenvValue is
the correct binding. gitlab's adminUsername gets the toBeTruthy guard its two
siblings already had.

web-svc-api and svc-cache-package collided with stalwart on local.http 8094 and
8095 and move to 8098 and 8099, which exhausts the 8001-8099 band.
svc-ai-agent-broker collided with stalwart on 192.168.223.0/24 and moves to
192.168.81.0/24; the suggester only subdivides blocks already in use at the
requested prefix, so the move was validated with --block.

web-app-weblate calls lookup('email') without emitting
lookup('container_extra_hosts'), so on an onion deploy its SMTP client drops
every message without an error.

test_main.py read the primary domain from a literal while main moved
INFINITO_DOMAIN to main.infinito.test. It now takes the value from
default_domain_primary(), in the shape e2beab9f1c uses on
feature/web-app-wazuh/add-role, so that branch merges here without a conflict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 03:21:09 +02:00
kevinveenbirkenbachandClaude Opus 5 f2fedfc677 Merge branch 'main' into feature/store-i18n
Fourteen real conflicts plus the 66 generated READMEs.

utils/cache/files.py and its test: both sides added a skip-dir rule, resolved
as a union so the root `build/` prune and the per-root walk key coexist.

plugins/lookup/database.py and postgres_libraries.py: main's added lookups kept,
with get_entity_name renamed to entity_name as this branch renamed it.

roles/web-app-{mailu,matrix}/meta/services.yml: main's `covers: [email]` and its
MAIL_PROVIDER-gated email entry, plus this branch's gitlab/jira/n8n/jitsi
consumers, which the merge base carried on neither side.

group_vars/all/17_resource.yml: this branch's lookup('resource', ...) refactor;
main's single added literal is already covered by _POSTGRES_MAX_LOCKS_PER_TRANSACTION.

docs/requirements/035-mcp-proxy-expansion.md: this branch moved the design into
ADR 0003, so the requirement keeps only the user story and the criteria. Main's
delta inside the moved block was two new roles, carried into the ADR audit:
web-app-stalwart as adapter_candidate, svc-net-tor-smtp as privileged_control_plane.
The no_surface total said 99 while its own subcategories summed to 112.

inventories/development/default.yml: biber keeps no explicit email. Both sides fixed
the same CI failure; this branch fixed it in _apply_account_defaults, which renders
biber@{{ DOMAIN_PRIMARY }} (utils/cache/users/__init__.py:68) — byte-identical to
the address main hardcoded, so the inventory line is a second copy of one fact.

roles/web-app-{gitlab,keycloak}/files/playwright/playwright.spec.js and
roles/web-app-gitlab/templates/playwright.env.j2: union of both import sets, with
requireDotenvValue kept over decodeDotenvQuotedValue for CANONICAL_DOMAIN because
an empty value must fail rather than match every substring.

The 66 role READMEs take this side: aceb5295ef moved Cosmos, Quick Setup and
Credits to the documentation build, and test_readme.py now rejects them in a
committed README. Main's prose edits inside them are carried over by hand (the
Mailu deprecation banner, Mailu -> Stalwart in erpnext and zammad), and the two
READMEs main added fresh are stripped of the three generated sections.
roles/web-app-sphinx/README.md stays deleted; 6eb9a59d14 merged that role into
web-app-docs. The root README's generated role table was regenerated from the
merged metadata.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 01:57:17 +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 a05071fe78 refactor(roles): let a role declare its entity name and drop the get_ prefix
The entity name keys a role's instance directory, its compose project and its container name, and was derived only by stripping the longest matching category prefix. Two roles under different categories whose names share a suffix therefore collapse onto one entity and overwrite each other's rendered stack, which no provides: can repair.

A literal entity_name in vars/main.yml now wins over the derivation. web-app-seaweedfs-console takes its shorter name back and declares the entity seaweedfs-console, which frees the seaweedfs key in its own services map for the engine consumer and removes the provides:, covers: and alias entries that stood in for it. A new lint pairs the declaration with the primary key it has to match, because a drift between them would drop the role from the provider set in silence.

Compared across all 288 roles, exactly one entity changes, and it restores what main already keeps on disk, so no instance directory moves.

The function and the filter lose their get_ prefix. Two lints read that prefix as the discriminator between the variable and the filter that fills it; both discriminate on the pipe now. Ansible's templar resolves a filter and a same-named variable independently, verified against ansible-core 2.20.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 19:40:53 +02:00
kevinveenbirkenbachandClaude Opus 5 75e90d0a3c fix(web-app-gitlab): declare the service keys its addons resolve against
c83c00da7d gave the role six addons whose enabled reads
lookup('config', 'web-app-gitlab', 'services.<x>.enabled'), but meta/services.yml
never gained confluence, jenkins, jira, matrix or mattermost. The lookup takes
two terms there, so strict=True applies and a missing path does not fall back:

    AppConfigKeyError: Key 'confluence' not found in dict at 'confluence'
    Full path so far: applications['web-app-gitlab'].services.confluence

Measured against HEAD: none of the five keys exist, and the strict getter
raises rather than yielding a false. Rendering any of those addons aborts.

jenkins, matrix and mattermost get the dynamic consumer flag. confluence and
jira are pinned to a literal false with a dynamic-flag marker, because both
roles are end of life and no inventory may bind them.

Each dynamic flag gets exactly one true-pinning round — mattermost in variant 0,
matrix in variant 1, jenkins in variant 2 — so the coverage contract is met
without pairing two heavy partners in one round.

test-integrations.js asserts that each instance integration page stays
admin-only, registered from playwright.spec.js.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 01:34:30 +02:00
kevinveenbirkenbachandClaude Opus 5 c83c00da7d feat(addons): extend the req-026 addon contract to twenty roles
Each addon now carries four artefacts: meta/addons/<id>.yml declares it
(enabled, required, mechanism, source, bridges, optional config),
tasks/addons/<id>.yml is its install hook, files/playwright/addons/<id>.spec.js
proves it, and templates/playwright.env.j2 emits the gate flag through
lookup('addon_env_flags', application_id). A hook holding nothing but the
document marker means a generic path already installs the addon.

roles/web-app-odoo/vars/main.yml is part of this commit rather than a later
one: the three new odoo addons declare no 'group', and ODOO_MODULES_CORE
filtered on value.group without guarding it, so the jinja undefined marker
broke ODOO_INIT_MODULES at env.j2 render time, before any container started,
in every variant and both modes. The selectattr('value.group','defined')
guard matches what web-app-keycloak and web-app-nextcloud already do at the
same shape.

Two addons are declared and deliberately off because no install path exists:
web-app-moodle's mod_jitsi and aiprovider_openwebui are absent from the image
build, and web-app-suitecrm's OutboundEmailAccounts has no SMTP wiring at all.
web-app-mattermost's alertmanager plugin is off because the official
marketplace carries no such plugin - 'mmctl plugin marketplace list' returns
twenty entries and none of them is it.

Residuals: web-app-gitlab's five addon specs clear cookies and only assert
that an anonymous visitor reaches /users/sign_in, which holds for any /admin/
path, so they pass without proving the integration. The sso gates added to
discourse's bridge specs keep them from failing in the one variant that
enables their partners, which also means those bridges are exercised in no
variant until a round pairs sso with the partner pins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-02 17:20:12 +02:00
kevinveenbirkenbachandClaude Opus 5 e8e89d293d refactor(seaweedfs): split the object store from its browser consoles
web-app-seaweedfs becomes two roles. web-svc-seaweedfs keeps the storage
engine: the container, the seaweedfs_data volume, the per-consumer S3
identities and the single canonical domain api. web-app-seaweedfs-console
takes the filer and master web UIs into its own compose stack behind the
admin-only oauth2 proxy, reaching the engine over the shared object-store
network. A node can now run the store without the consoles.

The console needs a distinct role name because get_entity_name strips the
category prefix: both halves would have resolved to the entity seaweedfs and
collided on the compose project, the instance directory and the container
name.

The filer and master ports move into the engine's services.seaweedfs.ports
.internal as their single owner; 01_init_consumer.yml consumes the master
port and would otherwise have resolved it only by role-var leakage from the
console, breaking bucket provisioning on exactly the console-less node the
split makes possible.

The engine's api vhost gains the prometheus locations. It never had them: the
override satisfied the cross-cutting-include lint only because the strings sat
in the SSO branch that the api domain never took.

Consumers, the objstore lookup and the catalogs follow the role id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 02:20:12 +02:00
kevinveenbirkenbachandClaude Opus 5 c616b8292f test(playwright): require every unhandled empty env default, not just matchers
48b119a381 refused an env value defaulted to "" only where it fed `endsWith`,
`startsWith` or `includes`. That is the loudest shape, not the only one:
`const baseUrl = normalizeBaseUrl(process.env.XMPP_BASE_URL || "")` followed by
a navigation to `${baseUrl}/` sends the spec to `/` when the deploy never wrote
the key, and nothing fails.

The rule is now general: an env binding defaulted to "" MUST have its emptiness
handled. Four forms count, and the lint decides all four itself so a suppression
stays rare:

  * bound through `requireDotenvValue`, which throws while the module loads;
  * a second fallback in the expression, `JSON.parse(... || "") || "{}"`;
  * the expression compares itself, `(... || "").toLowerCase() === "true"`,
    where empty simply is not equal;
  * the name's emptiness is tested in the file, through `expect(name, ...)`,
    `if (!name)`, `!name` or `name ? ... : ...`.

The third form earned its place immediately: without it a boolean coercion would
have needed a nocheck, and writing the rule turned that false positive into a
pass.

Of 304 empty-defaulted bindings, 241 already handle the case and 63 did not. 56
are now required. The remaining 7 keep `|| ""` behind a stated exception, and
finding them is the part worth recording: checking all 107 required keys against
the `{% if %}` nesting of their role's env template showed that
`web-app-openbao` emits LAM_BASE_URL and the four LDAP keys, and
`web-app-fediwall` its two source URLs, only inside a conditional. The binding
runs at module load, long before `test.skip`, so requiring them would have
broken openbao variants 1 and 2, which deploy no LAM. Reverting them re-created
a `decodeDotenvQuotedValue` use whose import the conversion had already dropped;
`node --check` does not catch that, a used-versus-imported sweep does.

Two further checks close the method's own gap: no `playwright.env.j2` contains
an `{% include %}`, and `02_run_one.yml:162` renders the file from exactly one
template per role, so the textual scan saw everything. Against the real rendered
env kept from a deploy of `web-app-openbao variant=0`, all 7 keys its specs
require are present.

Verified by running it: node --check on every rewritten file, eslint 0 errors
(the 187 warnings predate this), `make lint-playwright` parses 89 specs, the
four playwright lints pass. The playwright run carries the weight, because 99
bindings now throw at module load if their key is missing.

The predecessor also had a latent defect this replaces: its `Violation` was a
frozen dataclass without `order=True`, so the `sorted()` in its failure path
would have raised `TypeError`. It never ran, because the rule never found
anything to sort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 22:31:37 +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 fe2156b05e test(lint): make a looped include name its loop variable
A looped `include_tasks` or `include_role` leaves its item in scope for
everything the include reaches. Left at the default name, Ansible warns as
soon as any task inside loops as well, and the sharp case is quieter than the
warning: the included body reads `item` expecting the value the include was
called with, while an inner loop has already repointed the name. Nothing
fails, so the wrong value is written and the run reports success.

web-app-prometheus was carrying exactly that shape. It hands the item to an
included file through `vars:`, which Ansible templates lazily, so
`network_role_id` was one nested loop away from resolving to something else.

The rule complements the existing loop_var shadowing lint, which checks the
other direction and cannot see this one. It parses the task files rather than
scanning their lines: a line window around each include also catches the next
task's `loop:` and reports 55 findings where there are 22.

Fourteen of those are the plain dependency include and take
`dependency_role`, the name dsk-gnome already uses. The rest take a name for
what the item is. The gitlab and mastodon wait includes pass nothing through
`vars:` and their included file reads `item` four times, so each needed that
file renamed with it; naming only the parent would have left `item`
undefined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 16:50:34 +02:00
kevinveenbirkenbachandClaude Opus 5 37e80b957e refactor(roles): keep a container's environment in one file
A container's environment had two homes. sys-svc-container's base.yml.j2
already emits env_file for any role that ships templates/env.j2, and 89
roles do, yet fifty compose templates still opened an environment: block of
their own. The half written there is the half nobody greps when they go
looking for what a container is configured with, and a secret placed there
also shows up in docker inspect, where an env file does not.

Nine roles move their keys across: odoo twelve, jenkins five, zammad three,
friendica and peertube two each, and one apiece for lmstudio, ollama,
registry-cache and registry-docker. Four of them gain their first env.j2.
The keys are carried over unchanged except where the new home demands it:
YAML quotes are dropped, because an env file would take them as part of the
value, and odoo's database password gains dotenv_quote, which the inline
list never gave it.

The remaining forty-one blocks keep a nocheck that names the services the
role's single env.j2 would otherwise reach. Most are mcp sidecars whose
ADAPTER_BEARER must not land in the application container beside them;
gitlab's six services disagree about CONFIG_TEMPLATE_DIRECTORY; xwiki
re-renders its compose template a second time with the auth switches
flipped, which an env file rendered once cannot follow. Nine plus forty-one
accounts for all fifty.

A lint holds the line from here. It flags an environment: that opens a block
mapping in any role's compose template and takes a marker on that line, on
the comment block above it, or in the file's leading comment block. The
leading-comment scope is deliberate: a raw line window would let a marker
written for one service silence every later block in the same file, and a
unit test pins exactly that, failing against the window and passing against
the header.

Measured on the tree: the key set each role moves is identical before and
after, with no key lost or gained in any of the nine, and no moved value
contained a dollar sign, so nothing changed meaning between compose
interpolation and an env file that performs none. Dotted keys such as
zammad's discovery.type survive both docker compose config and docker stack
config unaltered.

Not yet deployed. Eight of the nine cannot spread a key at all, being single
service or, for jenkins, having the second service override
docker_compose_env. zammad is the exception: three elasticsearch keys now
reach its siblings, where ES_JAVA_OPTS is read only by a JVM entrypoint and
the dotted names are not valid shell identifiers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 17:42:22 +02:00
Alejandro Roman b11815b80b Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-26 18:07:16 +04:00
kevinveenbirkenbachandClaude Opus 5 53a20bc4c9 refactor(vars): ask the deployment mode by name, not by string comparison
DEPLOYMENT_MODE was compared against a quoted literal 219 times across 110
files. IS_COMPOSE_MODE and IS_SWARM_MODE say the same thing once, and
test_deployment_mode_booleans keeps it that way: only the file that defines
them may still compare.

Both are real booleans under this ansible, measured before the swap because
the alternative is silent: a variable that templates to the string "False" is
truthy in Jinja, which is why IS_STACK_HOST is written `| bool` at every use.
These need no filter.

What had to learn the names:

  * utils/annotations/task_gate.py, which several lints ask whether a task is
    compose-only gated;
  * four compose-template lints that recognise their gate by matching the
    expression;
  * the fixtures that stand in for the play's variables. They derive the
    booleans from the mode now instead of carrying them, or a test that
    overrides DEPLOYMENT_MODE keeps the old pair and asserts the wrong
    branch.

Each replacement was checked rather than trusted: the 206 conditions that
changed in place were evaluated old against new through the Templar under
compose, swarm and host, with no disagreement, and no changed line turned out
to be anything but a condition swap. `!= 'swarm'` became `not IS_SWARM_MODE`
rather than the shorter IS_COMPOSE_MODE, because only the literal translation
stays correct should the mode ever be host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-26 02:51:58 +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
infinito-nexus-updater[bot] 84a9c05564 update(images): update Docker image versions (web-app-gitlab) 2026-09-24 09:50:56 +00:00
Alejandro Roman 04a657649c Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-23 20:41:12 +04:00
infinito-nexus-updater[bot] b8a354508e update(images): update Docker image versions (web-app-gitlab) 2026-09-22 14:34:52 +00:00
Alejandro Roman 4b9a834546 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-21 03:25:20 +04:00
kevinveenbirkenbachandClaude Opus 5 dd05caa34f feat(services): require an explicit onion decision per host-bound port
A port under ports.local or ports.public binds an address on the stack host, which is what a HiddenServicePort can forward to. Whether it should be forwarded has a cost in both directions: silently off leaves a service unreachable that the operator believes is on the onion, and silently on publishes a database, an admin interface or a SOCKS proxy to everyone holding the onion address. Neither belongs in a default, so the flag is now mandatory and every false carries the reason it is false.

The first run produced 165 findings, of which 131 were one architectural fact repeated rather than 131 decisions. Four groups are therefore settled once in the lint instead: the proxy-fronted http, sso and websocket, because the node onion already forwards port 80 to the reverse proxy that routes every vhost, oauth2 hop and socket upgrade behind it; the implicit-TLS variants, because Tor is already the transport encryption and container_ports drops those listeners when TLS is off, which an onion deployment always is; the categories a hidden service cannot carry, listed once in plugins.lookup.tor_ports.UDP_ONLY_CATEGORIES, which the lint imports rather than restates; and any entity that already answers the question through exposed:.

That leaves 23 real per-port decisions, each with a reason naming its own consumer. svc-net-tor keeps its own socks, trans and dns off the onion: a published SOCKS port is an open proxy, and SocksPolicy cannot refuse it because the forward arrives from the node's own address.

Mailu turns smtp, submission, imap and pop3 on, which is what makes sending and receiving work over the onion, and leaves sieve off because ManageSieve manages filters rather than mail. Measured end to end through the merged applications view, across all 105 roles the deploy now forwards exactly 25, 110, 143 and 587.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:51:59 +02:00