Hard-assert the full Mastodon bridge when enabled: the admin panel shows the
provisioned oauth_instance_url pointing at the deployed partner host (distinct
from Nextcloud) and the per-user Connect-to-Mastodon drives the OAuth handoff to
the partner /oauth/authorize with client_id + response_type=code. The bridges:
[mastodon] declaration lets the deployment gate skip it cleanly when the partner
role is not deployed (as in the standard set) even though the NC app is enabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the per-user Connect-to-Jira-Cloud control renders (absence now fails)
and clicking it drives to the partner auth.atlassian.com /authorize carrying the
provisioned client_id + response_type=code, host distinct from Nextcloud. Like
google, the addon self-gates on credentials (meta enabled true only when api
jira.client_id/client_secret are set), so it runs only with real Atlassian OAuth
creds and skips cleanly otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the per-user Sign-in-with-Google control renders (absence now fails) and
clicking it hands off to the partner accounts.google.com /o/oauth2/v2/auth
carrying the provisioned client_id + response_type=code, host distinct from
Nextcloud. The addon self-gates on credentials: meta enabled is true only when
api google.client_id/client_secret are both set, so the spec runs only when real
Google OAuth creds are configured and skips cleanly otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert the per-user Connect-to-GitHub control renders (absence now fails, not
skips) and clicking it hands the top frame to the partner github.com
/login/oauth/authorize carrying the provisioned client_id, with the partner host
distinct from Nextcloud and redirect_uri pointing back at this instance.
response_type is intentionally not asserted (upstream integration_github does
not emit it for GitHub's OAuth app flow).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive the real cross-app User-API-Key handoff and assert it reaches the partner
Discourse, after confirming the integration_discourse Vue app actually booted —
not just that a config value exists. Verified live: integration_discourse 3.2.0
enabled with the partner url coupled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- meta: required true -> false; upstream julien-nc/integration_suitecrm needs
Nextcloud server major <= 24 and never enables on the deployed newer major, so
the addon gate skips its spec cleanly instead of asserting a surface that can
never render
- spec: drop the DOM-gated skips; assert the personal settings panel and the
#suitecrm-content OAuth section render and the instance URL points at the
partner host (clean once a compatible app version ships)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ensure_nextcloud_storage.rb: resolve the storage creator via
User.where(admin: true) with a respond_to?(:active) guard, since OpenProject 17
removed User::STATUSES (the prior .active.first path raised NameError)
- integration_openproject_provision.yml: parse the OAuth payload with
regex_search('{.*}') instead of trim, so polluted runner stdout still yields
valid JSON
- spec: assert the provisioned OAuth client is shown in admin (both sides
coupled) and the per-user connect drives the partner /oauth/authorize with the
client_id + response_type=code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provision a GitLab Doorkeeper OAuth application for Nextcloud and wire the
credentials into the integration_gitlab app so the per-user Connect-to-GitLab
flow reaches the partner authorize endpoint.
- provision_oauth_app.rb: create/renew the Doorkeeper app (organization_id set
to satisfy the newer GitLab oauth_applications check constraint), emit
CLIENT_ID/CLIENT_SECRET
- integration_gitlab_provision.yml: run it via gitlab-rails, parse the creds,
store them with occ config:app:set --sensitive (the app stores them encrypted)
- integration_gitlab.yml: probe the partner, read persisted creds, provision
only when the partner is up and not yet coupled
- spec: drive the real per-user OAuth connect and assert the partner
/oauth/authorize carries the provisioned client_id + response_type=code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- loginToStandaloneNextcloud signed-in-shell wait 60s -> 120s so cold
first-deploys do not time out mid-login
- export moodleBaseUrl from _shared and render MOODLE_BASE_URL /
DISCOURSE_BASE_URL (service-gated) for the moodle/discourse integration specs
- drop NEXTCLOUD_RICHDOCUMENTS_EXPECTED_WOPI_URL (richdocuments spec no longer
pins the WOPI host)
- strip narrative comments
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A bridged addon's <ID>_ADDON_ENABLED flag is now true only when one of its
declared bridge partners is in the deployed playwright role set
(TEST_E2E_PLAYWRIGHT_APPS): integration specs skip cleanly when the partner is
not deployed and assert the real coupling when it is. App-addons without
bridges are unaffected; an unresolvable set falls back to the prior
enabled-AND-required behaviour (no regression).
Adds TestBridgeDeploymentGating (7 cases): partner-deployed, not-deployed,
multi-bridge any-match, no-bridge passthrough, disabled stays false, fallback
without the set, and string-set splitting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
loginToOdoo treated the OAuth round-trip's URL as proof of login, but the implicit-token round-trip can land back on Odoo without an established session (false positive), and an existing Keycloak realm session bounces straight to an authenticated Odoo without ever showing the login form. Rework: race the issuer URL against the web-client locator, fill the Keycloak form only when it actually appears, then confirm the authenticated desk by waiting for the web client to render (waitFor — the non-waiting isVisible check raced the SPA bootstrap and flaked). Retry twice, then fail loudly. Verified in-container: odoo 10 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The universal-logout helper clicked the account-menu trigger and waited only 750ms for the dropdown to render its items. OpenProject's Angular SPA renders them slower under deploy load, so the logout control was missed (flaky biber universal-logout, recovered only on retry). Bump the post-click wait to 1500ms. Verified in-container: both universal-logout specs pass, no regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the erpnext primary entry enabled+shared so other roles can declare it as a dependency (erpnext: {enabled, shared}); the service registry now resolves it to web-app-erpnext, matching the web-app-matomo provider pattern.
Drop svc-db-mariadb from run_after — it crossed deploy types (server -> universal), which the service-loader rejects once the entry becomes a provider. The MariaDB dependency and ordering are already handled by the mariadb: shared service flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inner-loop rerunner copied only top-level *.js and personas/, never the role's addons/ (or fixtures/) tree, so addon-spec edits were invisible and every rerun silently used the stale deploy-time addon specs. Mirror the deploy-time stager so addon coverage reruns against the edited repo copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Odoo bootstraps a per-module web-client asset bundle on first access; under serial Playwright load the first render can exceed a minute. openModule now waits up to 120s for the app shell (90s for navigation) instead of 60s, so the module-UI coupling assertion is not a false negative on a busy instance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addon specs assert the integration is genuinely wired (admin config holds the partner value, host distinct from the app), not just that a page loads. Robust to the live DOM: strict-mode-safe locators (.first()), enabled-signal from the app's own panel (not the lazy apps list), and test.skip (not fail) when an app is disabled (NC-version-incompatible) or its partner role is absent/unconfigured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds playwright.env.j2 for mediawiki/pretix/xwiki (admin/biber creds, SSO/LDAP/service gates, addon_env_flags) and extends nextcloud's env with INTEGRATION_OPENAI_EXPECT_API_KEY and NEXTCLOUD_ONLYOFFICE_EXPECTED_DOCUMENT_SERVER_URL. services.yml nocheck flags for the partner DB/CSS services those roles expose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lint asserting that any addon whose activation is coupled to a service (services.*.enabled or 'in group_names') ships a tasks/addons/<id>.yml hook (exception logic or a documented no-exception comment).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tasks/addons hooks for the SSO/LDAP-coupled addons of the remaining web-app roles (exception-only; SPOT comments where the generic config + shared OIDC SPOT fully wire it). web-app-friendica gains the per-addon hook include in its 04_addons loader.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-addon tasks/addons hooks contain only exception logic beyond the generic loader (DRY/SPOT): config-complete/per-user integrations are documented SPOT comments; mattermost/openai/openproject/suitecrm perform genuine bidirectional OAuth/API provisioning (register client on partner, write client_id/secret back) via files/{mattermost,openproject,suitecrm} scripts, gated on the partner container running. meta/addons keys for gitlab/mastodon/matrix/peertube/zammad fixed from the inert url to the admin oauth_instance_url/instances the apps actually read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
occ config:app:set serialised non-mapping sequences via | string (Python repr), so list/dict configvalues like xwiki instances persisted as invalid JSON and the consumer fell back to empty. Encode any non-string sequence as JSON too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add plugins/lookup/playwright_workers.py computing a conservative parallel-worker count (CPU/RAM-derived, app-under-test concurrency-capped, reduced on CI) and wire test_e2e_playwright_workers + test_e2e_playwright_fully_parallel + test_e2e_playwright_test_timeout into playwright.config.js (read from env) and 02_run_one.yml (passed via -e). fullyParallel stays false so parallelism is file-level only and intra-file ordering assumptions stay safe. Raise run_timeout to 3600 for addon-heavy roles (nextcloud ~50 specs).
Split the role's variables by Ansible precedence and intent: derived/fixed constants live in vars/main.yml (UPPERCASE, high precedence, not operator-tuned), operator-tunable knobs live in defaults/main.yml (lowercase, overridable). VARIANT_INDEX stays UPPERCASE with a single definition. Update references across tasks/templates/inventory/docs accordingly. Includes a unit test for playwright_workers and an autoformat pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the per-template Jinja loop (which mis-unpacked dict2items and rendered every flag as KEY_ADDON_ENABLED=false, silently skipping every addon spec) with a single lookup('addon_env_flags', application_id). The plugin is the single source of truth for the <ADDON_ID>_ADDON_ENABLED flags consumed by tests/addon-gating.js: env-key derivation matches addon-gating.js exactly, and a flag is true only when the addon is both enabled AND required (required:false addons skip). Switch all six role templates to the lookup, extend the env-file schema lint to recognise whole-line {{ ... }} expressions, and add the plugin unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
07_patch_database execs into the Matomo container to sed its config.ini.php. Two swarm fixes: (1) target MATOMO_LOCAL_CID (resolved via container ps) instead of container_address, whose swarm form expands to the manager-only /usr/bin/resolve-container-id that is absent on the worker MATOMO_HOST_NODE this task delegates to (No such file); same nocheck pattern as 04. (2) sed -i --user root re-owns config.ini.php to root:root, so Matomo's www-data web user can no longer write it and the config API returns 500 (config not writable); chown it back to www-data after patching. The unused MATOMO_APP_CONTAINER_ADDRESS var is dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sys-stk-front-proxy applies injections before it deploys the vhost, and in swarm a tracking consumer can be provisioned before Matomo serves its own vhost (provider/consumer ordering). Probing Matomo therefore must not hard-fail the whole deploy: probe with failed_when false, and when Matomo is not reachable disable the Matomo snippet for that domain and continue (reapplied on a later pass once Matomo is up). The site-registration and tracking-injection body is moved to inject_apply.yml, included only when Matomo answers 200, so the reachable path is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
File requirement 028 for a self-hosted web-app-atmo role (upstream
flo-bit/atmo-events) that reuses web-app-bluesky for AT Protocol
identity, plus a one-way AT Protocol -> ActivityPub event bridge to
web-app-mobilizon. Index it in the requirements README and allow
WebFetch(atmo.rsvp), used while researching the upstream project.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In swarm the in-stack bootstrap service is deployed with the stack and races Matomo: it probes http://matomo, accepts the early 404 as reachable and drives the Playwright installer against a not-yet-ready Matomo, so the installer UI never becomes interactive and no token is emitted. Wait for Matomo to be healthy, force a fresh bootstrap task so the installer runs against a 200-serving Matomo, and strip the per-line service-log prefix before matching the token. Swarm path extracted into 05_bootstrap_swarm.yml, included only when DEPLOYMENT_MODE == swarm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recent Docker tightened docker cp tar extraction, so act aborts at job setup when the stock catthehacker runner image's /var/run symlink is unpacked into. Add a make act-runner-image target that builds local/act-runner-fixed (stock image with /var/run removed); pass it via ACT_PLATFORM_IMAGE. Document the symptom + fix in iteration/workflow.md and cross-reference from playwright.md and README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The liveness check sent one mail and failed the deploy on a transient 451 'try again later' emitted while mailu (postfix/rspamd) was restarting. Retry a fixed 12 times (12s apart) ONLY on transient exit codes (69 EX_UNAVAILABLE, 75 EX_TEMPFAIL, 124 timeout); any other code (config/usage/permanent reject) exits immediately with that exact code instead of being masked by retries.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin token tasks wrote their 'container exec -i ... curl ... -H ...' command as a >- folded scalar, but the continuation lines were indented deeper than the first content line. YAML keeps the newlines of more-indented lines, so the command stayed multi-line and bash ran each fragment separately ('docker exec requires at least 2 arguments', then '-H: command not found'). Re-indent the continuation lines to the base indent so the scalar folds into one command. Surfaced once the admin sub-service got a container name and the lookup stopped hard-failing earlier.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both the compose and the swarm bootstrap tasks registered the same matomo_bootstrap_run. In compose mode the skipped swarm task overwrote the variable with a skipped result (no stdout), so the token parser read an empty string and the assert failed. Register each task into its own variable (compose/swarm) and pick the mode-appropriate one when parsing the token and in the rescue diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bare policies like no/off render as 'restart: no' which YAML parses as a boolean, so 'docker compose config' rejects the file. Quote the scalar ('restart: "no"') to keep it a string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set a dash-style name on every addressed sub-service in meta (admin, antispam, redis, imap, smtp, oletools, antivirus, webdav, fetchmail, webmail, resolver, front), expose each as MAILU_<SVC>_CONTAINER_ADDRESS in vars, and drive the compose container_name from those vars so container_address matches the real container.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The service-loader flushes registered post-load roles cluster-wide (registration
happens once on the stack host, but "Run service-loader post-load roles" includes
the role for every host in the play). On a swarm storage host like the NFS server
there is no webserver and the app domains resolve to 127.0.0.1, so the webserver
health check fails with NameResolutionError / unreachable.
Self-gate the check to routing-mesh nodes (svc-swarm-node in swarm, all hosts in
compose), mirroring how sys-ctl-hlth-csp already self-gates. Keeps the manager and
workers (which reach the domains via the mesh); excludes non-mesh infra hosts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The swarm CI runs the Ansible controller as the non-root runner. During the
deploy, svc-storage-nfs-server mounts the cluster-shared NFS state on the
controller, where the token store lives root:root 0600. Controller-side lookups
(service / service_registry via get_merged_applications) read it, and Ansible
lookups cannot `become`, so the non-root controller hits [Errno 13] Permission
denied. The state export is no_root_squash, so a root controller reads it
exactly like the swarm nodes do.
Run the whole orchestrator as root: provision/extend/deploy all read the store
once the NFS mount is active (round 2+, since purge does not unmount between
rounds), so wrapping only the deploy was insufficient. Re-pin PATH/HOME because
sudo's secure_path drops the venv and ~/.ansible/collections. No-op when the
controller is already root (local act, production).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The swarm e2e now iterates one provision+deploy+e2e round per app variant,
mirroring the compose matrix deploy, so the keycloak TOTP env-gate is no
longer needed: the variant's services.keycloak.realm.totp_enabled=false is
baked into host_vars per round and resolves at deploy time in swarm too.
New cli/administration/deploy/swarm/matrix.py drives the rounds by reusing
the compose planner (plan_dev_inventory_matrix, _resolve_variant_payloads,
_bake_overrides): per round it provisions with the baked --vars overlay plus
--app-variants, extends the swarm topology, deploys (e2e in-deploy), waits
for convergence, and purges prior-round stacks between rounds via the new
purge_stacks.sh. The workflow collapses its per-step provision/deploy/verify
into one orchestrator step; chaos (seed/drain/assert) still runs once.
- cli/administration/deploy/swarm/matrix.py: NEW round orchestrator
- scripts/tests/deploy/swarm/purge_stacks.sh: NEW per-round swarm purge
- scripts/tests/deploy/swarm/05_provision_inventory.sh: per-round inv dir plus
conditional --vars / --app-variants (standalone path unchanged)
- .github/workflows/test-deploy-swarm.yml: single orchestrator step, timeout 360
- default.env, utils/env/handlers/passthrough.py: register INFINITO_VARS_PAYLOAD
plus INFINITO_APP_VARIANTS passthrough keys
- roles/web-app-keycloak/vars/main.yml: drop the ENVIRONMENT env-gate
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baserow's object storage is provided by seaweedfs; without an explicit
run_after edge the build dependency graph does not guarantee seaweedfs
deploys first (injected deps carry inclusion, not ordering), so a fresh
swarm round could schedule baserow against an absent S3 backend.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`/usr/bin/compose` (roles/sys-svc-compose/files/compose.py) auto-adds
`--env-file` and compose.yml carries service `env_file:`. `docker compose
build`/`config` parse+interpolate those env_file values and reject
$-bearing secrets (e.g. the dev keycloak password), whereas `docker stack
deploy` reads env verbatim and is unaffected. build/config need no runtime
env, so route them through env_file-stripped compose copies with no
--env-file; all other subcommands keep the original files + --env-file.
Adds `_subcommand`, `strip_env_files`, `_ENV_PARSING_SUBCOMMANDS` and unit
coverage for the new build/config path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The j2 extraction replaced the inline copy/content with ansible.builtin.template, which the stack-host-template lint flags as a bare template. The task is already stack-host-scoped (the parent include applies delegate_to STACK_HOST + run_once), so a leaf gate would be redundant; suppress with nocheck.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The node dnsmasq runs with no-resolv, so swarm container names (notably the swarm-mgr-01:5000 registry host) stopped resolving once the node resolver was pointed at it: workers could not pull custom-built images and rejected every task with "No such image". Route the node names to Docker's embedded DNS (127.0.0.11) in both the manager and worker dnsmasq configs, keeping the public upstreams and the domain override.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the inline copy/content block into templates/openssl-root-ca.cnf.j2 and switch the task to ansible.builtin.template; the rendered config is identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workers resolve the app domain to the manager IP (openresty is mode:host +
manager-pinned), so sys-ctl-hlth-webserver can run on every node instead of
being gated to the stack host. The shared Root CA is minted once on the
stack host and fetched by each worker, so a worker's trust store matches the
manager's leaf signer and the healthcheck's TLS chain verifies.
- sys-ctl-hlth-webserver: drop the IS_STACK_HOST gate (reachability, not
data-locality, now that workers resolve the domain to the manager)
- sys-ca-selfsigned: mint the Root CA once on STACK_HOST (run_once +
delegate_to), extract the generation steps to 02_mint.yml; non-stack hosts
slurp the cert from STACK_HOST and write it to their host-local path
(cert only, never the key)
- svc-swarm-node: anchor the shared CA early on every swarm node so it
exists before any per-node consumer reaches it; bundle the swarm-only
tasks under one DEPLOYMENT_MODE block
- sys-front-tls-selfsigned: gate leaf signing to the stack host (workers
have no CA key and run no manager-pinned openresty)
- scripts/tests/deploy/swarm/02_start_swarm_nodes.sh: workers run dnsmasq
resolving the domain to MGR_IP; excluded from the 127.0.0.1 /etc/hosts
entries that would shadow it
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CSP check derives its domain list + scheme from the locally-rendered nginx .conf files (os.listdir of the config dir), which exist only where the proxy renders them (the stack host); a swarm worker has no config dir to crawl. Gate the include on IS_STACK_HOST so it does not fail per-node. (Unlike sys-ctl-hlth-webserver, this is a genuine data-locality gate, not reachability.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>