The Playwright bootstrap installer (ghcr.io/kevinveenbirkenbach/matomo-bootstrap:1.1.13, the latest tag) scripts the install wizard against matomo 5.3.2 (its e2e compose). The role ran matomo:latest (currently 5.11.2), whose first-website wizard step changed, so the installer timed out ('first website form submit did not progress to tracking code') after 7 retries and failed the deploy. Pin matomo to 5.3.2 to stop the :latest drift.
Verified: a fresh matomo deploy completes, the bootstrap obtains its token with no installer timeout, and the matomo admin Playwright spec passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the spec runs during a full variant-3/4 deploy the host is under heavy provisioning load, so the Discourse Vue components render slowly and the cumulative test exceeded its 120s budget at the instance-address field. Raise the overall timeout to 240s and the URL-field and connect waits to 60s. Idle the spec passes in ~24s; this only adds margin for the loaded path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shared OAuth-credential writer stored the RAW value via IAppConfig::setValueString(..., sensitive: true), but the integration apps (gitlab/openproject/suitecrm/zammad) read back via getValueString(lazy) + ICrypto->decrypt and expect an ICrypto ciphertext. The mismatch made decrypt throw 'Authenticated ciphertext could not be decoded' while rendering the GitLab Personal section, which 500'd the shared connected-accounts page and cascaded into the integration_discourse spec timing out on the missing #discourse_prefs mount. Encrypt each value with ICrypto and store it lazy + non-sensitive, mirroring the apps' own setEncryptedAppValue.
Verified in-container: getAdminClientId() now succeeds, a fresh variant-3 provision no longer returns a 500, and the integration_discourse spec passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chaos phase (09 seed -> 10 drain -> 11 assert) assumes the app runs on a
worker it can drain. A role declaring `default_placement: manager` (single-node
DBs, the edge proxy, ...) only ever runs on the manager, so
09_seed_content.sh's `NODE=$(... | grep -vx "$MGR" | head -1)` matched nothing
and, under `set -o pipefail`, killed the script with a silent exit 1 before its
own `[ -z NODE ]` handler could report it.
_context.sh now derives DEFAULT_PLACEMENT_MANAGER from the role's
meta/services.yml and exposes skip_chaos_if_manager_pinned(); 09/10/11 call it
and exit 0 (chaos not applicable) for those roles. 09 also appends `|| true` to
the NODE pipeline so a genuinely-absent worker task on a non-pinned role reaches
the honest failure handler instead of dying silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the document-start marker and indent the canonical-domains list entry to
match the project's yamllint profile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/meta/resolve/apps.sh reads INFINITO_DEPLOY_MODE (added with the
per-mode skip feature) but the key had no registry entry, failing
tests.lint.filesystem.env.test_used_keys_declared. Add an empty default
to default.env and the key to the static-passthrough STATIC_KEYS so the
CI workflows' compose/swarm value passes through; empty disables the
per-mode skip filtering locally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the empty-config compose deploy failure (assert
BACKUP_TO_USB_MOUNT != "") with a working test path:
- meta/variants.yml: pin non-empty mount/target/source so the CI matrix
(which applies variants) deploys the plug-triggered backup service. No
device appears in CI, so the .mount unit only waits and the oneshot
backup never fires.
- tasks/main.yml: fix the assert typo BACKUP_TO_USB_target -> ..._TARGET.
The lowercase name was undefined, so the assert would still error even
with the variant config present.
- vars/main.yml: derive entity_name via get_entity_name and build the
config lookup path from it instead of hardcoding 'local-2-device'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the `2>/dev/null || true` that swallowed the roundtrip skip-check (a failed
get_role_skip silently became "no skip"), and replace `${PYTHON:-python3}` with
strict `${PYTHON}` in roundtrip.sh and run.sh. Both source
scripts/meta/env/load.sh (which sources python.sh, always setting PYTHON or
failing) before the usage, so the fallback was dead code that only masked a
PYTHON-unset setup error as a confusing system-python failure. Now such a
failure aborts loudly under set -euo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `skip: [compose, swarm]` field on a role's meta/services.yml primary entity
so an app can opt out of the test-deploy matrix per deploy mode, replacing the
hardcoded exclude regex in apps.sh (the "TODO: Remove exclude condition"). The
filter threads through both discovery paths: the CI workflows pass
INFINITO_DEPLOY_MODE -> apps.sh -> `cli meta roles applications type
--skip-mode`, and roundtrip.sh honours the same get_role_skip SPOT per app/mode.
Apply skip: [compose, swarm] to svc-storage-nfs-server and svc-storage-nfs-client
(NFS storage is swarm-only and set up via the cluster inventory, not the matrix)
and web-opt-rdr-www (the previously regex-excluded app).
Covered by get_role_skip and skip_mode-filter unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve pihole meta conflicts toward the branch: keep the bond:1 service flags and the YAML normalization; merged main carried no unique pihole content (only pre-normalization formatting and the absence of the bond lines). bookwyrm meta auto-merged from main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Playwright native-login helper clicked the first match of a broad 'form button[type=submit]' selector, which on the website login page non-deterministically resolved to the navbar search button (oe_search_button, type=submit) instead of the Log in button — an unstable element the click waited on until the 180s timeout, making the admin-login addon specs (account/contacts/crm/project/sale_management) flaky. Scope the selector to the login form (form:has(input[name=login])) and target the Log in button by role (getByRole button, name /log in/i).
Diagnosed from the CI error-context page snapshot (navbar search button before the login form) and the run's flaky pass/fail pattern with all failures at the login step; not yet re-run against a live Odoo deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gitlab/openproject/suitecrm/zammad integration tasks gated their partner-running check on 'container exec <partner> true' (rc==0), which returns rc 127 on partner images lacking a true binary, wrongly skipping the integration. Switch to 'container inspect -f {{.State.Running}}' gated on stdout-trimmed == 'true' — the same probe already shipped for the mattermost integration in 8023e4b8b. inspect never enters the container, so the in-container PATH lookup that broke the old probe is irrelevant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nextcloud->mattermost OAuth provisioning failed because the partner-running probe used 'container exec <mm> true' (rc 127 — no true binary in the mattermost image), which gated provisioning off and left the OAuth app unprovisioned (spec failed on oauth_instance_url=null). Replace it with 'container inspect -f {{.State.Running}}' gated on stdout-trimmed == true, provision the OAuth app via a controller-run script using a short-lived admin PAT, and rewrite the spec as a behavioral connect-redirect check.
Mattermost personal access tokens are now enabled only when the consumer web-app-nextcloud is deployed (services.mattermost.user_access_token_enabled renders from 'web-app-nextcloud' in group_names) instead of being hardcoded on.
Verified: probe live in the DiD stack (inspect running -> rc0, missing -> graceful skip); the user-access-token chain renders True/False via a real Templar (nextcloud present -> True, absent -> False); service-flag integration tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every test-deploy-swarm matrix job died at 01_start_nfs.sh with
`INFINITO_SWARM_TEST_LABEL: unbound variable`: the label (consumed by
01_start_nfs.sh, 02_start_swarm_nodes.sh and clean_all.sh) was added to
default.env but never sourced by 00_topology.sh nor exported to the job env.
Broaden the 00_topology.sh source filter to `^INFINITO_SWARM_[A-Z0-9_]+=` (every
swarm key, future-proof) and export INFINITO_SWARM_TEST_LABEL from the
"Load swarm topology SPOT into job env" step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Legacy mautrix-python facebook/instagram bridges are EOL and fail at runtime (appservice bot masquerade 403). Replace both with the bridgev2 mautrix-meta addon: config, schema credential, variant-2 wiring, and a profile spec with a positive @metabot probe plus an un-bridged-MXID negative control.
playwright.env.j2 now emits MAUTRIX_<bridge>_ADDON_ENABLED only for bridges that are actually deployed, so each bridge spec runs iff its bridge is up (closes the skip-as-green gap) and is telegram-drop aware.
Verified: compose variant-2 deploy failed=0; @metabot registers on the live Synapse; bridge suite green (meta/signal/whatsapp/slack), telegram skips (no API creds).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-recorded while a research agent fetched deepwiki.com; added in sorted
position in permissions.allow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure formatter pass, no behaviour change: add the `---` document marker, indent
the run_after list item, normalize a quote style and drop over-aligned key
padding in 01_core.yml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-existing test-lint failures surfaced while greening the suite:
- default.env: move the INFINITO_SWARM_* block into ASC order (it sat before
INFINITO_LIMIT/LINT instead of after INFINITO_SUBNET) and trim the
INFINITO_SWARM_TEST_LABEL comment under the 140-char cap.
- Makefile: order act-swarm-clean before act-swarm-down and fold the
act-swarm-clean comment block to one description line plus Note: markers.
- scripts/tests/deploy/swarm/clean_all.sh: add the executable bit.
- tests/unit/plugins/filter/test_ruby.py: autoformat reflow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two pre-existing ruff findings in the complexity reporter (from an earlier
refactor) blocked `make autoformat` / `make test`: model.py used a bare
hashlib.sha1 for a non-security cache key (S324) and render.py built a list with
a for-append loop (PERF401). Pass usedforsecurity=False and switch to
list.extend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The compose-flavour realm-import file runs `compose stop/run/up` and chdirs into
directories.instance, but it is only ever included via the DEPLOYMENT_MODE-driven
`05_realm_import/{{ DEPLOYMENT_MODE }}.yml` switch, so those compose-only calls
never reach a swarm host. Add the
`# include-gated: when: DEPLOYMENT_MODE == "compose"` file-header marker so the
compose-verb / compose-chdir lints treat the file as exempt; it was missing since
the 05_realm_import flavour split.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DEPLOYMENT_MODE is binary (swarm / compose), so a negated `!= 'swarm'` gate
always means `== 'compose'`. Replace every negated gate across roles/ and
group_vars with the positive form for readability, and add a lint
(tests/lint/ansible/variables/test_no_deployment_mode_negation.py) that forbids
`DEPLOYMENT_MODE !=` in .yml/.yaml/.j2 with a `# nocheck:
deployment-mode-negation` escape.
The compose-verb / chdir / jinja-gate lint guidance messages now suggest the
positive form too; the gate matcher (utils/annotations/task_gate.py) and its
test inputs keep recognising the legacy `!= 'swarm'` so a nocheck-marked
exception is still detected as compose-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A swarm docker stack deploy env-file value must be a single physical line; the
multi-line GITLAB_OMNIBUS_CONFIG was read as separate whitespace-named env
entries and aborted the deploy. Add a depth-aware ruby_single_line filter that
collapses the Ruby program to one line (statement-boundary newlines at bracket
depth 0 become ';', newlines inside an open bracket or a string become a space
so hash/array literals like the OIDC omniauth_providers stay valid) and apply it
on the swarm branch only; compose keeps the multi-line value unchanged.
Covered by tests/unit/plugins/filter/test_ruby.py (10 cases incl. a realistic
omnibus config, escaped quotes and nested literals).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm the default replicas=node-count made erpnext's 7 services overflow its
per-stack /28 overlay (16 IPs), so swarm rejected tasks with "ip addresses may
be exhausted". Rather than pin everything to one replica, let the stateless
services scale and size the overlay to fit:
- backend (gunicorn), queue-short/queue-long (RQ workers) and the nginx frontend
scale to node-count: state lives in MariaDB + Redis, jobs are claimed
atomically from Redis, and swarm reaches each service via its overlay VIP (the
published port is ingress, so DOCKER_BIND_HOST binding does not pin them).
- websocket (no socket.io redis adapter, needs sticky sessions), scheduler
(singleton, else duplicate scheduled jobs) and configurator (one-shot) stay at
one replica via a single service_replicas = 1 above those blocks.
- widen the erpnext overlay from the packed 192.168.105.176/28 to a fresh
192.168.214.0/24 (4 scalable x node-count + 3 singletons + VIPs exceed a /28).
Also relocate the configurator to the end of the template: its
{% set docker_restart_policy = 'no' %} is template-scoped and was leaking
restart:no / swarm condition:none onto every long-running service. With it last
the one-shot policy stays scoped to the configurator; depends_on is by name so
file order is safe, and compose is unaffected (compose_replicas emits nothing in
compose mode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm the bluesky api/web/view vhosts proxied to the wrong upstream: the
front proxy used the app entity name as the service key and resolve_upstream
hard-failed without an internal port (bluesky has no
services.bluesky.ports.internal). Pass the swarm service keys (pds /
bluesky-broker / view) and the internal ports (added to
services.{api,web,view,broker}) per mode, and freeze + re-pass
proxy_internal_port in sys-stk-front-proxy so it survives the nested role loads
and reaches html.conf.j2 (mirrors the existing front_proxy_service_key freeze).
The sys-stk-front-proxy change is compose-neutral (resolve_upstream ignores
internal_port in compose) and a no-op for the 9 non-passing apps (internal_port
stays ''); it also co-fixes web-svc-coturn, whose deliberate
proxy_internal_port: 80 was being lost (coturn has no
services.coturn.ports.internal, so swarm resolve_upstream hard-failed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The setup execs ran 'container exec {{ DECIDIM_CONTAINER }}' (a compose-only
container name) and probed health via community.docker.docker_container_info,
which returns undefined in swarm and broke the until-expr with "object of type
has no len()". Resolve the hosting node + the local container ID per mode
(01_setup/{swarm,compose}.yml, selected by a DEPLOYMENT_MODE-driven include) and
run every exec/inspect delegated to that node against the resolved CID, mirroring
the web-app-matomo resolve pattern. The six rails steps
(migrate/seed/org-host/admin-user/oidc) are logically unchanged; only the
container targeting moved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shared-DB 'container network connect' handler attaches discourse to the DB's local container by name, which only exists in compose. In swarm the DB is a service reachable via its overlay alias, so the by-name connect aborts. Gate the handler with DEPLOYMENT_MODE == 'compose'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
05_realm_import.yml becomes a thin orchestrator: probe whether the realm exists, then (when import is enabled and the realm is missing) include the per-mode body 05_realm_import/{{ DEPLOYMENT_MODE }}.yml and re-run 04_login. The compose path (stop/import/restart) moves verbatim to 05_realm_import/compose.yml; the swarm scale path moves to 05_realm_import/swarm.yml (was 05b_realm_import_swarm.yml); 05a_realm_import_run.yml is absorbed and removed. Pure structural move, no behavior change; ansible-lint clean (production profile).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The role cached keycloak's swarm container ID into KEYCLOAK_LOCAL_CID and reused it for every 'container exec <CID> kcadm.sh'. In swarm the realm-import scale-up reschedules keycloak, so the cached CID dies mid-block; the next kcadm exec aborts with 'No such container' and the module falls into the create branch, surfacing as 'Failed to create Keycloak object' on Update Client settings (15 swarm apps).
Make kcadm resilient: keycloak_kcadm_update.py re-resolves a fresh healthy CID via the swarm service label and retries on 'No such container' (bounded). Quiesce churn first: 05b waits for replicas X/X stable across 3 consecutive polls before re-resolving (stability poll extracted to files/wait_replicas_stable.sh). Add --filter health=healthy to CID resolution in 04_login and 05b. The 26.6.3 pin is unrelated. Swarm-gated; compose runs the module exactly once as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swarm bootstrap fix: the single 'container service update --force' plus one-shot log-poll aborted on a flaky installer (restart_policy:no never re-runs it). Merge force-update and token-poll into one retried block (each attempt re-forces a fresh installer task, 2-3 attempts); the poll loop lives in files/bootstrap_swarm_poll.sh (script module, vars passed as env).
Restructure: split the bootstrap into mode flavors (05_bootstrap/flavors/{swarm,compose}.yml) and per-mode debug (05_bootstrap/debug/{swarm,compose}.yml), both selected by one DEPLOYMENT_MODE-driven include; renumber 07-10 to 06-09. Swarm-only behavior changes; compose paths unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm the coolconfig post-deploy exec fired while the task was still Preparing (the shared-service deferred flush skips the convergence gate), so the container id resolved empty and container exec failed with 'invalid container name or ID: value is empty'. Add a swarm-gated wait on the task reaching Running before the exec, mirroring web-app-matomo/tasks/10_resolve_host_cid.yml. Compose unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Delete the per-deploy-type workflows (test-deploy-local/universal/
workstation) and the redundant act-all/act-app runner scripts (replaced
by make act-workflow). Only compose and swarm deploy tests remain.
Both now discover the full application matrix (every deployment type,
lifecycle-filtered to alpha/beta/rc/stable) instead of just the server/
web-* scope, via apps.sh resolving applications.type without --type.
Retire INFINITO_DEPLOY_TYPE entirely: discovery is now unconditional and
the deploy step never read it. Removed from apps.sh, ci/all.sh, the local
deploy scripts, the two workflows, default.env, the env passthrough list,
the dotenv generator test, the Makefile compose-deploy target, and docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unbounded `page.waitForLoadState("networkidle")` in the shared admin/seaweedfs
personas and the dashboard simpleicons-card test waited the long default before
the existing `.catch` swallowed it, so apps that never go network-idle
(websocket/long-poll: mailu admin, matomo) made the Playwright run exceed its
1200s task timeout. The result was already discarded, so capping each wait at
15s only bounds it; it matches the timeout already used elsewhere
(web-app-mailu playwright.spec.js, pihole, bluesky).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The compose template published the resolver on {{ DOCKER_BIND_HOST }}:53. Under
DinD DOCKER_BIND_HOST is 0.0.0.0 and the host already binds :53 (its own
systemd-resolved), so `compose up` failed with "address already in use". Since
5dad81d3f made svc-dns-unbound required, unbound now deploys in every
mailu-bearing compose stack, turning that latent clash into the dominant
compose CI failure. Consumers reach unbound at UNBOUND_RESOLVER_IP on the
overlay and the readiness probe runs container-internal, so the host publish is
unused under DinD: gate it to non-DinD hosts only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the single complexity __main__.py into graph (discovery + BFS),
model (ComplexityRow + scoring), render (table/json/string) and cli
(argparse + main); __main__ stays a thin entry point.
Add a 'base' column (sha1 of the role's name unioned with its embedded
services, sorted) so roles in the same mutual-embedding cluster share a
key, and a 'siblings' column listing the rest of that cluster. --unique
keeps one role per base. roundtrip's default app list now passes
--unique to skip redundant same-cluster deploys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hoist the swarm-test node, network and NFS-export-base names out of the shell
harness (00_topology.sh) into default.env (INFINITO_SWARM_*), so the shell
harness and the Python harness (extend_inventory, write_extras) read one source
instead of each hardcoding the suffixes. 00_topology.sh, extend_inventory.py,
write_extras.py and 01/09/11 now read the SPOT; passthrough.py exports the keys
into the generated .env. 00_topology.sh reads default.env directly (not .env)
because the workflow sources it before make dotenv runs.
Rename topology.sh -> 00_topology.sh (sorts ahead of 01_/02_, the order it is
sourced in) and update all 10 references.
Add scripts/tests/deploy/swarm/clean_all.sh + make act-swarm-clean to reclaim
wedged act-swarm state between runs, label-scoped via INFINITO_SWARM_TEST_LABEL
so it never touches unrelated containers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>