Without service_replicas the swarm init one-shot fanned out to node-count (3)
replicas that raced installation and migrations on the shared DB and NFS, so
shopware_init never converged. Pin service_replicas=1; compose is unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ejabberd.yml docker config was mounted mode 0440 root-owned, but the
ejabberd/ecs image runs as uid 9000 and could not read it, so the entrypoint
sed failed with permission denied and the stack never converged. Set mode 0444.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Since ldap.yaml is delivered as a read-only docker config, the entrypoint's broad
'chown -R /var/www/html' (and the two find chmod passes) hit a read-only file and,
under set -e, aborted the whole boot before install/apache: every replica died at
'chown ... ldap.yaml: Read-only file system' and the stack never converged. Make
the three baseline permission commands best-effort with || true; the writable app
dirs below are still chown'd strictly so real failures surface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The config.exs docker config was mounted mode 0440 root-owned, but the mobilizon
image runs as a non-root user and could not read it, aborting boot with
'could not read file /etc/mobilizon/config.exs: permission denied' and never
converging in swarm. Set mode 0444; config.exs holds no literal secrets (all via
System.get_env), so world-readable inside the container is safe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stock xwiki entrypoint restores WEB-INF/xwiki.properties from the data volume and then rewrites it in place; mounting our rendered config read-only over WEB-INF (a swarm config object) makes that writeback fail with 'Read-only file system' and the service never converges. Add an entrypoint wrapper that copies the config (now mounted at /etc/infinito/xwiki.properties) into /usr/local/xwiki/data and lets the stock entrypoint own WEB-INF. Stage the wrapper into the compose build context the same way web-app-jenkins does. Validated in-container: config loaded, no read-only error, Tomcat starts. Compose is unaffected: the wrapper copies then execs the stock entrypoint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alerting_mattermost_webhook_url and alerting_telegram_bot_token were declared with algorithm url/token, which value_generator does not implement, so an unset credential became the literal string 'undefined'. That is truthy, so alertmanager.yml rendered api_url: 'undefined', which alertmanager rejects (unsupported scheme) and crash-loops, so prometheus_alertmanager never converges in swarm. Declaring them default: '' makes an unset value empty (inventory.py writes a default verbatim, no generation), so the existing guards skip the optional receiver. This is the root fix; the earlier consumer-side template guard was reverted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MAIL_FROM_NAME and MAIL_REPLYTO_NAME rendered the provider title plus a
' - Snipe-IT' suffix unquoted, so phpdotenv aborted with 'unexpected whitespace'
and every replica failed to boot. Wrap both through dotenv_quote.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The KIX frontend image reads only REDIS_CACHE_HOST/PORT and ignores
REDIS_CACHE_PASSWORD, so it cannot authenticate against the shared svc-db-redis
(always requirepass + ACL) and crash-loops on NOAUTH. Pin redis.shared=false so
the engine lookup wires the embedded passwordless sidecar (host redis, no
password) that KIX's on-premise stack expects. Drop the now-static flag's
variant coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both default to node-count replicas in swarm and race on the shared NFS
volume: ejabberd holds a single-writer Mnesia store at /home/ejabberd/database,
suitecrm runs a first-boot install that collides across replicas (all tasks
killed unhealthy). Pin service_replicas=1 for each.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The redis service was switched to enabled with a dynamic shared flag but
meta/variants.yml carried no coverage, tripping the variant lint and bond
baseline checks. Mirror the mariadb pinning: variant-0 pins redis.shared true
(host baseline), variant-1 pins it false.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correct 'Birchenbach' -> 'Birkenbach' and normalise the non-breaking
hyphen (U+2011) to an ASCII hyphen in galaxy_info.author across 34
roles, so the author SPOT matches the regenerated README Credits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DiD runner ran as a separate compose service matrix-mdad while the app entity is matrix, so the swarm overlay upstream needed a service_key override to reach matrix_matrix-mdad. Name the service, container_name and entity all matrix (MATRIX_MDAD_SERVICE, MATRIX_MDAD_CONTAINER, services.matrix.name) so the overlay DNS is matrix and resolve_upstream returns matrix:8008 by default. Drop the webserver_proxy_service_key override; keep proxy_internal_port (synapse 8008, element 8080).
Also drop the now-dead matrix-mdad meta entry (resource_filter resolves to the entity, so its cpus/mem were never read), flavor-gate the volume mounts (mdad to ansible, synapse/element to compose) so the ansible flavor no longer declares compose-only volumes/configs, and have diagnostics.sh read the container name from the MATRIX_CONTAINER env instead of hardcoding it. Renderer + unit validated: swarm matrix:8008/8080, compose 127.0.0.1:8021/8022.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Role README Credits now name galaxy_info.author (plain bold or wrapped
in a Markdown link), matched byte-for-byte against the role's own
meta/main.yml, instead of a hard-coded maintainer name. Regenerate all
245 role README Credits blocks to the new 'Implemented by ...' format.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compose_replicas defaults every service to the node count (3 in the CI swarm), so espocrm, listmonk, pixelfed, mobilizon and odoo fanned out to 3 replicas that then raced on their shared NFS volume during first-boot init and crashed: espocrm 'unable to create folder data/cache', listmonk 'database does not appear to be setup', pixelfed worker waits on a .docker.init marker owned by a peer, mobilizon 'config.exs permission denied', odoo flaps on shared filestore init. These are stateful single-writer apps (and their daemon/websocket/worker sidecars are singletons by design -- multiple would duplicate cron jobs, break the fixed ZeroMQ DSN, or conflict on Horizon keys). Pin each stack to service_replicas = 1, matching the existing jenkins/erpnext/matomo pattern. Swarm-only (deploy.replicas is not emitted in compose).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The convergence gate flagged any service where running \!= desired replicas as not converged. A run-once job (restart_policy: none) such as matomo_bootstrap or erpnext_configurator runs, exits 0 and then permanently reports 0/1, so the gate false-failed the deploy even though the job succeeded. Now, for a service short of its desired count, consult docker service ps and treat it as converged only when a task reached DesiredState=Shutdown/CurrentState=Complete with no task still wanting to run; genuinely crashing (Failed) or still-starting (Running/Ready) services stay flagged. Since matomo_bootstrap is bundled into most swarm groups, this was a broad false-negative. Swarm-only; compose never invokes this script.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Under self_signed TLS the shared sso-proxy template (web-app-keycloak) now both bind-mounts the platform CA to CA_TRUST.cert_container (sso_proxy/container.yml.j2) and points provider_ca_files at it (oauth2-proxy-keycloak.cfg.j2), gated on the same condition as this task. pihole's lineinfile added a SECOND provider_ca_files (the wrapper-injected /tmp path, which is skipped for the distroless oauth2-proxy image), producing 'toml: key provider_ca_files is already defined' so the sidecar crash-looped and never converged. The task is redundant and its path less reliable than the template's explicit bind mount, so remove it and the now-unused PIHOLE_SSO_PROXY_CA_CONTAINER_PATH.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The curl healthcheck include already emits start_period, and jenkins appended a second one (10m) right after the include, producing a duplicate mapping key so 'docker compose config' failed to parse (yaml: mapping key start_period already defined). Make the curl include's start_period overridable via container_healthcheck_start_period (default 30s, so the 26 other curl-include roles are unchanged) and have jenkins set it to 10m before the include instead of appending.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With replicas>1 on shared NFS, gitea's default LevelDB queue is single-writer: only one replica gets the lock, the others crash with 'unable to lock level db at /data/gitea/queues/common: resource temporarily unavailable' and the service never converges. env.j2 already routes queue/cache/session to Redis when enabled; turn it on (mirrors the mastodon redis block) so every replica shares the Redis-backed queue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ganesha flavor (dev/act swarm test only; prod uses kernel nfsd) had no restart policy. Ganesha FSAL_VFS can SEGV under DinD load, and a single crash took down every client NFS mount and failed the swarm deploy. Install a systemd drop-in (Restart=on-failure, RestartSec=2, StartLimitIntervalSec=0) and daemon_reload on start. Validated live: kill -s SEGV then the service self-restarts (nrestarts=1) and clients recover. Compose is unaffected (no NFS).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
down_act_outer.sh is a best-effort pre-clean (set +e), but its last statement (docker rm -f) can race act's own --rm finalize and return non-zero, so the script exited non-zero and aborted swarm-zombie before the deploy. Force exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In swarm the shared openresty runs as an overlay container, so 127.0.0.1 is its own loopback, not the node's. bbb deploys node-local (host network) and its nginx binds the bbb-net gateway 10.7.7.1, a host IP the overlay reaches via inter-bridge routing. resolve_upstream now honors an opt-in services.<entity>.upstream_host in the compose branch (default 127.0.0.1) and bbb sets 10.7.7.1; proxy_pass collapses compose_mode_force apps to the static compose form. Compose keeps working because host-network openresty reaches 10.7.7.1 too. Validated live: curl bbb.talk/b/ returned 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The linked article returns HTTP 404 and failed the external URL
reachability test; it was an off-topic generic-linux resource anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The seeded local admin is now the shared-model user
web-app-semaphore-breakglass (login breakglass-administrator, password =
ansible_become_password, accounts: [] so it is never provisioned to
LDAP/Keycloak, forward: administrator so its mail lands in the administrator
mailbox). SEMAPHORE_ADMIN_* read that user via lookup('users'); README and
variant table updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to the accounts model: the rbac role defined by web-app-mailu is
now 'bot'; update every meta/users.yml role list that referenced mail-bot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Model change: per-user 'reserved: true|false' becomes 'accounts', a list of
host/mailbox/identity account types. Default when absent derives from roles
(bot/administrator -> [mailbox, identity], else [identity]) both for role-file
users and — via _apply_account_defaults after the inventory merge — for
inventory-only users, so users like biber keep their LDAP/Keycloak accounts.
Auto-generated role-suffix reservations get accounts: [].
New 'forward' field: mailu creates <username>@domain -> forward user's email
via the new 04b_manage_alias.yml (create-only; no_log on the loop so user
dicts never hit the log). Mailbox creation and password rotation now gate on
'mailbox' in accounts instead of role names; the mailu rbac role is renamed
mail-bot -> bot (token gate included).
svc-db-openldap drops the provision.reserved flag; OPENLDAP_USERS is always
the identity subset. reserved_usernames/non_reserved_users now classify by
'identity' in accounts, keeping the Keycloak deny-list semantics. root/backup
pin accounts: [host], administrator [host, mailbox, identity], helpdesk
[mailbox].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BBB runs the external bigbluebutton/docker project (host networking,
node-local stateful stack) and cannot be a docker stack deploy service.
Add a per-role compose_mode override so it deploys via node-local
compose up on STACK_HOST even inside a swarm cluster:
- sys-svc-compose: compose_mode_force (per-role override) + computed
compose_mode (SPOT) in defaults; up.yml + handlers route on
compose_mode instead of DEPLOYMENT_MODE.
- container_address: honour compose_mode_force so a node-local role's
exec targets its compose container (bare name), not a swarm service.
- web-app-bigbluebutton: compose_mode_force=compose, ungate compose
generation for swarm, flush handlers in-scope, and add
services.bigbluebutton.ports.internal.http for the swarm proxy upstream.
- web-app-matrix: add services.element.ports.internal.http (swarm proxy).
- test_variant_bundles: pin compose mode in the main test so it is not
polluted by another test leaking INFINITO_DEPLOY_MODE=swarm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The complexity report orders roles coverage-first and caps them with --max-jobs; that order must drive selection. Once selected, re-sort the surviving roles alphabetically so the swarm and compose deploy matrices list jobs in a stable ascending order. The storage filter and variant_bundles both preserve input order, so the final matrix stays sorted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swarm can place collabora on any node, but its deploy-time coolconfig
runs container exec on STACK_HOST (the manager); resolve-container-id
only finds the container locally when it runs on the manager. Pin the
role via default_placement so the exec reaches the task (same pattern as
svc-db-postgres). Single-instance office backend, so no scaling is lost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ansible-lint --fix exits 8 (RC.FIXED_VIOLATIONS) when it writes an auto-fix with zero failures remaining -- a normal outcome, same class as 0 (clean) and 2 (violations remain that --fix could not auto-resolve, same as the markdownlint/eslint rc<=1 pattern in this file). Previously only 0/2 were accepted, so a successful auto-fix could FAIL the whole autoformat run.
Also delete narration comments across the file that only restated the adjacent code; keep the two comments that flag a real trip-wire (status_dir crash-avoidance, write_status param doc).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three folded 'when:/failed_when: >- {{ ... }}' conditionals (mediawiki, joomla, sys-front-tls-selfsigned) triggered the ansible-core 'Conditionals should not be surrounded by templating delimiters' deprecation, which fails the deploy deprecation gate. Drop the delimiters (a folded conditional is already a bare Jinja expression).
Extend two lints so this cannot regress and does not misfire: test_no_templated_conditionals now catches folded/block scalars (was inline-only); test_vars_usage_in_yaml now scans failed_when/until/changed_when for var usage (was when/loop/with_* only), so bare-expression conditionals no longer make their vars look unused.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The converge-failure diagnostics truncated service logs to --tail 50 (and head -100), but a crashing service prints its exception at the TOP of the trace, so the root cause was cut off and non-converging services could not be debugged from CI. Remove the caps in the converge-failure rescue (wait_converged.yml) and the diagnostics collector (12_collect_diagnostics.sh) so the full log, including the exception, is captured.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On swarm a 'type: bind' of a host-rendered file exists only on the node that rendered it; other nodes reject the service ('invalid mount config: bind source path does not exist') so it never converges. Convert the single-file binds to 'type: config' (swarm-distributed, content-hashed name rotates on change), mirroring the working nginx_conf sibling: moodle+wordpress msmtprc (0444), decidim allowed_hosts.rb (0444), bluesky pdsadmin (0555, executable).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docker rm -f on a node stuck on a dead NFS mount races (did not receive
an exit event) and leaves the container stopped but not removed. Add a
SIGKILL + plain docker rm retry loop before the host-docker-restart
fallback, so make swarm-clean self-recovers without sudo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mastodon/mastodon:latest floated to a Rails 8.1 major whose boot contract changed (fatal without a VAPID key), breaking the swarm deploy without any change on our side. Pin server and streaming to v4.6.2 so the boot contract can't shift under us again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mastodon on the current mastodon/mastodon:latest (Ruby 4.0 / Rails 8.1.3) aborts at Zeitwerk eager_load_all (task: non-zero exit 1) without a usable VAPID key. The two keys were declared 'plain', so under CI allow-empty-plain they render empty and the web+sidekiq services never converge. Omitting the env vars (0fc5408db) was not enough: empty and absent both crash identically, so a valid key must be present. Add 'vapid_private'/'vapid_public' credential algorithms that emit one linked P-256 keypair (private = unpadded base64url 32-byte scalar, public = unpadded base64url uncompressed point 0x04||X||Y), wire the mastodon schema to them, and render both keys unconditionally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fresh, un-installed Matomo serves 302 on /index.php (redirect to the web installer), but the healthcheck demanded exactly 200, so it never became healthy: swarm reschedule-looped the container (0/1) and the bootstrap replicas piled up and raced on the database, so the installer never completed (rc=1) across every matomo-dependent bundle. Accept any 2xx/3xx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Catch the ansible-core 'conditionals surrounded by templating delimiters' deprecation at lint time. Excludes meta/{volumes,networks,services}.yml filter-when fields and templates; suppress with # nocheck: templated-conditional.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A per-item boolean guard carried as a data field named 'when' must hold {{ EXPR }} so the consumer can template it to a real bool; but ansible-core 2.21+ then flags 'conditionals surrounded by templating delimiters'. Rename the data field to 'condition' (a data field may legitimately hold {{ }}) so no genuine conditional carries {{ }}. Also wraps the four previously-bare listmonk entries, which were silently always-False, so those settings now apply when enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-variant app expanded into N swarm matrix entries, and
swarm-zombie ran them all on one host under identical SWARM_NAME
cluster names, so every deploy after the first aborted with
'network ... already exists'. Pin the act matrix to variant 0
(default, overridable via variant=) so a multi-variant app validates
a single cluster, in parity with the compose step's variant=0.
workflow.sh now splits ACT_MATRIX on ';' into multiple --matrix
filters so apps and variant can be pinned together.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The curl healthcheck template lacked a start_period (unlike the http/nc ones), so slow-starting containers were marked unhealthy and rescheduled during boot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A first XWiki boot on a fresh volume under CI contention exceeds the 300s readiness budget (connection refused the whole window). Double retries to give the JVM time to open the HTTP connector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With LDAP on, the submit button is 'LDAP' not 'Login', so the native password-login e2e could never find it. Skip it under ldap, as it already skips under sso.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The logo/favicon settings gated on a URL string via | bool (always False), so branding was silently dropped. Use a length check. Note S3 object storage as a pending TODO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.env files do not strip trailing inline comments, so INSTALL_LOCK became the literal string 'true # Locks the installation page' and db migrations failed MustInstalled(). Put the comment on its own line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
moodle renders the msmtp template which needs sys_svc_mail_msmtp_ca_bundle, a set_fact from sys-svc-mail-msmtp; when that role had not run in the play the fact was undefined and the render failed. Include the role when the fact is missing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>