Memcached, Joomla, Moodle, Odoo, Shopware, Snipe-IT and SuiteCRM set
their cache size, PHP memory_limit, OPcache, worker limits and search
heap as literals next to a mem_limit they never read. They now ask
lookup('service_memory_mb') for a share of that limit. The shares are
chosen so every rendered value stays what it was.
Where the parameter lives in a file that cannot hold a template, the
value is handed in: Shopware's init script reads INIT_PHP_MEMORY_LIMIT
and refuses to start without it, SuiteCRM's image takes it as a build
argument. Shopware's worker gets --memory-limit as a command appended
to its entrypoint, and Moodle's OPcache size moves from the static ini
into the rendered one.
The performance.php.memory_limit keys of Joomla and Moodle are gone
from services.yml; nothing else read them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The negative control aborted every <host>/logout and then asserted the
admin bearerAuth cookie survives. It failed in run 32597195876, job
Shopware#0, in 5 of 6 executions: the cookie was gone anyway.
The premise was right and the target was wrong. The trace of the failing
run shows all nine sweep aborts landing - 584 ms too late:
51.534 -1 shop/api/oauth/sso/config logoutSso() cut off by the
injected logout redirect
51.538 200 auth/.../openid-connect/logout
52.842 200 auth/.../logout/logout-confirm
53.377 200 logout.<domain>/?sid=... conductor, Clear-Site-Data
53.961 -1 9x <host>/logout sweep, correctly blocked
54.256 200 shop/admin cookie already gone
What deletes the cookie is the Clear-Site-Data header on web-svc-logout's
conductor page - "the ONLY path-agnostic cookie deletion mechanism" per
universal-logout's app.py - and it clears the whole registrable domain,
which is how an origin on logout.<domain> reaches a cookie on
shop.<domain>. The per-app /logout requests are the second mechanism, not
the first. Shopware's own clearAuthState never runs either: the injected
redirect preempts it.
The route now blocks the conductor, which is the honest simulation of
"web-svc-logout never lands". It loads after Keycloak's logout-confirm,
so blocking it cannot disturb that flow.
Two anchors identify it, because one is not enough on every axis.
`lookup('tls', ...)` family-aligns a cross-app reference per consumer
(utils/tls_common.py align_domain_to_consumer), so where web-svc-logout
is onion-primary and this role and Keycloak sit in different families,
the origin resolved here is not the one Keycloak framed. The front-channel
parameters Keycloak appends catch that case. LOGOUT_URL stays optional for
the same reason - an empty lookup must fall through to the second anchor
rather than fail the test.
The sibling's poll message names the real mechanism and keeps the
rationale for its 30s wait, which 5b6d12f12 added for a reproduced
Tor-only failure.
Verified on clearnet evidence only: the failing job skipped every Tor
task, and no Tor run in either pipeline produced a trace, since traces are
retain-on-failure and those jobs were green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ca40eee68 made the admin test wait for bearerAuth to disappear before leaving
Keycloak's logout page. Nothing proves that wait is load-bearing. Delete it and
the suite stays green on clearnet, because there the sweep wins the race
anyway; it turns red only over Tor, where a red job reads as flake. The
property the wait protects is therefore untested exactly where it matters.
Racing it deliberately cannot fix that: a test that leaves early and asserts
the session survived would pass over Tor and fail on clearnet - the same
latency dependency, mirrored. 801923eb3 measured that asymmetry on Funkwhale:
"Only swarm+tor is slow enough to lose the race."
So this does not race. page.route aborts every request whose pathname is
/logout - each app host the conductor sweeps - and the test then asserts the
admin cookie SURVIVES. What is pinned is the causal link, not a timing: the
session outlives a logout exactly when the sweep does not land. That stays
true however fast the sweep becomes, and breaks only if logout stops depending
on it, which is a change worth failing on.
The pathname filter is deliberate. A glob on **/logout* would also swallow
Keycloak's own /realms/x/protocol/openid-connect/logout and break the flow
under test; url.pathname === "/logout" matches the sweep targets and neither
Keycloak's end-session endpoint nor the conductor page at /.
Two ways this test could pass while proving nothing, both closed. A parallel
page.on("request") counter separates them: no request to /logout at all means
the conductor never reached its sweep, the precondition is absent and the test
skips - making the sweep land is the sibling test's assertion. Requests seen
but nothing intercepted means the abort is a no-op, which fails loudly rather
than skipping forever if page.route ever stops matching.
Skipping rather than failing on the absent precondition also keeps the cost
bounded: playwright.config.js runs workers: 1 with retries: 2, so a failure
here would spend three full Tor login cycles proving something the sibling
already reports.
Interception was measured, not assumed: with three origins, a cross-origin
iframe and keepalive: true, the handler fires and the target server sees no
request. signIn/dismissFirstRunWizard/clickLogout are extracted so the second
test costs helpers rather than a copy of the first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e59ea21c6 waited on document.cookie, which is origin-scoped. By the time
it runs the page sits on logout.<domain>, where Shopware's bearerAuth -
host-scoped to shop.<domain> - is not visible. The predicate was true on
the first poll, so the wait did nothing and three of the six jobs were
unchanged.
page.context().cookies() reads the browser context's jar regardless of
which document is displayed, so the same post-condition can be observed
from Keycloak's logout page.
The wait matters because the thing that clears that cookie is not
Shopware and not the injected patch - it is web-svc-logout. Keycloak
loads its conductor as the front-channel logout URL in an iframe; the
conductor fetches <host>/logout for every domain with
credentials: "include" (universal-logout templates/conductor.html.j2),
each /logout deletes the cookies visible on that host and sends
Clear-Site-Data (app.py), and roles/web-svc-logout/templates/logout-proxy.conf.j2
supplies the matching Access-Control-Allow-Origin plus
Allow-Credentials. Every link of that chain is in place.
What was missing was time. In run 32284541284 the trace shows the
logout-confirm click at 16543.805 and the navigation to /admin at
16902.227 - 358 ms, against 220-350 ms per Tor hop. The conductor's
document had not finished loading, so its sweep never ran:
grep -c -a "onion/logout" on the trace network log returns 0. On clearnet
the same chain fits inside those 358 ms, which is why variant 0 passes
there and fails over Tor.
A failure now reports the cookie that survived instead of the missing
password field two lines below, which said nothing about the cause.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4a543b3e1 moved the assertion back onto /admin so a surviving session
would fail where it means something. It does fail there - and the reason
is that nothing waits for the logout to finish before the page is torn
down.
waitForLoadState only observes a navigation that is already committed; it
never waits for one a click is about to start. Shopware's SPA logout
triggers no document load at all, so the call returned in 0.7ms and the
gotoOnion below fired 3.4ms after the click, killing the in-flight
teardown. Measured in run 32284541284 job 96178512465's trace: click at
10363.718, waitForLoadState returns 10420.992, goto starts 10422.365.
Proof the teardown never completed: POST /api/oauth/token with
grant_type=refresh_token returned 200 five minutes later, and
_action/user/logout appears zero times in the network log (positive
control _action: 6).
Waiting on the credential instead of on a load states the real
precondition, and it is faster on the happy path than any fixed settle
would be - it ends the moment the cookie goes. The catch keeps a timeout
from masking the assertion below it with a different error.
Six jobs across both runs, and the grid rules out the two obvious
culprits: variant 2 carries no injected logout script and still fails,
and variant 2 swarm+clearnet fails while variant 2 swarm+tor passes. It
is a race, not a tor defect and not an injector defect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The native admin spec clicked in-app logout and then waited 60s for a password field on whatever page it had landed on. That page is never Shopware: with web-svc-logout attached, the flow ends on logout.<domain>, the universal-logout status page, which is terminal by design (README: "Front Channel Logout URL ... status page with live feedback").
Evidence from job 95883075100: the error-context snapshot shows heading "Logging out" plus paragraph "You are logging out from following apps" and a list - the web-svc-logout UI, not Keycloak's end-session page, which carries a Logout button instead (that one was already handled by a30d67a63). The openresty access log confirms the chain at 03:38:58: POST /realms/.../logout/logout-confirm 200, then GET logout.infinito.example/?sid=...&iss=... 200, 2555 bytes.
Returning to ${appBaseUrl}/admin before the expectation puts the claim where it can be true or false. This is the same move c7e5b0a27 made for web-app-funkwhale, and the shared persona flow has always done it via assertUnauthenticatedLanding (personas/utils/landing.js). The assertion itself is untouched, so the test gets stricter, not more lenient: a session that survived logout now fails here instead of at a spot that said nothing about it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.
Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
container_address resolves a container id for exec and emits a $(...) subshell in swarm; setup/oidc.yml passes the value through shlex.quote, so the substitution never ran and client.php wrote the literal text into token_endpoint and userinfo_endpoint. The deploy stayed green and every admin SSO login failed at the code-to-token exchange. container_service is the maintained lookup for the addressable form and yields keycloak in compose, keycloak_keycloak in swarm.
init.sh no longer waits on the S3 endpoint: c5a149b11 unmounted the config it guarded, while a hard fail there still took web, worker and scheduler with it. The admin-bundle check now runs unconditionally — it was skipped exactly when objstore was on, i.e. in the only variant with SSO, although the bundles are local either way. The two objstore env vars lost their last consumer with it.
no_log now sits on the two tasks that carry the admin password and the OIDC client secret; handing it through include_tasks apply: does not censor them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A command substitution inside an argument does not trip set -e, so a failing password generator would have left --password= and Shopware would have prompted for it interactively; on a non-TTY that kills init and with it every service gated on service_completed_successfully. The value is now generated in its own assignment.
privileges.php could not tell a missing user from an already-set flag, since both report zero affected rows. It now verifies the account exists, which is the only place a silently failed admin provisioning can surface: admin_user.sh swallows both of its commands with || true.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shopware's in-app logout leaves the browser on Keycloak's end-session page, which asks 'Do you want to log out?' because the request carries no id_token_hint. The spec waited 60s for a password field that page never renders. The shared persona helper already handles this for inAppLogout; it is now exported so the role-local spec can use it too.
Evidence: the error-context of job 95119330653 shows heading "Logging out" and button "Logout" where the login form was expected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AdminOpenAuth clears the admin flag and the ACL roles on every login that no role-assignment rule matched (UserResolver::getUserChangeSet). The Playwright login therefore demoted the provisioned administrator, the admin backend rendered empty, and the next provisioning run answered 403 FRAMEWORK__MISSING_PRIVILEGE_ERROR. The client now carries a rule with userBecomeAdmin, and a separate step re-asserts the flag for accounts an earlier login already demoted, which no console command can do.
discoveryDocumentUrl stays empty because a successful discovery assigns the document over the explicit endpoints and moves the back channel onto the .onion host cURL refuses to resolve.
system:install --basic-setup provisions admin/shopware with a published password; that account is locked on first install. shopware.yaml is mounted again after being rendered but unmounted since the mounts moved into meta/volumes.yml. files/ is split into php/ and shell/ trees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The keycloak provider derives every endpoint from one auth-server-url, so both the browser redirect and the server-side calls pointed at the onion. cURL refuses .onion outright under RFC 7686 -- it never opens a socket -- so discovery failed, the authorization endpoint stayed empty and the login button redirected to itself.
The generic open_id_connect provider takes the endpoints individually. The authorization endpoint keeps the onion, which is the address the browser reaches through Tor; token and userinfo point at the internal service name, which cURL accepts. discoveryDocumentUrl still names the onion and therefore fails, which is what leaves the explicit endpoints in place -- discoverWellKnown overwrites them whenever it succeeds, and Keycloak advertises unusable values under every address it is asked through.
Verified in the running container before any redeploy: patching the live client row to this shape makes GET /admin/open-auth/<id>/remote answer 307 to the Keycloak authorization endpoint instead of to itself, and the browser now completes the Keycloak login -- the spec fails afterwards on the admin menu rather than on the login form. The payload was checked against a stub first and matches the live patch byte for byte.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bin/console user:update does not exist. The installed shopware/core ships exactly UserCreateCommand, UserChangePasswordCommand and UserListCommand. Both call sites hid the failure: one with || true, the other with || true and 2>/dev/null on top.
rename_admin_user.sh consisted of nothing else. It found the default admin user, printed Renaming: admin -> administrator and then did nothing, so the message was a false statement on every deploy. Script and task are removed. In ensure/admin_user.sh the user:update --email call goes too; user:create already passes --email and that works -- the administrator in the running stack carries administrator@infinito.example, read back through the admin API.
This is behaviour-identical: the removed calls never did anything. What it does change is visibility -- the Shopware default user admin with user@example.com survives every install, which is exactly what the rename was meant to prevent. Shopware offers no console command for that, so closing it needs the admin API and a decision about the account; that is deliberately not part of this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
meta/services.yml pinned sso.flavor: oauth2, so an oauth2-proxy fronted the whole role. That gated the storefront too and left /admin behind a proxy the administrator persona could not log out of, while tasks/setup/oidc.yml -- the only OIDC wiring -- installed a literal placeholder plugin name behind an || true and wrote InfxOidc.config.* keys that no plugin reads. tasks/03_setup.yml and tasks/04_cleanup.yml were included by nothing, so none of it ever ran.
The flavor moves to oidc, which drops the proxy on its own because sso_proxy.yml keys off is_proxy_gated: the storefront becomes public and /admin authenticates through heptacom/shopware-platform-admin-open-auth against Keycloak. The plugin is baked into files/Dockerfile beside the existing composer require, installed and activated by tasks/setup/oidc.yml, and its Keycloak client row is written idempotently by files/ensure/oidc_client.php over the Admin API -- GET by provider, then PATCH or POST -- because the plugin keeps its configuration in the heptacom_admin_open_auth_client DAL entity, not in system_config.
The web service gains container_extra_hosts. With the proxy gone the token exchange runs inside the shopware container, which under swarm resolves clearnet only and could not reach the keycloak onion.
The LDAP plugin tasks are deleted rather than wired. No admin-LDAP extension for 6.7 is installable unattended -- the only real one is a paid Store extension needing a licence token -- and arming tasks/03_setup.yml would have fired the INFX_LDAP_PLUGIN placeholder on every deploy that ships OpenLDAP, since meta/services.yml derives ldap.enabled from group membership. Directory identities reach the backend through the Keycloak LDAP federation instead.
PERSONA_BIBER_BLOCKED becomes unconditional: biber has an account in no variant. test-admin-native.js keeps the authenticated journey and now drives the AdminOpenAuth provider link when SSO is on.
Verified by execution: the provisioning payload and both idempotency branches against a stub; composer resolving 9.1.0 against shopware/core 6.7.13.0 without conflict; the install gate rejecting a no-plugins-found run and accepting both success shapes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Playwright declares the timeout option on locator.isVisible() and isHidden() as deprecated and ignored: both return immediately and never wait. Passing resolveTimeout(10_000) to one therefore changes nothing at runtime and only tells the reader a wait happened that never did. That reading is what let 7538b8a0b conclude two genuine call sites were fine, and what hid the false negative b97cf8888 had to fix.
Sixty call sites across fifty-eight role-local specs lose the argument and nothing else. Every added line in this commit is a bare isVisible(); a grep for added lines that are not one returns zero, so a behaviour change cannot have entered here. Files whose diff also alters waiting semantics are deliberately not part of this commit.
A repository lint added in b97cf8888 keeps the construct out. It matches isVisible and isHidden only, because isChecked, isDisabled, isEditable and isEnabled take a real timeout.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 commits: the socialhome role reimplemented against the project it actually
deploys, native-login specs for funkwhale and shopware, a window.open IdP
handoff and a settings-link fallback in the shared persona helpers, a swarm
converge budget that gives up on a terminal task, and a long tail of role
fixes.
Eleven conflicts. The shared persona helpers (keycloak.js, logout.js) take the
incoming structure — they gained real behaviour this branch never had — with
this branch's resolveTimeout convention re-applied over it and
confirmKeycloakLogoutIfPrompted re-attached at every return point; without it
the Keycloak SSO session outlives the logout. playwright.config.js keeps this
branch's version: both sides set actionTimeout, but ours routes through an env
var and 15s is too short behind an onion. socialhome's spec takes the incoming
content — 2dbd6eb10 found the role described the wrong upstream project
entirely — and keeps gotoOnion and expectHstsWhenTls, since demanding HSTS over
a plaintext onion is a false failure. Both READMEs were regenerated rather than
merged by hand.
Three collisions arrived with no conflict marker at all, because each side
changed a different place in a different file:
- group_vars/all/18_swarm.yml declared swarm_stack_converge_retries twice.
Same value, so nothing breaks today; editing the documented one would have
silently changed nothing.
- svc-net-tor and web-app-socialhome both claimed 192.168.74.0/24. Socialhome,
the newer role, moves to 192.168.75.0/24.
- web-app-socialhome/meta/schema.yml arrived as a new file, so ef5ea7cb5's
rename never saw it. Its credential would not have resolved and the role
would have deployed with an unusable administrator account.
The incoming specs also needed this branch's conventions: raw timeouts wrapped,
four page.goto calls routed through gotoOnion, a request.get given an
onion-aware timeout, and 21 roles marked playwright-service-flag over the tor
block this branch added to them.
make quality green on the merge result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The shared persona helper cannot log the administrator out of Shopware. The only logout control lives in .sw-admin-menu__user-actions, which the administration stylesheet keeps at display:none until its toggle is clicked, and that toggle is a bare div carrying no role, no aria-haspopup and no dropdown class, so nothing in the helper's trigger set matches it. Its second fallback, following a same-origin settings link, misses as well because the administration is hash-routed and every settings target is #/sw/settings/...
Declare PERSONA_ADMINISTRATOR_BLOCKED and move the journey into a role-local spec that drives Shopware's own selectors: native admin login, an authenticated backend assertion, logout through the user-actions toggle, and back to the login form. It skips when SSO_SERVICE_ENABLED=true, because the oauth2-proxy then owns /admin and the native form is unreachable.
The spec leaves the first-run wizard by navigating away from its route rather than closing it: the wizard renders with :closable=false on its first step, so it has neither a close button nor an escape handler, while its footer intercepts the pointer events aimed at the admin menu behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.
Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).
Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The earlier attempt routed sessions through framework.session.handler_id with a redis DSN, which lazy-loads symfony/cache's RedisProxy whose dump() declaration is incompatible with the image's php-redis - a PHP fatal in every php container under swarm. Route sessions below Symfony instead: bake a conf.d ini registering phpredis's native session save handler with session.save_path taken from the REDIS_SESSION_TARGET env at runtime (php.ini env substitution, no secret in the image), emit that env with phpredis auth[user]/auth[pass] url-encoded query syntax from the engine lookup, and set framework.session.handler_id to null so Symfony defers to php.ini. Sessions are shared across replicas; cache and messenger paths stay untouched. All Dockerfile prose comments dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cache and messenger already use the mounted REDIS_URL, but the framework.yaml omitted a session handler, so sessions were node-local files and logins/carts broke behind the swarm VIP. Set framework.session.handler_id to the existing REDIS_URL so sessions are shared, and drop the entity replicas:1 pin so web and worker default to the topology count. Re-pin the genuine singletons: init (one-shot install/migrate), scheduler (double-dispatches on >1 poller) and opensearch (discovery.type=single-node). Compose is byte-identical (replicas ignored). Confidence ~94% (session cross-replica reasoned, not live-verified).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HSTS (Strict-Transport-Security) is an HTTPS-only header: a browser ignores it
over plain http (RFC 6797) and OpenResty only emits it on the TLS vhost. On an
onion node TLS is off and every domain is served over http, so the inline
`expect(headers["strict-transport-security"]).toBeTruthy()` in 33 specs failed
with `Received: undefined`.
Route all 33 through the shared expectHstsWhenTls(headers, baseUrl, label)
helper (personas/utils/tls.js), which requires HSTS over https and asserts its
absence over http. On a TLS node the base URL is https, so the assertion is
identical to before; on an onion node the same spec now passes. HSTS logic
lives in one place (SPOT) instead of 34 inline copies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reconcile main's object-store feature and pre-swarm deploy tasks with the branch's compose/networks/volumes SPOT and swarm lints.
objstore (seaweedfs/minio): standalone compose + embedded service templates migrated to the container_image/container_volumes/container_networks/compose_volumes/compose_networks/depends_on lookups; meta/volumes.yml on the canonical dict schema with s3.json and nginx.conf as type:config; embedded services made swarm-compatible via compose_only/compose_restart.
deploy tasks (erpnext, opentalk, seaweedfs, shopware, snipe-it, wordpress, yourls): container exec routed through the container_address resolver and gated on IS_STACK_HOST, shell module plus stack_host_template, no chdir or compose-verb forms.
Also removed orphaned vars, restored MAILU_INSTANCE_DIR, freed the seaweedfs subnet (192.168.206.0/24), stripped literal quotes from the nextcloud/bookwyrm env templates, and routed main's objstore compose_volumes tests through dir_var_lib.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire nine database-backed web-app roles to the shared SeaweedFS object store
via the engine-agnostic objstore lookup, following the existing consumer recipe
(declare the seaweedfs service, render the app S3 config from
lookup('objstore', application_id, ...), add the e2e test). Each app's S3
config keys were verified against the upstream docs/source, and all honor
SeaweedFS path-style addressing + the internal http endpoint:
- fider: BLOB_STORAGE=s3 + BLOB_STORAGE_S3_*
- snipe-it: split s3_private/s3_public disks, single bucket via BUCKET_ROOT, *_AWS_PATH_STYLE
- mobilizon: Uploader.S3 + ExAws (private)
- decidim: ActiveStorage amazon service, force_path_style
- taiga: django-storages, AWS_S3_ADDRESSING_STYLE=path
- shopware: Flysystem amazon-s3 adapter, use_path_style_endpoint
- magento: remote_storage driver aws-s3 + endpoint
- matrix: s3_storage_provider module baked into the image + media_storage_providers
- gitlab: consolidated object storage, single bucket split into per-type virtual buckets, path_style
Both object-store guards (test_database_requires_seaweedfs,
test_seaweedfs_consumer_playwright) pass; autoformat clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
shopware/core 6.7.8.2 locks symfony/messenger (and console/framework-bundle)
to ~7.4.0. The Dockerfile then ran `composer require symfony/redis-messenger:^6.4`,
which forces symfony/messenger ^6.x and conflicts with the locked 7.4 family,
so composer aborts with exit code 2 at the builder stage and the image fails
to build (deploy failed).
Bump the constraint to ^7.4. symfony/redis-messenger 7.4 requires
symfony/messenger ^7.3|^8.0, satisfied by the locked 7.4, so the require
resolves cleanly against the scaffolded 6.7.8.2 project.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a mode-aware `container_address` lookup plugin and a
`resolve-container-id` helper script that resolve swarm task IDs at
exec time (compose mode returns bare names unchanged). Migrates ~25
roles + ~28 task files from `command:` to `shell:` so the lookup's
`$(...)` subshell evaluates correctly, and extends `database` lookup
with an `address` key for cross-role DB exec.
Three new lint tests enforce the contract: every container exec
target goes through the resolver, lives behind an IS_STACK_HOST gate
(walks the include graph one hop deep), and uses `shell:` rather than
`command:` when the target is a Jinja reference. Existing nocheck
suppressions on bare-name DB exec sites (nextcloud redis, listmonk,
moodle OIDC, mailu) are replaced with the lookup-based form.
Fixes the systemd Environment= injection in sys-ctl-cln-disc-space
(it never expanded $(...)) by passing STACK / SERVICE_KEY parts and
resolving inside the script. Extracts shopware admin and mediawiki
extension install bodies into dedicated .sh files invoked via env.
Rewrites password-bearing shell tasks to either `| quote` directly or
the `-e VAR=... | quote` + `sh -c '... ${VAR} ...'` env-bridge pattern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops `MAX_LINES` from 12 to 11 in `test_inline_script_block_size.py`,
catching the five blocks that were sitting exactly on the previous
threshold. Pure-bash blocks move to dedicated `.sh` files invoked via
`ansible.builtin.script:` or `run: bash …`; the keycloak heredoc with a
templated JSON payload splits into a `set_fact` plus a folded
`printf | kcadm … -f -` one-liner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote four cheap rules in `eslint.config.js`:
* `no-unused-vars`: warn → error (with `argsIgnorePattern: "^_"`)
* `no-var`: error
* `eqeqeq`: error (always)
* `playwright/no-wait-for-navigation`: off → error
* `linterOptions.reportUnusedDisableDirectives`: off → warn
Add a repo-root CommonJS block so `eslint.config.js` itself sees Node
globals (`require`, `module`).
Fix every flagged offender:
* 138 stale persona-helper imports across 70 spec files
(`isServiceEnabled`, `skipUnlessServiceEnabled`, `runAdminFlow`,
... — leftovers from the SPOT consolidation).
* 4 unused catch bindings → modern `catch {}` syntax.
* 2 unused `dashboardBaseUrl` declarations (minio, moodle).
* 1 unused `label` arg → renamed to `_label` to opt out per the
argsIgnorePattern.
* 1 intentionally-kept legacy helper in web-app-wordpress's spec
receives a per-line `eslint-disable-next-line` with reason; the
preceding docstring already explained why it stays.
* 1 `page.waitForNavigation` in web-app-fediwall's Mastodon OIDC flow
receives a per-line disable with reason — the navigation target
depends on the runtime OIDC issuer; `waitForURL` would need a
runtime-built pattern.
* 10 stale `// eslint-disable-next-line no-console` directives now
surfaced by `reportUnusedDisableDirectives`; removed.
ESLint after this pass: 0 errors, 154 warnings (148 pre-existing
`playwright/expect-expect` plus 6 `playwright/no-force-option` that
were already inherited from `playwright/flat/recommended`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).
* Document the rule in `docs/contributing/documentation.md` under
"Comments". Code MUST describe the *why* in present-state language;
contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
`cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
`eslint.config.js`. Pure comment / docstring / log-message edits;
no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
enforce the rule going forward across `.js`, `.py`, `.yml`,
`.yaml`, `.j2`, and `.sh` files. The legacy
`test_no_stale_requirement_refs.py` is exempt as the only file
whose purpose is to detect these very tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both helpers were inlined verbatim in 71 of the role-local
`playwright.spec.js` files (52 with the compact form, 19 with the
verbose form, plus one parameter-name variant). All bodies were
semantically identical.
Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dotenv.js` holds
the canonical pair (`decodeDotenvQuotedValue` and `normalizeBaseUrl`),
re-exported through `personas/utils/index.js` so specs that
`require("./personas")` get them via the existing aggregator.
- Every role spec drops its inline copies and pulls the helpers from
`./personas` instead. Specs that only reference one of the two
carry only that one in the destructuring (no unused-import noise).
Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` no
longer carries the "once a third spec needs them, MUST be promoted"
directive: the helpers live at the shared path and inline copies are
forbidden.
Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
baseline; no new unused-imports introduced).
- 0 inline `function decodeDotenvQuotedValue|normalizeBaseUrl`
definitions remain in any role spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make `tests/lint/.../test_has_spec.py` raise instead of warn when a
roles/web-app-* role has no `files/playwright.spec.js`, then ship the
27 minimal specs needed to satisfy the new floor.
Each new spec follows `roles/web-app-mig` (TLS + canonical-domain
baseline, HTML content-type, three persona flows) with role-specific
admin-interaction labels seeded from app domain knowledge. The bodies
are intentionally static — `templates/playwright.env.j2` is not
shipped per role yet, so the specs do not run end-to-end on deploy;
detail tightening per role is follow-up work.
Also fix `_body_contains_stub_marker` in `test_no_stub_tests.py`: it
substring-matched `TODO` against `body.upper()`, which flagged
`masTODOn` inside the mastodon spec's body regex. Switched to
word-boundary matching so genuine `// TODO:` markers still trip the
lint while embedded substrings in app names or regex alternations do
not.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* scripts/lint/autoformat.sh: rename AUTOFORMAT_PARALLEL to PARALLEL,
declare it at file start with default `true`; still env-overridable.
Reword the worker comment so the literal `# shellcheck` token no
longer appears at start-of-line and trips SC1072/SC1073.
* scripts/lint/galaxy.sh: drop the GALAXY_FATAL / GALAXY_NAMESPACE /
GALAXY_PARALLEL env vars (over-engineered for a single repo). Failure
is always fatal. NAMESPACE is an internal constant. Add the same
PARALLEL env var (default `true`, env-overridable); falsy demotes the
xargs fan-out to one worker.
* docs/contributing/environment/variables.md: collapse the four
AUTOFORMAT_*/GALAXY_* rows into one `PARALLEL` row covering both
consumers.
* docs/contributing/tools/make.md: refer to `PARALLEL=0` everywhere;
add the lint-galaxy parallelism note.
Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ARG declarations used in FROM instructions must be in the global scope (before
the first FROM). Declaring them inside a build stage makes them invisible to
subsequent FROM instructions, causing "UndefinedArgInFrom" and an invalid
reference format error at build time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- added new file roles/web-app-shopware/files/framework.yaml defining trusted_proxies and trusted_headers for Symfony
- mounted framework.yaml into /var/www/html/config/packages/ in docker-compose
- exposed new role vars SHOPWARE_FRAMEWORK_HOST/DOCKER for mounting path
- rendered framework.yaml via Ansible copy task with proper permissions
- adjusted env.j2 to set TRUSTED_PROXIES and TRUSTED_HOSTS dynamically from domains and networks
- added SHOPWARE_DOMAIN var to vars/main.yml
- removed inline framework.yaml creation from Dockerfile (now managed via mount)
- updated proxy template (html.conf.j2) to include X-Forwarded-Ssl header
- improved init.sh permission handling for shared volumes
See ChatGPT conversation for implementation details and rationale:
https://chatgpt.com/share/690d4fe7-2830-800f-8b6d-b868e7fe0e97
- Added 'font-src data:' to CSP whitelist to allow inline fonts in Admin UI
- Refactored init.sh to run as root only for volume permission setup, then drop privileges to www-data
- Unified all bash invocations to sh for POSIX compliance
- Added missing 'bundles' named volume and mount to Docker Compose
- Set init container to run as root (0:0) for permission setup
- Added admin user rename step via Ansible task
See discussion: https://chatgpt.com/share/69087361-859c-800f-862c-7413350cca3e
- moved init.sh from template to files/ for direct copying and bind mounting
- removed hardcoded user creation from init process
- added database emptiness check before running system:install
- added new task 03_admin.yml to ensure admin user exists and update password/email via Ansible
- switched docker exec shell from bash to sh for Alpine compatibility
- updated Dockerfile and docker-compose.yml accordingly for mount-based init script