Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 e7b5860b5b fix(test-e2e-playwright): submit native admin login via Enter; stop matching account as an auth marker
OpenProject's /login renders a header Sign-in toggle plus a duplicate pulldown form, so a generic Sign-in button click hits the toggle and never submits the credentials. Press Enter on the password field to post the enclosing form (the button click stays as a fallback). Also drop the bare account token from the admin auth marker so an unauthenticated Create-a-new-account link no longer masks a failed login as no-logout-control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:47:23 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4cac9b8041 fix(web-app-postmarks): block the administrator persona when SSO is disabled
Postmarks admin auth is the SSO trusted-header bridge only; with SSO off there is no native admin login for the persona to drive, so the no-SSO variant failed to reach an authenticated surface. Gate PERSONA_ADMINISTRATOR_BLOCKED on services.sso.enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:47:23 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e36de9d2e0 fix(web-app-openproject): activate the seeded admin so native login is accepted
In the no-SSO variant the admin persona drives native login, but it was rejected ('Invalid user or password') even though the seeded credentials match. Set the user status to active in ensure_admin_user.rb so a freshly-created admin can authenticate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:47:22 +02:00
kevinveenbirkenbachandClaude Opus 4.8 359cbe0bc1 fix(web-app-matrix): run the SeaweedFS avatar test only on the ansible flavor
Synapse S3 media offload (synapse-s3-storage-provider) is wired only in the ansible flavor; the compose flavor stores media on local disk, so the matrix bucket never grows and the avatar seaweedfs check failed on variant-2 (compose). Expose MATRIX_FLAVOR and skip the test unless the flavor is ansible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:47:22 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7151a0d7df fix(web-app-baserow): block the administrator persona when SSO is disabled
Baserow's admin is SSO-gated; with SSO off there is no provisioned native frontend admin (first run forces /signup), so the persona can never authenticate and the no-SSO variant timed out. Gate PERSONA_ADMINISTRATOR_BLOCKED on services.sso.enabled, mirroring the biber gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:47:22 +02:00
kevinveenbirkenbachandClaude Opus 4.8 122f7e43d0 fix(web-app-mailu): retry user-create/password so a transient DB or admin restart does not fail the deploy
On busy runners the mailu admin/MariaDB container can be OOM-killed or briefly refuse connections mid user-loop (flask mailu user -> ConnectionRefused rc=1 / SIGKILL rc=137), which failed co-deployed roles (opentalk, fusiondirectory). Add retries/delay/until to the user-create and password-change commands so a transient outage is waited out; 'already exists' still short-circuits as success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:47:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8ca8d4bbb1 fix(swarm-resolver): prefer locally-running container before cluster lookup
`resolve-container-id` previously used the first running task from the
cluster-wide `container service ps` listing and emitted that task's
container ID. When the picked task lived on a worker but the caller
ran on the manager (every `delegate_to: STACK_HOST` exec path), the
follow-up `docker exec <id>` failed with
`Error response from daemon: No such container: <id>`.

Add a fast path that queries the local docker daemon via
`label=com.docker.swarm.service.name=<svc>` and returns a locally-
running task's container ID directly. The original cluster-wide
task lookup stays as a fallback for the case where no replica is
local — same behaviour as before, no regression possible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 10e60d278e fix(baserow): drop |quote on container_address subshell expression
`BASEROW_APP_CONTAINER_ADDRESS` resolves via the `container_address`
lookup to a bash subshell string `"$(/usr/bin/resolve-container-id
<stack> <service>)"` in swarm mode (and to the bare container_name
in compose mode). The migrations + bootstrap-admin tasks fed it
into `container exec -i` through `| quote`, which wraps the whole
value in single quotes — bash then does NOT evaluate `$()` and
`container exec` is handed the literal string, failing with
`Error response from daemon: No such container: "$(/usr/bin/...)"`.

Drop `| quote` on the two `ansible.builtin.shell` invocations
(migrations + bootstrap). The lookup's output is already shell-safe
(the subshell is double-quoted internally, the compose-mode bare
name has no shell metacharacters). The `ansible.builtin.command`
restart task on the compose-only branch keeps its `| quote` because
that uses `command:` (no shell), runs only under `DEPLOYMENT_MODE
\!= 'swarm'`, and the bare name is shell-irrelevant there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a415eecd1c fix(matomo): pin Matomo to 5.9.0 + raise installer timeouts
The matomo-bootstrap:1.1.13 Playwright walker fails at the
setupSuperUser step against `matomo:latest` (currently 5.11.x):
`Could not find a Next/Continue control in the installer UI`. The
bootstrap was last aligned with the Matomo DOM in v1.1.12 (changelog
2026-02-15, Matomo 5.7.x). 5.9.0 is the oldest 5.x tag still
pullable from Docker Hub, putting us as close to the bootstrap's
known-good DOM as the registry allows.

Defense-in-depth: raise the installer wait/step/deadline knobs in
bootstrap.env.j2 so slow renders under swarm/NFS don't trip the
Playwright walker. Also lower
`MATOMO_INSTALLER_SUPERUSER_RELOAD_INTERVAL_S` from the upstream
default 30 to 10 so the orchestrator's reload-aware helper recovers
faster when execution reaches that branch.

Downgrade caveat: deploys currently running matomo:latest carry a
5.11.x DB schema; the next re-deploy will pin 5.9.0 and Matomo may
refuse to start or behave unexpectedly. Back up the database before
applying this commit on a live cluster.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b627e383a1 fix(keycloak-swarm): manager pin + LOCAL_CID + chdir + sso-proxy DNS/CA
Make web-app-keycloak deployable in swarm without the manager-only
assumptions of the original compose-only shape.

Manager-side delegate scaffolding:
* tasks/02_manager_ops.yml: switch the `include_tasks` of
  02a_manager_ops_on_host.yml to the dict form so the `apply:
  delegate_to: KEYCLOAK_HOST_NODE` block actually attaches (free
  form raised `conflicting action statements: include_tasks, apply`
  in ansible 2.19).

Swarm bind sources that exist only on the manager:
* tasks/05b_realm_import_swarm.yml: pin the one-shot
  `<stack>_<service>_import` service to `node.role == manager` so
  the realm-import bind mount `/opt/compose/keycloak/volumes/import`
  resolves.
* templates/sso_proxy/container.yml.j2: same manager pin for the
  sso-proxy sidecar (its oauth2-proxy cfg bind source also lives
  only on the manager). Also add `extra_hosts` for the keycloak
  auth domain pointing at `host-gateway` so OIDC discovery reaches
  the host's openresty, and mount the self-signed CA when TLS_MODE
  is `self_signed`.
* templates/sso_proxy/oauth2-proxy-keycloak.cfg.j2: add
  `provider_ca_files` so oauth2-proxy validates the self-signed
  cert instead of failing OIDC discovery with TLS EOF.

CA mount path SPOT:
* group_vars/all/02_tls.yml: add `CA_TRUST.cert_container` for the
  in-container CA file path so the literal `/etc/ssl/certs/...` is
  defined once.
* roles/web-app-matrix/vars/main.yml: consume the new SPOT.

Container ID resolution in delegated tasks:
* vars/main.yml: add `KEYCLOAK_TARGET_CONTAINER` (= LOCAL_CID with
  fallback to KEYCLOAK_CONTAINER_ADDRESS) and recompose
  `KEYCLOAK_EXEC_CONTAINER` from it, replacing the inline default.
* tasks/update/05c_per_app_mappers.yml and
  tasks/update/ldap/per_app_mapper_one.yml: `KC_CONTAINER` env now
  resolves via the new SPOT, so the per-app-mapper helper scripts
  hit the actual swarm container ID on the delegated host node
  (compose mode keeps using the bare container_name via the
  fallback).

Delegated script paths:
* tasks/update/08_users_email_verified.yml and
  tasks/update/08_users_realm.yml: drop
  `chdir: "{{ role_path }}/files"` from `ansible.builtin.script`.
  role_path is a controller-side absolute path; on a delegated
  DinD swarm node that path doesn't exist and the script fails to
  start. Both scripts only consume env vars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9cee64430a feat(act-swarm-zombie): disable= plumbing via ACT_ENV semicolon
* Makefile: `make act-swarm-zombie app=<id> disable=<keys>` now
  forwards the `disable=` value into the act run via ACT_ENV. The
  swarm provision CLI inside the workflow reads it through
  `apply_services_disabled_from_env` and removes the named provider
  roles from the test inventory + sets their `enabled: false /
  shared: false` overrides in host_vars. Matches the iteration spec
  in docs/agents/action/iteration/role.md.
* scripts/tests/deploy/act/workflow.sh: switch the ACT_ENV pair
  separator from comma to semicolon so values can themselves carry
  commas (the disable= list is comma-separated). Only one in-tree
  caller (the Makefile target above) currently uses ACT_ENV with
  multiple pairs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3b07cb86f2 fix(swarm-deploy): inventory-filter closure + lint accepts dict-form include
* cli/administration/deploy/swarm/closure.py: drop dep-walked roles
  that are not present in the inventory's role groups so the
  `validate_application_ids` strict in-inventory check no longer
  collides with `disable=` (which removes provider roles from the
  inventory). Operator-supplied seed IDs and the
  `default_placement: manager` safety net are kept unchanged.
* tests/unit/cli/administration/deploy/swarm/test_closure.py: add
  `test_operator_ids_dep_walked_not_in_inventory_dropped` to pin
  the new behaviour and update the existing scenarios to put the
  filtered roles in their test inventories.
* tests/lint/ansible/shell/test_container_exec_is_stack_host_gated.py:
  recognise `include_tasks: { file: X, apply: { ... } }` dict form
  alongside the free-form string. Without it the lint's include
  graph dropped the apply-include edge and false-positived gated
  files reached by it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 02cf3c5e9e fix(users-cache): substitute ORGANIZATION + SOFTWARE_NAME placeholders
Two scalar group-vars are referenced verbatim in
`roles/user-*/meta/users.yml` (`lastname: "{{ ORGANIZATION }}"`,
`description: "owned by {{ SOFTWARE_NAME }}"`). Ansible 2.19's
TrustedAsTemplate gate silently leaves those untagged Jinja
literals unrendered, so downstream consumers received the raw
`{{ ORGANIZATION }}` string. Keycloak rejected the resulting user
updates with `error-person-name-invalid-character` because `{` /
`}` are not valid in a person name.

Add `substitute_scalar_placeholders` next to the existing
`substitute_primary_domain_placeholder`. Both helpers run from
`get_merged_users` immediately before `_render_with_templar`, so the
cache returns fully-resolved scalars regardless of the templar's
trust gate.

Also restructure: move the cache into a `utils.cache.users` package
so the placeholder helpers no longer need a redundant `users_`
prefix:

  utils/cache/
    users/
      __init__.py     was utils/cache/users.py (public API
                      `get_user_defaults` / `get_merged_users` is
                      unchanged; consumers using
                      `from utils.cache.users import ...` keep
                      working via package __init__).
      placeholders.py the substitution helpers, dropping the
                      redundant `users_` prefix.

  tests/unit/utils/cache/
    users/
      __init__.py
      test_module.py        was test_users_module.py
      test_placeholders.py  new: 15 unit tests covering both
                            helpers (nested-structure walk, URL /
                            port handling for DOMAIN_PRIMARY,
                            Jinja-in-raw-value resolution, empty /
                            missing skip paths, and a guardrail
                            pinning the current
                            `_SCALAR_USER_PLACEHOLDERS` tuple).

Update README links under `utils/cache/` and
`docs/contributing/artefact/files/plugins/lookup/users.md` to point
at the new package path.

Placeholders covered today: ORGANIZATION, SOFTWARE_NAME. Extend the
tuple in `utils/cache/users/placeholders.py` when a new scalar
group-var appears in user metas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 af69d00789 fix(lookup): container_volumes prepends newline so indent(4) lands on column 4
The container_volumes lookup returns a YAML string starting with
`volumes:` at column 0. Templates consume it via
`{{ lookup('container_volumes', application_id, '<svc>') | indent(4) }}`
in 129 places.

Jinja's `indent(width)` only indents lines AFTER the first by
default (indentfirst=False), so the rendered output kept `volumes:`
at column 0 and the rest at column 4 — producing a top-level
`volumes:` map in the middle of a service block, which broke the
compose.yml structure (the following `ports:` / `networks:` keys
ended up inside the volumes list and `compose build` failed with
`did not find expected '-' indicator`).

Prepending a leading newline to the lookup's return value lets
`indent(4)` indent every meaningful line including the first one,
without forcing 129 templates to switch to `indent(4, True)` (which
would also need a separate audit of which call-sites already supply
their own leading whitespace).

Existing filter-side unit tests in
tests/unit/plugins/filter/test_container_volumes.py exercise the
underlying `container_volumes()` function which is unaffected — the
newline is added only at the lookup boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 13414814af refactor(lookups): generalize address/service for swarm + application key
container_address + container_service are mode-aware lookups that
build a docker-exec-safe address from `application_id + service_key`.
Two adjustments to align them with how the rest of the project uses
service keys:

* Swarm-mode return value now uses the lookup's `service_key`
  argument directly as the swarm service-suffix instead of
  `services.<key>.name`. This matches the documented contract in
  `roles/sys-svc-compose/files/resolve_container_id.sh` (the helper
  composes `<stack>_<SERVICE_KEY>`) and what `docker stack deploy`
  names a service after (the compose-yaml service key, not the
  compose-side `container_name:` override).

* Compose-mode `_resolve_bare_name` falls back to
  `services[entity_name]` when `service_key == 'application'` and
  the meta has no top-level `application:` entry. 26 roles use
  `application:` as their compose-yaml service key while keying
  their meta/services.yml under the entity name (baserow, listmonk,
  wordpress, mobilizon, etc.). Without the fallback, every consumer
  of `container_address(application_id, 'application')` raised
  `service 'application' missing in '<role>' services config` at
  task-runtime. The fallback is intentionally narrow (only the
  literal 'application' key) so an actual typo in another
  service_key still raises clearly.

Unit-test contract update: test_container_address.py replaces
`test_swarm_mode_uses_resolved_service_name` with
`test_swarm_mode_uses_compose_yaml_service_key`, asserting the
resolver receives the service_key ('mattermost') rather than the
`services.<key>.name` value ('custom-mm-name'). Existing
`test_unknown_service_raises` still passes because the fallback only
fires for service_key='application'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
infinito-nexus-updater[bot] bc0a521a69 update(images): update Docker image versions 2026-06-16 09:40:25 +00:00
kevinveenbirkenbach 37eac59672 Merge pull request #318 from infinito-nexus/dependabot/npm_and_yarn/playwright/test-1.61.0
chore(deps-dev): bump @playwright/test from 1.60.0 to 1.61.0
2026-06-16 11:39:59 +02:00
kevinveenbirkenbach 7258e9b372 Merge branch 'main' into dependabot/npm_and_yarn/playwright/test-1.61.0 2026-06-16 11:39:45 +02:00
kevinveenbirkenbach 5fe5c1f890 Merge pull request #317 from infinito-nexus/dependabot/npm_and_yarn/eslint-10.5.0
chore(deps-dev): bump eslint from 10.4.1 to 10.5.0
2026-06-16 11:37:34 +02:00
dependabot[bot] bb8cdf6814 chore(deps-dev): bump @playwright/test from 1.60.0 to 1.61.0
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.60.0 to 1.61.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 00:07:07 +00:00
kevinveenbirkenbachandClaude Opus 4.8 f9143d9ac5 chore(web-app-matrix): track matrix-docker-ansible-deploy from spantaleev upstream
Switch the MDAD upstream from the kevinveenbirkenbach fork back to the canonical spantaleev/matrix-docker-ansible-deploy repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:04 +02:00
kevinveenbirkenbachandClaude Opus 4.8 61e4ab3411 docs(claude): forbid raw docker exec; reach the live stack via make targets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 97f4e2002a fix(web-app-matrix): relax Synapse rc_login so the SSO e2e suite does not exhaust the burst
Synapse's default rc_login (burst 3, ~0.17/s) is too strict for the SSO e2e suite, where consecutive persona sign-ins drain the burst and leave later sign-ins cycling consent <-> M_LIMIT_EXCEEDED until the test is torn down (Matrix avatar never uploaded). Set matrix_synapse_rc_login with a higher burst via the MDAD ansible flavor vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dcca3dad39 fix(web-app-postmarks): build the admin URL from the origin to avoid a /admin/admin 404
After the proxy SSO handoff the page URL is already /admin, so url().replace(/\$/,'')+'/admin' produced /admin/admin (HTTP 404) and the logout probe found no control. Resolve /admin against the origin with new URL() so it always lands on <origin>/admin, whose navbar carries the native logout link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9b92dc050f fix(web-app-akaunting): wait for the Vue login form before submitting native credentials
The native-login block was skipped when the non-waiting isVisible check ran before the Vue form mounted, so the admin never authenticated and the company-settings logo upload never happened (bucket stayed empty). Wait for the email field + Vue hydration, fill and submit, then confirm the URL leaves /auth/login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 01c23a370d fix(web-app-baserow): move the injected floating logout fallback to bottom-left
When no header/nav anchor exists the logout affordance is appended to document.body as a fixed-position control. Move it from top-right to bottom-left so it no longer overlaps Baserow's top-right '+ Add new' action, and drop a duplicate display:inline-flex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:02 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0214ed8666 fix(web-app-yourls): expose ADMIN_NATIVE_PASSWORD for the no-SSO native admin login
YOURLS' native admin authenticates against the role-local credentials.administrator_password, not the global user password. Export it as ADMIN_NATIVE_PASSWORD (env keys kept in ASC order) so the admin persona's native-login fallback can sign in at /admin/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:02 +02:00
kevinveenbirkenbachandClaude Opus 4.8 25044572b4 fix(web-app-openproject): make the no-SSO admin native login work; skip biber without an account
ensure_admin_user.rb sets the admin password only when the user is new or the current password does not match (avoids re-validating against former-password rules on every redeploy) and clears force_password_change so native login does not hit the change-password interstitial. playwright.env.j2 blocks the biber persona in the no-SSO variant where biber has no account.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:02 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2748c025ca fix(web-app-decidim): point ActiveStorage direct uploads at the public S3 endpoint
ActiveStorage presigned the browser PUT against the internal AWS_ENDPOINT (http://seaweedfs:8333), which the browser cannot reach and CSP disallows, so avatar uploads never landed in the bucket (objects 161->161). Use the public S3 endpoint (objstore public_url minus the bucket path) and allow the SeaweedFS S3 host in connect-src.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ccc1795cca fix(web-app-erpnext): render the Keycloak SSO button on /login under gunicorn --preload
gunicorn runs --preload, so workers fork before the Social Login Key is applied and serve /login from a stale provider cache that clear_cache() (a separate one-shot process) cannot refresh. apply_oidc_settings.py now reports whether the key changed; 03_oidc.yml restarts the backend only on a real change (no steady-state bounce). The e2e helper asserts the SSO button is visible and clicks it instead of falling back to the 500ing oauth2_logins callback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d4f43a43de fix(test-e2e-playwright): add a no-SSO native-login fallback to the admin persona
When SSO is disabled the OIDC step is a no-op, so drive the role's own username/password form with :visible selectors (skips hidden anti-autofill decoys such as snipe-it's password_fake) and a path-walk over /login,/admin/,/admin. The landing-page probe waits 10s; per-path probes 5s so a slow form at / is not abandoned. Fixes no-SSO admin journeys (snipe-it, yourls, openproject).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4cdfde8eef fix(test-e2e-playwright): seaweedfs check requires a NEW object key by default; opt-in mtime for single-slot consumers
Background workers (PeerTube transcode, Pixelfed media jobs) rewrite existing objects during the poll window, so accepting any changed mtime made those consumers false-green. Default is now a new key only (the original count-growth semantics); single-slot consumers that overwrite one fixed key in place (fider tenant logo) pass expectInPlaceOverwrite to also accept a changed mtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 01:58:00 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d612b98ff5 fix(images): correct service_keys + decidim double-tag
Adversarial audit of a98357418 surfaced five roles whose container_image
lookup passed a service_key that does not exist in the role's
meta/services.yml, plus one role producing a double-tagged ref.

* baserow / listmonk / mobilizon: service_key 'application' is the
  compose-internal service name, not the meta key. Correct to the
  entity-name key ('baserow', 'listmonk', 'mobilizon').
* wordpress: same fix with custom=True preserved.
* web-svc-xmpp: 'ejabberd' is the image name, not the meta service
  entry. The meta key is 'xmpp'; the image lookup returns
  'ejabberd/ecs:latest' as before, but via the correct service entry.
* web-app-decidim: DECIDIM_CUSTOM_IMAGE already embeds the tag
  ('<image>:<version>-infinito'), so passing it as image= AND version=
  produced 'image:tag:tag'. Drop the override and pass
  version=DECIDIM_VERSION ~ '-infinito' so the lookup composes the ref
  cleanly. Remove the now-orphan DECIDIM_CUSTOM_IMAGE var.

Empirically verified all five service_keys resolve via
get_merged_applications. Without these fixes every deploy that
includes any of these roles would have raised
'container_image: service ... missing in ...' at compose render time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 93b568eb57 fix(swarm-registry): backfill upstream pull+tag for prefix-only images
The container_image lookup now returns a registry-prefixed image
string in swarm mode unconditionally. When the resulting compose.yml
references e.g. `swarm-mgr-01:5000/registry:2` but the local docker
daemon has not yet been told to tag that name, the previous
push-only path failed with "An image does not exist locally".

Add a backfill branch: if the image is already prefix-form and
neither has a sibling `build:` block nor an existing manifest in
the registry, strip the prefix to derive the upstream ref, pull it,
retag locally with the prefixed name, then continue to the push
step.

This fixes the svc-registry-cache bootstrap step where the cache
service uses `registry:2` upstream but the swarm scheduler needs
the prefixed `<host>/registry:2` tag in the swarm registry before
it can place the service. Previously this only worked accidentally
for roles whose build pass produced the local tag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0ffbb73d72 refactor(images): central container_image lookup + migrate all roles
Add plugins/lookup/container_image.py: returns the full `image: "<ref>"`
line so templates drop the redundant image: prefix and surrounding
quotes. Supports `custom=True` to auto-construct `<entity_name>_custom`
for locally-built images, plus explicit `image=`/`version=` overrides
for exotic cases (taiga back-auth/front-oidc variants, decidim infinito
suffix, etc.).

* 25 unit tests for the lookup cover compose vs swarm prefix, custom
  flag, overrides, tag_only=True, missing version warnings.
* New strict lint (tests/lint/.../test_no_raw_image_in_compose_templates)
  fails on any raw `image:` line in roles/*/templates/*.j2 unless
  nocheck-suppressed. Legacy `image: "{{ lookup(...) }}"` form no
  longer accepted.
* Migrate 159 image refs across ~80 roles to the standalone
  `{{ lookup('container_image', application_id, '<service_key>') }}`
  form. 26 roles use `custom=True` (mariadb, postgres, openldap,
  jenkins, jira, gitlab, nextcloud, mediawiki, shopware, wordpress,
  odoo, joomla, openproject, bookwyrm, espocrm, hugo, littlejs,
  fediwall, roulette-wheel, openresty, confluence and more).
* Fix svc-db-redis application_id leak: service.yml.j2 is included
  into consuming roles where application_id evaluates to the outer
  role, so the image lookup needs the hardcoded 'svc-db-redis'.
* Integration tests (test_templates_require_image,
  test_build_requires_image) recognise the standalone lookup as an
  implicit `image:` at the same indent.
* Mediawiki swarm tasks: add explicit `:{{ MEDIAWIKI_VERSION }}` to
  the registry-prefixed image string because the custom var is now
  bare.

Only 3 legitimate exotic nocheck-suppressed image lines remain:
matrix-mdad runner (nested services.matrix.runner.image path),
matrix mautrix bridges (per-iteration dynamic) and svc-registry-docker
(chicken-and-egg self-reference).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4812601ff7 feat(swarm): cache external images in local registry + skip-self guard
Workers in the DiD swarm cluster cannot reach upstream registries
(Docker Hub, ghcr) from the overlay network. Image pulls hang
indefinitely in "Preparing", which used to abort with a task-converge
timeout much later in the deploy. Until now the existing
`swarm_registry_push.py` only retagged + pushed services that had a
`build:` key (locally-built images); plain upstream images were left
for each worker to pull on its own, which never works in the DiD path.

This change extends the script to also cache upstream images:
* `docker pull <image>` on the manager,
* `docker tag <image> <prefix><image>`,
* `docker push <prefix><image>` to the on-cluster registry
  (`svc-registry-docker`),
* rewrite the on-disk compose.yml in place so the swarm scheduler
  pulls the prefixed name from the local registry over the overlay
  network.

Idempotency: a `docker manifest inspect <prefix><image>` probe runs
before each push; on re-deploys with no upstream version bump the
push is a fast no-op. Hard-fails on any non-zero docker
pull/tag/push so the standard "fail loud when upstream broken" policy
holds; no `failed_when: false`.

Chicken-and-egg guard: when the registry service itself
(`svc-registry-docker`) is being deployed, the new branch would try to
push `registry:2` to its own not-yet-running registry endpoint and
loop on `connection refused`. The handler now skips the sync step
when `application_id == 'svc-registry-docker'`. The registry's own
image is pulled directly by the manager (default_placement: manager)
and never needs to be cached into itself.

Mechanics:
* `swarm_registry_push.py` renamed to `swarm_registry_sync.py` to
  reflect the broadened scope (pull + tag + push, not just push).
* `roles/sys-svc-compose/handlers/swarm.yml`: updated script
  reference, updated task title, added the registry-self-skip
  conjunction in the `when:` guard.

Validated via act-swarm-zombie web-app-baserow: deploy progressed
from ok=145 (aborted at the chicken-and-egg push) to ok=806 once the
guard landed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b0ce32d6c3 ops(act/workflow): preflight on stale devtmpfs binds in actcache
Sandboxed sessions leave behind devtmpfs bind-mounts under
${ACT_ACTION_CACHE_PATH}/<action>@<ver>/.claude/{hooks,settings.json,
settings.local.json,skills}/ and .mcp.json. Act tries to refresh the
cached action repo on the next run, can't `os.RemoveAll` the mounts
(EBUSY), and dies in `Set up job` with no actionable error.

Add a preflight that detects stale `devtmpfs` mounts under the active
cache path and aborts with exit 3 and a copy-pasteable
`sudo umount … && sudo rm -rf …` remediation BEFORE invoking act. Stays
silent when no stale mounts are present.

Confirmed firing in iter 2 of a sandboxed act-swarm-zombie run; silent
in every subsequent t-series run after the operator cleaned the cache.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2a125977af fix(users): substitute primary-domain placeholder before render_with_templar
`get_user_defaults()` builds every user's email/lastname/description with
a hardcoded `primary_domain="{{ DOMAIN_PRIMARY }}"` Jinja placeholder and
relies on `_render_with_templar` to resolve it later. In some Ansible
templating contexts (notably swarm-mode tasks delegated to STACK_HOST)
the templar can't fully resolve `{{ DOMAIN_PRIMARY }}` against the
inventory expression `{{ lookup('env', 'INFINITO_DOMAIN') }}`, so the
raw placeholder gets cached on the very first miss and reused across
every subsequent `lookup('users', ...)` call in that play. The leak
surfaced downstream as
`MATOMO_ADMIN_EMAIL=administrator@{{ DOMAIN_PRIMARY }}` in the rendered
matomo bootstrap.env, which `docker stack deploy` then refused with
`template: expansion:1: function "DOMAIN_PRIMARY" not defined`.

Two robustness fixes in `get_merged_users` so the cache never holds a
half-resolved placeholder:

1) Pull `DOMAIN_PRIMARY` / `SYSTEM_EMAIL_DOMAIN` from
   `templar.available_variables` when missing from the explicit
   `variables=` kwarg, so the cache key + downstream resolution see the
   same domain regardless of who invokes the lookup.

2) Walk the materialized users dict and replace literal
   `{{ DOMAIN_PRIMARY }}` substrings with the resolved primary-domain
   value (computed via the existing `_normalize_domain_candidate`-style
   logic) *before* handing the dict to `_render_with_templar`. Deterministic
   in pure Python, no dependency on templar succeeding for that one
   placeholder.

Validated in act-swarm-zombie t11: the exact `MATOMO_ADMIN_EMAIL=...
{{ DOMAIN_PRIMARY }}` error that broke t10 at line 13172 is gone, and
the run progressed past matomo-bootstrap into the post-deploy phase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b0067512ba fix(volumes): render Jinja in mount source/target + coerce config/secret mode to int
Two related fixes in the volume-rendering filters, both surfaced by
`docker stack deploy` failing on the rendered compose.yml.

1) Render Jinja in mount source/target via the lookup-supplied
   render_jinja closure. `container_volumes` previously emitted the
   `meta/volumes.yml` `source:` / `mounts[].target:` strings unchanged;
   in swarm those flowed verbatim into compose.yml as
   `{{ lookup('nginx', ...) }}:/usr/local/openresty/nginx/conf/conf.d`
   and broke `docker stack deploy expanding mount target` with
   `function "lookup" not defined`. `compose_volumes` had the analogous
   bug for top-level `configs:` / `secrets:` source fields.
   Add `_maybe_render` to `compose_volumes` and the matching
   try/except-AnsibleUndefinedVariable render block to `container_volumes`
   for bind source + mount target, raising `AnsibleFilterError` with the
   offending expression so failures stay diagnosable.

2) Coerce config/secret `mode:` to int. The YAML/J2 idiom carries `mode`
   as a string ("0440"); `docker stack deploy` rejects that as
   "services.X.configs.N.mode must be a number". Add `_coerce_mode_int`
   (octal-string -> int, with bool + empty + non-octal guards) and
   convert before emission. Update the per-service config/secret unit
   tests to expect `0o440` / `0o400` instead of the legacy strings.

Validated by the t5 -> t8 act-swarm-zombie progression (mode-int gate
cleared at t6; bind source/target + config/secret source render cleared
at t7 -> t8, lifting ok=452 to ok=798).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 897ad5886e fix(lookups): tag canonical-volume Jinja sources as TrustedAsTemplate before rendering
`get_canonical_volumes()` returns `meta/volumes.yml` `source:` / `target:`
strings via plain YAML load, so under Ansible 2.19+ they are not
`TrustedAsTemplate`-tagged. When `container_volumes` / `compose_volumes`
later asked their captured `templar.template()` to render those strings,
the trust gate short-circuited silently and the raw `{{ ... }}` flowed
verbatim into the rendered compose.yml, breaking `docker compose build`
and `docker stack deploy` with "service refers to undefined volume
{{ ... }}".

Replace the `contextlib.suppress(Exception)` wrapper in each lookup's
`render_jinja` closure with an unconditional `templar.template` call
whose input is wrapped in `_trust_as_template`, so the engine actually
compiles the template. Nested `lookup('container', ..., 'directories.x')`
calls inside canonical bind sources resolve through normal Jinja
plumbing once the outer string is trusted.

Validated by the t5 -> t8 act-swarm-zombie progression
(ok=261 -> ok=452 -> ok=798).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ebff14798a chore(playbook): any_errors_fatal so swarm deploys abort on first host failure
Without `any_errors_fatal`, a per-host failure (e.g. a stuck swarm task
convergence wait on one node) lets Ansible keep iterating through the
rest of the play before reporting the failure. For swarm test runs that
take 30+ minutes, that wastes a full second pass of irrelevant role
work after the root cause is already visible. Aborting at the first
fatal lets the operator inspect the live cluster while the failure
context is fresh and shortens the iteration loop materially.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e6664c61f6 refactor(sys-svc-compose/handlers): split by deployment mode
Extract the mode-specific handlers from a single main.yml with per-task
`when: DEPLOYMENT_MODE ==/\!= 'swarm'` conditions into two sibling files
(compose.yml, swarm.yml) imported via `import_tasks` with the mode guard
on the wrapper. The mode condition is set once at the import site
instead of being repeated on every handler.

main.yml keeps the shared prune handler (storage-constrained, fires for
both compose-up and compose-build listens regardless of mode).

Also: the handler-invocation lint at
tests/integration/ansible/handlers/test_invoked.py now follows
import_tasks references inside handlers/main.yml so it picks up handler
definitions in sibling files. Without this, the new layout fails the
"every notify maps to a handler" check because the scanner previously
only looked at main.yml.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2539ba5cb2 chore(claude/settings): allow writes to ~/.cache/act for sandbox act runs
Adds ~/.cache/act to the sandbox filesystem allowWrite list so the
act-swarm-zombie iteration loop can refresh per-action git caches
(actions/upload-artifact@v7 etc.) without hitting "Read-only file
system" when act re-clones upstream tags. Mirrors the existing
~/.cache/{gh,pip,pre-commit} entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fce4432d8c style(lint): satisfy ruff in the rails-runner lint (PIE810 tuple startswith, drop redundant nested if)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:49:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 706456e71b test(lint): forbid inline Ruby in Ansible 'rails runner' tasks — require an external .rb fed via a file lookup (nocheck: rails-runner-inline)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d91a09743b chore(web-app-discourse): drop the stale commented-out OIDC account-link inspiration snippet from config.yml.j2
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8bded8fdae refactor(web-app-wordpress): extract the WP Discourse API-key revoke rails-runner Ruby to files/ruby/revoke_wp_discourse_api_keys.rb fed via file lookup
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a85a822fd5 fix(web-app-openproject): link the administrator to the LDAP auth source for header SSO (fixes trusted-header 401) and extract all rails-runner Ruby to files/ruby/*.rb fed via file lookup
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2cbf9d6ad5 fix(web-app-shopware): wait for the S3 object store and retry asset/theme builds in init.sh so a concurrent SeaweedFS hard-restart no longer fails the deploy (DiD-reproduced)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a24c93c3d2 fix(test-e2e-playwright): add a native username/password login fallback to the admin persona for no-SSO variants (snipe-it)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:47 +02:00