- 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>
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>
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>
Add tests/lint/ansible/services/test_addons_playwright_spec.py requiring roles/<role>/files/playwright/addons/<id>.spec.js for EVERY addon of EVERY role — no mechanism auto-exempt and no # nocheck escape. Ship the 87 per-addon specs (gated via skipUnlessAddonEnabled so they skip cleanly when the addon is disabled in the current variant), the test-e2e-playwright addon-gating harness (addon-gating.js, playwright.config.js, recursive 02_run_one.yml), the shared odoo helper, and the ADDON_<ID>_ENABLED flags in the six playwright.env.j2 templates. Relax test_env_file_schema to skip Jinja-templated keys in the ADDON loop. Doc updates (suppression.md, requirements/026) follow in a separate commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the S3 objectstore config (templates/config/objectstore.config.php.j2) into NC's config dir before install, gated on objstore.enabled — NC cannot switch an already-installed instance to objectstore primary storage, so it must be present at install time.
Add the SeaweedFS consumer storage Playwright test (files/playwright/test-seaweedfs.js, playwright.env.j2 SEAWEEDFS_* vars, spec wiring; drop the playwright-service-flag nocheck for the seaweedfs service) plus the shared runSeaweedfsStorageCheck helper (admin logs into the Filer once, counts bucket objects via the Filer JSON listing, asserts growth) and the seaweedfs-consumer integration lint.
web-app-seaweedfs: bind weed to 0.0.0.0 (-ip.bind) so the multi-homed gateway is reachable by consumers that resolve the service name to its IP on the shared network.
nextcloud_install: classify 'was not found on the appstore' as unavailable (skippable, non-fatal) so a plugin without a release for the pinned NC version does not abort the whole deploy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The biber-LDAP-login test asserted that biber's URL after logout
matches `/realms/.+/protocol/openid-connect/logout`, but its
variant gating (test.skip when `oidc || \!ldap`) means the test only
runs in nextcloud variant 2 — where meta/variants.yml explicitly
sets `services.logout.enabled=false`. Without web-svc-logout in
scope, no universal-logout JS is injected, the native Nextcloud
logout never hits Keycloak's end-session endpoint, and the assertion
times out 100% of the time (job 77602110528 on run 26362676803).
The combination `oidc=false + logout=true` is semantically
impossible: universal-logout coordinates logout across an OIDC realm
and has nothing to do when no OIDC session exists. The Keycloak
end-session assertion is structurally never reachable from this
test's scope.
Mirror the existing post-logout verification in
test-login-biber-oidc.js (which passed in variant 0): navigate to
/login and assert no authenticated Nextcloud shell is visible. That
check is variant-agnostic and captures the real semantic intent —
"after clicking Log out, biber is no longer signed in" — without
depending on the universal-logout injection chain.
Static check only (per operator request).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements docs/requirements/021-sso-flavor-migration.md atomically:
* Schema: every role's meta/services.yml replaces the parallel
`oauth2:` / `oidc:` top-level blocks with a single `sso:` block
carrying `enabled`, `shared`, and a `flavor` discriminator
(`oidc` | `oauth2` | `saml`; default `oidc`). Flavor-specific
sub-keys live under `sso.<flavor>.*` (`sso.oauth2.{origin,acl,
allowed_groups}`, `sso.oidc.plugin`).
* Provider role: roles/web-app-oauth2-proxy/ is deleted in its
entirety; its 5 sidecar templates move under
roles/web-app-keycloak/templates/sso_proxy/, and the per-consumer
rendering task lives at roles/web-app-keycloak/tasks/sso_proxy.yml.
web-app-keycloak now declares `provides: sso`.
* Runtime: new resolver utils/roles/applications/services/sso.py
centralises the SSO predicates (`is_enabled`, `is_proxy_gated`,
`is_oidc_native`) and the oauth2 sub-values
(`oauth2_origin_{host,port}`, `oauth2_acl`, `oauth2_allowed_groups`).
Three lookup plugins consume the resolver: `sso` (single-app
predicate access), `sso_proxy_consumers` (enumerates oauth2-flavored
consumers), `sso_oidc_plugin` (renamed from `oidc_flavor`, Nextcloud
plugin selector). sys-stk-backend, sys-svc-compose, sys-svc-proxy
and plugins/filter/compose_volumes.py all gate on
`lookup('sso', ..., 'is_proxy_gated')` instead of manually combining
`enabled` and `flavor`.
* Identifier renames repo-wide: `services.<entity>.ports.local.oauth2`
→ `ports.local.sso`; `PORT_BANDS.local.oauth2` → `PORT_BANDS.local.sso`;
`OAUTH2_PROXY_*` env/Jinja vars → `SSO_PROXY_*`;
`oauth2_proxy_cookie_secret` → `sso_proxy_cookie_secret`;
`OAUTH2_SERVICE_ENABLED` + `OIDC_SERVICE_ENABLED` collapse into
`SSO_SERVICE_ENABLED`; Playwright gating helpers
(isServiceEnabled/skipUnlessServiceEnabled/requireService/safeIsEnabled/
safeSkipUnlessEnabled) move from `oidc`/`oauth2` to `sso`.
* Dual-block resolution: per req's Background-Table, web-app-bookwyrm
is `flavor: oauth2`; web-app-gitea and web-app-friendica keep
`flavor: oauth2` with their existing `oauth2.acl.blacklist`
(/user/login) and `oauth2.acl.whitelist` (federation + discovery
endpoints) — preserving the operative oauth2-proxy gating.
* Tests: the two mutual-exclusion guards
(tests/integration/iam/oauth2_oidc/test_mutual_exclusive.py and
test_acl_mutual_exclusive.py) are deleted per Decision #10;
test_oauth2_contract.py is renamed and rewritten to
test_sso_contract.py, asserting that `flavor: oauth2` roles carry
`oauth2.origin.{host,port}` plus `ports.local.sso`; test_sso.py
(web-app integration) now requires the unified `sso:` block with an
explicit flavor or a `# nocheck: sso` opt-out; new
tests/lint/repository/no_legacy_sso_paths/ guards the source tree
against re-introducing the legacy strings.
* Docs: req 21 itself has every AC checked off, and reqs 006, 008,
009, 013, 015, 016, 019, 020 plus 10+ role READMEs and
docs/contributing/actions/testing/suppression.md are updated to the
unified SSO naming.
Empirically validated end-to-end against the validation app set
(web-app-keycloak, web-app-nextcloud, web-app-bookwyrm, web-app-gitea,
web-app-friendica, web-app-dashboard) in matrix variants 0 and 1; v2
also passes the 4 target roles' Playwright after a documented
pre-existing KC_HOSTNAME-drift workaround (separate concern, not
caused by this refactor).
BREAKING CHANGE: every consumer of `services.oauth2.*`,
`services.oidc.*`, `OAUTH2_PROXY_*`, `OAUTH2_SERVICE_ENABLED`,
`OIDC_SERVICE_ENABLED`, `oauth2_proxy_cookie_secret`,
`oauth2_proxy_application_id`, `ports.local.oauth2`, the
`web-app-oauth2-proxy` role, or the legacy gating helper service names
(`oidc`/`oauth2`) must migrate to the unified `sso` shape documented
in docs/requirements/021-sso-flavor-migration.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NC 33's login form labels the username textbox simply "Account name"
(no "or email" suffix), so the prior regex matched nothing and every
login-via-native-form spec timed out at the 5-s default. Broaden the
pattern to accept either label shape.
The biber-LDAP scenario also failed at the logout assertion: the
universal-logout patch from sys-front-inj-logout rewrites the click
target to Keycloak's logout endpoint, which in user_ldap-only mode
ends on Keycloak's "You are logged out" info page rather than NC's
/login. Replace the negative shell-absence check with a positive
poll on the Keycloak logout URL that matches what the universal-logout
patch actually does.
meta/services.yml — switch `lookup('config', application_id, ...)` to the
spelled-out role id `'web-app-nextcloud'`. `application_id` is undefined
when meta/services.yml is rendered (the applications registry is built
before any role-vars binding), which made the lookup return the raw
unrendered Jinja string instead of the resolved oidc.enabled value, and
in turn flipped legacy_login_mask.enabled to false in variant 1 where
it should have been true. The matching `twofactor_*` plugin entries in
the same file already use the hardcoded form for the same reason.
files/playwright/_shared.js — anchor `nextcloudCredentialSubmitPattern`
to `^(sign in|log in)$` so it matches only the credential-form submit
button. With auth.webauthn.enabled=true (the post-fix variant-1 state)
Nextcloud renders an additional `Log in with a device` button next to
the credential `Log in`, which the previous unanchored pattern matched
too — strict-mode violation, test fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ESLint reported 2 errors after the playwright-spec split refactors:
- `playwright/no-wait-for-navigation` rule "not found" at the inline
disable directive in web-app-fediwall/_shared.js, because the
playwright plugin block was scoped to `playwright.spec.js` only.
- `enterNextcloudLoginThroughVisibleEntryPoint` undefined in the
web-app-nextcloud `_shared.js` module.exports — a refactor
(daefcf146) intentionally dropped the helper but left the export
reference behind.
Widen the playwright plugin glob to `roles/**/files/playwright/**/*.js`
so the same rule set applies to the now-common per-test submodules
(_shared.js, test-*.js splits), and remove the dangling export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the Talk env vars, settings-text scraping helpers, spreed
test-button verifier, and the former `verifyNextcloudTalkAdminSettings`
flow out of `_shared.js` into `test-talk-admin-settings.js` so the gate
flag, env-presence guard, and assertion logic live in a single module.
Drop the redundant `verifyNextcloudTalkAdminSettings` calls from the
admin OIDC and native login specs — the dedicated Talk test now owns
that coverage. Add `talk: true` / `talk: false` variant pins for the
new dynamic `services.talk.enabled` flag so coverage stays complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the taiga/pixelfed pattern: `playwright.spec.js` shrinks from a
834-LOC monolith to a 28-LOC orchestrator that wires `beforeEach` and
require()s one test module per scenario; all shared env, locator,
modal, login/logout and Talk-admin verification logic moves to
`_shared.js`. Each test stays atomar and individually inspectable via
its filename.
Login coverage is now persona × flavor instead of one monolithic
"nextcloud login" that implicitly assumed OIDC:
- test-login-admin-oidc.js skip \!oidcEnabled
- test-login-admin-native.js skip oidcEnabled (uses
NEXTCLOUD_DIRECT_LOGIN_PASSWORD
via the shared
login helper)
- test-login-biber-oidc.js skip \!oidcEnabled
- test-login-biber-ldap.js skip oidcEnabled || \!ldapEnabled (biber is
not autoprovisioned
in pure-native NC)
Persona scenarios (guest / biber / administrator post-login flows) keep
delegating to the shared `runGuestFlow` / `runBiberFlow` / `runAdminFlow`
helpers and stay universal across all variants.
Also drops an unused helper (`enterNextcloudLoginThroughVisibleEntryPoint`)
that the deleted always-on `test-nextcloud-login.js` was the only
consumer of.
Removes direct process.env.<X>_SERVICE_ENABLED reads (and bespoke
per-role <ROLE>_OIDC_ENABLED / SERVICE_* / OIDC_ENABLED env flags) in
favor of skipUnlessServiceEnabled/isServiceEnabled from
roles/test-e2e-playwright/files/service-gating.js, per
docs/contributing/artefact/files/role/playwright.specs.js.md service-
gating contract.
Per-role changes:
- discourse, matrix, dashboard: replace process.env.<X>_SERVICE_ENABLED
with the helper; drop now-redundant nocheck markers on the canonical
env flags.
- taiga: drop TAIGA_OAUTH2_ENABLED / TAIGA_OIDC_ENABLED env lines;
consume OAUTH2_SERVICE_ENABLED / OIDC_SERVICE_ENABLED via the helper.
- opentalk, opencloud, nextcloud: drop <ROLE>_OIDC_ENABLED (and
NEXTCLOUD_LDAP_ENABLED) env lines; spec switches to
isServiceEnabled('oidc'|'ldap').
- moodle: drop SERVICE_OIDC / SERVICE_LDAP / SERVICE_LAM in favor of
the canonical OIDC_SERVICE_ENABLED / LDAP_SERVICE_ENABLED plus a new
LAM_SERVICE_ENABLED rendered from 'web-app-lam' group membership.
- decidim: drop OIDC_ENABLED env line; SSO-button-visible scenario
uses skipUnlessServiceEnabled('oidc').
Dashboard asset test split:
The previously bundled "dashboard loads core css, javascript,
simpleicons, and logo assets" test triple-gated (css/cdn/logout)
skipped the entire surface when any single shared service was off.
Split into six focused tests so disabling one service skips only its
specific assertion:
- shared CSS injection (gate: css)
- CDN-served role-local stylesheet (gate: cdn)
- logout.js injection (gate: logout)
- simpleicon-backed cards (gate: simpleicons; nocheck dropped)
- role-core JS modules + logos (ungated, role-core)
- iframe-sync postMessage handler (ungated, role-core)
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>
Five role specs (dashboard, nextcloud, odoo, pixelfed, taiga)
duplicated four small utility helpers across one-cluster-each
inline copies:
- isVisible(locator) — `locator.first().isVisible()`
with the strict-mode / detached
error swallowed.
- waitForFrameUrl(iframe, ...) — poll the embedded iframe URL
until it contains a matcher.
- findFirstVisibleCandidate(list) — pick the first visible
candidate from a label/locator
list and return the resolved
single-element locator.
- escapeRegex(value) — escape regex metacharacters
so a raw string can be used as
a `new RegExp(...)` source.
Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dom.js` collects
all four helpers (canonical 1-cluster bodies, byte-equivalent to the
former inline copies).
- `personas/utils/index.js` re-exports the new module so callers reach
the helpers via `require("./personas")`.
- The five specs strip their inline definitions and amend the
destructuring with whichever helpers their body still references.
Drift handling
--------------
None. All four helpers were 1-cluster across the call sites; this is
mechanical substitution.
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).
- 0 inline `function isVisible|waitForFrameUrl|findFirstVisibleCandidate|escapeRegex`
definitions remain in any role spec.
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>
Adds runGuestFlow / runBiberFlow / runAdminFlow scenarios to the
nextcloud spec, with admin-flow driving Administration / Users / Apps /
Files; meta/services.yml + playwright.env.j2 updated to gate and feed
the new flow.
Reason: continuation of the persona-library rollout (d3e5ebe44 →
239c0b71e); finishes the nextcloud entry against the central library
so the new lint cannot find a stub-shaped body here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`oidc_login_auto_redirect=true` is the happy path on /login: the
pulsejet/nextcloud-oidc-login boot hook bounces straight to Keycloak.
Under cold first-deploy load that hook can lose its `header('Location:...')`
when an earlier hook has already emitted output, and /login renders the
NC chrome with the password form hidden but the OIDC alt-login button
(`<a href="/apps/oidc_login/oidc" class="oidc-button">`) visible.
Treat that button as a valid entry point so the spec stays green
regardless of whether auto-redirect actually fires. The existing
`kind === "social-login"` branch in `loginToStandaloneNextcloud` already
clicks through to Keycloak and waits for the credential form, so adding
the candidates is the only required wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two failures surfaced once the metadata-plugin fix stopped aborting
the Deploy web-app-nextcloud CI job before the Playwright suite ran:
✘ nextcloud talk admin settings (3 retries, 53s each)
✘ dashboard to nextcloud login (3 retries, 60s each)
Both failures funneled through `clickAllTalkTestServerButtonsAndVerify`
expecting the regex
`/OK:\s*Running version:\s*\S+[\s\S]*?OK:\s*Running version:\s*\S+/i`
to match `document.body.innerText` — i.e. two consecutive
`"OK: Running version: <ver>"` markers. The diagnostic from a fresh
deploy showed the actual rendered Talk-admin state:
Warning: Running version: 2.0.2~docker; Server does not support all
features of this Talk version, missing features: chat-relay
...
OK: Running version: 0.1 (the recording backend)
i.e. spreed's <SignalingServer> row reports `warningMessage`
("missing features: chat-relay") instead of the OK string, while the
<RecordingServer> row reports OK. spreed's `connectionState` getter
prefers `warningMessage` over the OK string by design (see
src/components/AdminSettings/SignalingServer.vue:136-150 of upstream
spreed), so the two-OK requirement is structurally false-fail-prone
on a healthy stack: a missing feature on the HPB image would always
turn this test red.
Fix the spec, not the deploy:
- Wait for `span.test-connection` to attach before counting buttons.
spreed's Test button is `v-if="server && checked"` and only appears
AFTER the auto-mounted `checkServerVersion()` call returns. Polling
too early found zero buttons, so `clicked.length > 0` could only
pass on a lucky race.
- Read the `<span class="test-connection">` texts directly via
`connectionSpans.allInnerTexts()` instead of pulling
`document.body.innerText`. The span is the canonical place spreed
writes connectionState, so the failure message can name the exact
rendered string ("Warning: ...", "Status: Checking connection",
"Error: ...") instead of dumping the whole admin chrome.
- Require ONE OK marker (recording row) instead of two. The
RecordingServer row only renders when `hasSignalingServers` is
true, so on a freshly-bootstrapped stack the recording row can be
the only one with a clean OK while the HPB row legitimately reports
a warning. Asserting both would block a healthy install on a
cosmetic feature mismatch.
- Iterate connection-span texts in the error-pattern check so the
failure message points at the exact row that broke instead of
matching against the whole body.
Verified locally via
`SERVICES_DISABLED='matomo,email' make deploy-reuse-kept-apps APPS=web-app-nextcloud`:
✓ nextcloud talk admin settings (23.8s)
✓ dashboard to nextcloud login (29.7s)
✓ biber logs into nextcloud via OIDC and logs out (4.7s)
PLAY RECAP: ok=1728, changed=131, failed=0, rescued=0, skipped=643.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`#user-menu button` is non-strict on Nextcloud 32+: the wrapper
`#user-menu` contains both the trigger button (aria-label "Settings
menu") AND the submenu's own buttons once it has been opened. The
biber OIDC logout test in roles/web-app-nextcloud/files/playwright.spec.js
then strict-mode-violated with:
locator.click: Error: strict mode violation:
locator('#user-menu button') resolved to 2 elements:
1) <button ... aria-label="Settings menu" ...>
2) <button ... class="button-vue button-vue--size-normal
button-vue--icon-only button-vue--vue-secondary">
Pin the locator to the trigger via its aria-label, with `> button` and
`.first()` fallbacks so localized aria-labels still work.
Surfaced once the metadata-plugin failure stopped aborting the deploy
before the test phase ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).
req-008 — role meta layout
* Move every role's metadata under meta/<topic>.yml with file-root
convention (no wrapping key matching the basename):
- schema/main.yml + config/main.yml.credentials → meta/schema.yml
- users/main.yml → meta/users.yml
- config/main.yml.server → meta/server.yml
- config/main.yml.rbac → meta/rbac.yml
- config/main.yml.compose.services + every other top-level key
of config/main.yml inlined under <entity>.<key>
→ meta/services.yml
- config/main.yml.compose.volumes → meta/volumes.yml
* Drop the legacy schema/, users/, config/ directories from every role.
* meta/schema.yml: support nested credential keys, optional `default:`
field (Jinja literal, not rendered/validated/regenerated), implicit
`algorithm: plain`.
* Materialised paths drop the `compose.` prefix:
compose.services.<e>.<…> → services.<e>.<…>
compose.volumes.<k> → volumes.<k>
<former-top-level>.<…> → services.<entity_name>.<former-top-level>.<…>
* Update every Python consumer (utils/cache, utils/manager, utils/applications,
plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
and task file to the new paths.
* No legacy fallback. Repository-wide grep for `compose.services.` /
`compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
* Add tests/lint/repository/test_role_meta_layout.py guarding against the
old paths returning, and an explicit lint guard that meta/variants.yml
files do not carry a `compose:` wrapper.
req-009 — per-role networks/ports
* Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
dns_resolver}.
* Delete group_vars/all/09_ports.yml entirely; ports now live on the
service entity that exposes them at
roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
* Rename + int-cast every former top-level `services.<entity>.port`
field to `services.<entity>.ports.inter` (single int).
* Always-category-keyed maps for ports.local / ports.public; relay
spans use {start, end} integers.
* Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
of truth for the per-<scope>.<category> band ranges, consumed by both
the new CLI helpers and the lint test.
* New CLI helpers (cli meta ports suggest, cli meta networks suggest)
walk the live role tree gap-first then increment-fallback, with
deterministic output and non-zero exit on capacity overflow.
* cli create role consumes both helpers when scaffolding a new role.
* Lint extensions: bare-int port shape rejection, host-bound port
collision check (single ints + relay span integers, inter excluded),
band-membership check with documented allow-list for the legacy
web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.
req-010 — run_after / lifecycle migration
* Move galaxy_info.run_after and galaxy_info.lifecycle out of
meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
lifecycle}; <primary_entity> is the value returned by
get_entity_name(role_name).
* For multi-entity roles whose primary entity has no compose service
(web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
gen roles), create a dedicated metadata-only top-level entry under
<primary_entity>.
* Introduce utils/roles/meta_lookup.py with get_role_run_after /
get_role_lifecycle that resolve <primary_entity> once and read from
meta/services.yml; degrade gracefully when meta/services.yml is
absent OR the field is absent; raise MetaServicesShapeError when the
file exists but is malformed.
* Update every consumer (utils/service_registry, utils/invokable,
cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
use the helper.
* Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
run_after/lifecycle never on a non-primary entity; empty run_after
lists rejected.
Also folded into this change set
* services_disabler now writes `services.<svc>.{enabled,shared}` into
host_vars instead of the old `compose.services.<svc>.{...}` path; the
discover/find pair walks meta/services.yml and only treats entries
carrying enabled/shared/provides as provider declarations (so
metadata-only entries from req-010 do not create spurious provider
rows).
* service_registry skips `run_after` deps whose target was filtered
out via SERVICES_DISABLED instead of failing the whole load.
* cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
consume the new applications.<role>.services.<svc>.image shape
(mirror.md updated to match).
* utils/domains/list.build_applications_from_roles now reads
meta/server.yml directly (it had been silently returning {} since
config/main.yml stopped existing).
* utils/roles/resource_validation.filter_roles_by_min_storage reads
meta/services.yml.<entity>.min_storage instead of the legacy
compose.services path.
* web-app-bluesky pre-clones the social-app build context BEFORE
sys-svc-compose so the build context is on disk by the time
sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
* 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
web-app-wordpress, web-app-keycloak) lose their `compose: services:`
wrapper so deep-merge over meta/services.yml lands on the new
file-root shape.
Validation
* Unit tests: 1571 passed.
* Lint tests: 50 passed.
* Integration tests: 161 passed (1 skipped).
* Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
discourse,mattermost}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- playwright.spec.js: add "biber logs into nextcloud via OIDC and logs
out" scenario covering the non-administrator persona required by the
MUSTs; parameterize loginToStandaloneNextcloud on username/password,
add a logoutStandaloneNextcloud helper, and wrap biber's first login
in a retry so the LDAP first-login-provisioning delay does not flake.
- playwright.env.j2: expose BIBER_USERNAME and BIBER_PASSWORD so the
spec can read biber credentials from the rendered env.
- docs/contributing/.../playwright.specs.js.md: codify the MUST that
existing scenarios may not be deleted while optimizing a spec.
- roles/web-app-nextcloud/docs/LDAP.md: document the biber first-login
caveat against fresh Nextcloud + LDAP deployments.
Validated via scripts/tests/e2e/rerun-spec.sh web-app-nextcloud:
3 passed (talk admin settings, dashboard to nextcloud login, biber).
Expose compose.services.{oidc,ldap}.enabled as NEXTCLOUD_OIDC_ENABLED and
NEXTCLOUD_LDAP_ENABLED in the Playwright env, and derive a single
`nextcloudLoginFlavor` in the spec:
- OIDC + LDAP -> "oidc_login" (pulsejet/nextcloud-oidc-login,
auto_redirect hands straight to Keycloak)
- OIDC only -> "sociallogin" (nextcloud/sociallogin shows a
"Log in with Keycloak" entry first)
- no OIDC -> "native" (no Keycloak handoff; NC credential form
filled with the direct admin password)
loginToStandaloneNextcloud now builds its initial-state candidate list
per flavor instead of accepting every possible login UI shape, and the
native flavor uses NEXTCLOUD_DIRECT_LOGIN_PASSWORD rather than the
Keycloak user password.
Verified via reuse-kept redeploy (make deploy-reuse-kept-apps
APPS=web-app-nextcloud SERVICES_DISABLED="css matomo email"): both
Playwright scenarios pass (2 passed, 26.3s).
Beyond the existing error-string absence check, require the Talk admin page
to render at least two "OK: Running version: X" markers after the test
buttons are clicked. Those markers come from the auto-run reachability
checks that spreed fires on mount for the HPB signaling server and the
recording backend. TURN/STUN are left to the error-absence check because
the Playwright container can't rely on WebRTC UDP reachability to coturn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the Talk admin Playwright check to also click every "Test server" /
"Test this server" button on the admin page and fail if any of the known
spreed error strings (Cannot connect, No working ICE, Server seems to be a
Signaling server, Testing server seems to be broken) appear afterwards. This
verifies the HPB signaling / STUN / TURN endpoints are actually reachable,
not only that the configured values are rendered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite loginToStandaloneNextcloud to drive the OIDC flow (Nextcloud -> Keycloak
-> Nextcloud) instead of the native credential form, which is hidden by
oidc_login_hide_password_form. Add section-divider and WHY comments to make the
spec easier to read.
Provide defaults for the compose.services.oidc.flavor lookup so oidc_login stays
enabled when the key is absent, and default user_ldap to enabled when the LDAP
service config is missing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- plugins/lookup/config.py: honor default by setting skip_missing_app=not strict
so lookup('config', app, path, default) returns the default when the app is
missing from the applications dict, instead of raising during early constructor
phases. Fixes 5 test-development jobs.
- roles/web-app-nextcloud/files/playwright.spec.js: replace unsupported
.toSatisfy() with a boolean check inside expect.poll() + .toBe(true).
Fixes Deploy web-app-nextcloud.
- roles/sys-svc-container/files/container.py: when the target image has no
ENTRYPOINT, inject the internal CA as a volume plus NODE_EXTRA_CA_CERTS,
SSL_CERT_FILE, REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE instead of falling
back to a plain 'docker run' without CA trust. Fixes npm
UNABLE_TO_VERIFY_LEAF_SIGNATURE inside Microsoft Playwright images for
moodle, prometheus and onlyoffice deploys.
- tests/unit/roles/sys-svc-container/files/test_container.py: cover the new
no-ENTRYPOINT CA-injection path.
Note: web-app-mini-qr 'lookup ghcr.io: no such host' is not addressed here
and likely needs a separate investigation of CoreDNS forwarding on the CI
runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 07_admin_identities.yml: resolves LDAP-backed admin identities
for OCC calls that require the actual NC username, not the LDAP DN
- vars: add NEXTCLOUD_HPB_TURN/STUN_ACTIVE_CONFIG selecting standalone
or onboard TURN based on enabled flags; add TALK_ADMIN_URL
- config: talk container gated on spreed plugin flag
- env.j2 + playwright.env.j2: expose active TURN/STUN config and
Talk admin URL for container env and E2E tests
- playwright.spec.js: extend with Talk TURN/STUN E2E assertions
- schema: add spreed.enabled validation
- coturn compose: move relay port range before TLS block so ports are
always set regardless of TLS mode
- docs/requirements: document TURN relay requirement
- test_resolver: cover new resolution paths
- Increase global test timeout from 60s to 300s; the OIDC login flow
alone can consume the full 60s budget, causing timeouts in subsequent
wait steps (waitForVisibleCandidate at line 171 in the CI logs)
- Replace non-existent NC 30+ selectors verified against the actual
nextcloud:production-fpm-alpine image:
- remove [aria-controls="header-menu-user-menu"] (not in NC 30+)
- remove settingsMenuButton role "Settings menu" (not confirmed)
- replace #content-vue / #appmenu with #header-start__appmenu
(layout.user.php uses <nav id="header-start__appmenu">)
- remove #logout (no such ID in layout.user.php)
- Dismiss the firstrunwizard modal before clicking the user menu:
firstrunwizard-activate.mjs opens a NcModal after DOMContentLoaded
on first login; its .modal-mask intercepts all clicks and would
cause the user-menu click to fail silently on first login
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>