Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 009d0eb15c test(web-app-nextcloud): real integration_mastodon bridge coupling
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>
2026-06-21 06:03:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 aeb1520174 test(web-app-nextcloud): real integration_jira OAuth connect coupling
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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 276f1d05aa test(web-app-nextcloud): real integration_google OAuth connect coupling
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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 135ebccbc5 test(web-app-nextcloud): real integration_github OAuth connect coupling
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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 98918efe75 test(web-app-nextcloud): real integration_discourse User-API-Key coupling
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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5c0e731e2d test(web-app-nextcloud): integration_suitecrm clean spec + mark optional
- 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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 462c20cbb9 fix(web-app-nextcloud): integration_openproject OAuth provisioning + full e2e
- 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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 110ca7d134 feat(web-app-nextcloud): real integration_gitlab two-way OAuth coupling
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>
2026-06-21 06:03:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 48fbf53c39 test(playwright): nextcloud shared spec infra — login robustness, env exports
- 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>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2b027bb64a feat(addon_env_flags): gate bridged addon flags on partner deployment
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>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 81d1fd47d7 fix(web-app-erpnext): self-provider playwright-service-flag nocheck
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1727c07ee9 style(web-app-discourse): single space before inline nocheck in ldap-auth meta
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a8245db213 fix(web-app-odoo): robust SSO login — wait for web client, handle existing realm session
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>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8fb1af2345 fix(test-e2e-playwright): widen in-app logout dropdown render wait to 1.5s
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>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e00c78034f feat(web-app-erpnext): expose as consumable shared service
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>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a2176018a1 fix(test-e2e-playwright): rerun-spec stages addons/ and fixtures/ subdirs
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>
2026-06-21 06:03:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 200738162b style(desk-gnome): single space before inline nocheck comment in nasa-apod meta
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 54c5b91fe2 test(web-app-odoo): generous timeout for heavy Odoo module web-client bootstrap
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>
2026-06-21 06:03:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 650da2df56 test(playwright): addon specs verify integrations are configured and coupled
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>
2026-06-21 06:03:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0e57ab23cd feat(playwright): env contracts for mediawiki/pretix/xwiki + nextcloud integration flags
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>
2026-06-21 06:03:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d738c12015 test(addons): require a tasks/addons hook for every service-coupled addon
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>
2026-06-21 06:03:07 +02:00
kevinveenbirkenbachandClaude Opus 4.8 92dd4c7a65 feat(addons): per-addon integration hooks for discourse/friendica/joomla/mediawiki/pretix/xwiki
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>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 87c05ddc5d feat(web-app-nextcloud): real addon integration hooks (exception-only, bidirectional where needed)
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>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9aa975d665 fix(web-app-nextcloud): JSON-encode list/dict addon configvalues in plugin loader
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>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d2f5efa7fe feat(test-e2e-playwright): parallel workers via plugin + tunable timeouts; split role vars by precedence
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>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 70786aa126 refactor(addons): emit playwright addon gate flags via addon_env_flags lookup
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>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbach c277eaff8e Set lifecycle to beta 2026-06-21 06:03:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 418a2eafae fix(web-app-matomo): patch DB config on the node and keep it web-writable
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>
2026-06-20 23:35:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f73c072739 fix(sys-front-inj-matomo): degrade gracefully when Matomo is unreachable
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>
2026-06-20 23:35:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5cd9053b05 docs(requirements): add 028 atmo.rsvp role + Mobilizon bridge
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>
2026-06-20 23:35:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 51a9e81da0 fix(web-app-matomo): make the swarm Matomo bootstrap robust
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>
2026-06-20 23:35:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 761450363b feat(make): add act-runner-image target for Docker-28/29 act runs
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>
2026-06-20 12:16:49 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0863b83c92 chore(claude): sort permissions.allow, record git add/log allows
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:26:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6273bfde94 fix(sys-ctl-hlth-msmtp): retry only transient SMTP failures
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>
2026-06-20 11:26:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e5637511d0 fix(web-app-mailu): fold container-exec shell scalars to a single line
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>
2026-06-20 11:26:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 091dc12846 fix(web-app-matomo): separate compose/swarm bootstrap registers
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>
2026-06-20 11:26:25 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b5a000e5ca refactor(sys-ctl-hlth-webserver): drop redundant self-gate comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 04:35:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc398084f9 fix(compose_restart): quote the restart policy scalar
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>
2026-06-20 04:35:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4a2b041f1f fix(web-app-mailu): uniform mailu-<svc> container names
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>
2026-06-20 04:35:14 +02:00
kevinveenbirkenbachandClaude Opus 4.8 af1f80f5c8 fix(sys-ctl-hlth-webserver): self-gate health check to mesh nodes
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>
2026-06-20 03:32:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6df5d13df1 fix(swarm-test): run the matrix orchestrator as root for NFS token-store reads
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>
2026-06-20 02:41:55 +02:00
kevinveenbirkenbach 91950154a4 Merge remote-tracking branch 'fork/main' into feature/swarm-nfs
# Conflicts:
#	.github/workflows/test-deploy-local.yml
2026-06-20 00:54:16 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ca51251231 feat(swarm): variant-matrix e2e parity with compose (drop keycloak env-gate)
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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fd1dc57413 fix(baserow): order web-app-seaweedfs before baserow via run_after
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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2c985b6dd6 fix(compose): strip service env_file for build/config subcommands
`/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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 def7898738 fix(sys-ca-selfsigned): nocheck the stack-host-template lint on the openssl-config template
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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 aef16df3b7 fix(swarm-test): resolve registry host on nodes via Docker embedded DNS
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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d2f95910e3 refactor(sys-ca-selfsigned): render openssl root-CA config from a j2 template
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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bc41a0e0f2 feat(swarm): worker DNS + shared Root CA replace the webserver health gate
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>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c6094539de fix(sys-ctl-hlth-csp): run the CSP health check on the stack host only
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>
2026-06-20 00:44:38 +02:00