Filter no longer takes a `database_volume=` parameter. The volume name is
derived from the central provider's `services.<dbtype>.name` (shared) or
`<entity>_database` (dedicated) — mirroring `plugins/lookup/database.py`'s
`volume_prefix + host` construction. Simultaneous postgres + mariadb on
the same role now raises an AnsibleFilterError with a clear message;
support is deferred per the docstring TODO.
The integration test (renamed from `test_shared_service_volumes.py` to
`test_compose_volumes_call_required.py`) now requires every
`roles/*/templates/compose.yml.j2` to call `compose_volumes`, so the
top-level `volumes:` block is always rendered. Without it, flipping a
shared service to dedicated mode adds a named-volume reference that
Compose rejects with `service "<name>" refers to undefined volume`.
Adds the call to 14 templates that were missing it (dashboard, hugo,
sphinx, bridgy-fed, fediwall, littlejs, mig, mini-qr, navigator,
roulette-wheel, simpleicons, collabora, logout, openresty) and strips
the `database_volume=...` argument from every other role plus Taiga's
now-unused `TAIGA_DATABASE_VOLUME` var.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keycloak's compose.yml.j2 included the embedded postgres service via
`sys-svc-rdbms/templates/services/postgres.yml.j2`, but never rendered
a top-level `volumes:` block. The embedded postgres template
references a named volume (`source: database`), so when
`postgres.shared=false` (dedicated DB, e.g. the variant-1 matrix run)
`compose config` failed validation with
`service "database" refers to undefined volume database`, blocking the
deploy at the compose-pull handler. Call `compose_volumes` like every
other consumer role (bookwyrm, nextcloud, …) so the top-level block
is emitted unconditionally — empty in the shared-DB variant, with the
`database` volume declared in the dedicated-DB variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`lookup('database', <consumer>, 'image')` previously returned the bare
dbtype (`postgres`), so an embedded dedicated database
(`postgres.shared=false`) rendered `image: postgres:<svc-db-postgres
version>` even when the central provider shipped a different image
(`svc-db-postgres` declares `postgis/postgis`). `compose pull` then
failed with `manifest unknown` because `postgres:17-3.5` does not
exist on Docker Hub. Look the image up from the central provider's
`services.<dbtype>.image` and fall back to the bare dbtype only when
the provider declares none, so dedicated instances ship the same
image as the shared provider.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Set a `keycloakRoundTripCompleted` flag once `performKeycloakLogin`
has returned and the page is on the role's canonical hostname.
oauth2-proxy-gated roles whose upstream UI ships no Account/Logout
menu (Prometheus, status pages, raw upstream surfaces) now satisfy
the persona auth-marker without a probe-able in-app control. The
visible-marker poll still runs for roles that DO surface one.
* Add `[data-injected-logout]` as the first logout candidate so the
shared `inAppLogout` helper picks up the per-role JS-injected
fallback button (e.g. the Prometheus navbar patch) before falling
through to the regex-driven candidates.
* Replace the implicit "snapshot isVisible" gate at the top of
`inAppLogout` with a `waitForAnyLogoutCandidate` poll that pings the
candidate set for up to 10 s. Required because the universal /
per-role logout helpers inject their button asynchronously after
DOMContentLoaded plus a MutationObserver settle; the previous
synchronous check failed before the button could mount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Prometheus UI ships no in-app logout entry, so an oauth2-proxy-
gated operator can authenticate but never reach the universal-logout
chain from inside the app. Add a per-role `templates/javascript.js.j2`
(activated via `services.javascript.enabled=shared=true` in
`meta/services.yml`) that injects a Logout SVG button to the right of
Prometheus' existing icon group (theme toggle, settings, docs) once
the navbar has mounted. The injection is gated server-side on
`services.oauth2.enabled` because oauth2-proxy session cookies are
`HttpOnly` and unreachable from `document.cookie`. The button carries
`data-injected-logout` so the persona helpers can find it via the
shared `inAppLogout` candidates list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the upstream navigation fix that renders
`data-bs-toggle="dropdown"` on top-level dropdown toggles, so
Bootstrap 5 binds the click handler natively and the Apps / Account
dropdowns open reliably on click. Drops the runtime JS stopgap the
previous version needed, and clears the now-resolved entries from the
role's TODO.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wait for the role's Login link with `Locator.waitFor` instead of the
snapshot-only `isVisible` — `isVisible({timeout})` only governs locator
resolution and never polls for visibility, so an OIDC-driven UI that
injects the Login link asynchronously (e.g. dashboard's `oidc.js`
fetched at runtime from the CDN) was treated as logged-out and the
helper returned in milliseconds without ever clicking the link.
* Once the link is visible, prefer a direct `page.goto(href)` to the
embedded openid-connect/auth URL when the link exposes one; fall back
to a real click plus `page.waitForURL(/openid-connect\/auth/)` so a
JS-handler-intercepted click (keycloak adapter, single-page-app shell)
still drives the test through Keycloak.
* Restrict the post-login URL fallback to nested frames. The main
frame can park on the role's canonical domain without an authenticated
session (the role's auth chain failed silently), so accepting the main
frame URL as proof of authentication produced false positives. The
iframe fallback still covers the legitimate "app surface rendered via
embed" case.
* Broaden the auth marker so role=link account/profile/user-menu
controls and `aria-label='account'`, `aria-haspopup`, Bootstrap
`data-bs-toggle='dropdown'`, and a `href*='logout|end_session'` link
count as the post-login surface signal. Roles that hide the auth
marker behind a closed dropdown still surface a probe-able anchor.
* Rebuild `inAppLogout` to iterate every visible menu trigger (Bootstrap
dropdowns, `.user-menu-toggle`, `data-testid='user'`, ARIA-popup
controls), not just the first match. After each click the helper
polls for logout/menuitem/link candidates inside the dropdown,
closes the menu again before trying the next trigger, and falls back
to a top-level `a[href*='logout']` so static-only logout links work
even when no dropdown wraps them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-product Atlassian URLs are unstable; point at the root domain so
the link does not rot when Atlassian reorganises the product pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shared biber/admin persona helpers in
roles/test-e2e-playwright/files/personas/*.js collapse the persona
scenario when either APP_BASE_URL or CANONICAL_DOMAIN is empty. The
twelve roles below shipped their env template without
CANONICAL_DOMAIN, so the helper fell into the auth-less collapse
branch and silently `test.skip()`-ed the persona scenarios despite
the roles having an actual Keycloak-backed auth surface. Render
CANONICAL_DOMAIN on every role whose spec drives a real persona so
the scenario runs end-to-end and a real regression cannot hide
behind a silent skip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the verbose
`(lookup('applications', application_id).server.domains.canonical | first)`
chain with the dedicated `lookup('domain', application_id)` plugin
across every playwright.env.j2 that renders `CANONICAL_DOMAIN`. The
helper already exists at plugins/lookup/domain.py and resolves the
canonical primary domain via the cached domain-merge pipeline, so the
inline canonical-list traversal is unnecessary noise that obscures
the actual contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render BIBER_USERNAME/BIBER_PASSWORD on the role-local env templates
that drive the shared biber persona helper and, where the
administrator branch shares the same template, ADMIN_USERNAME/
ADMIN_PASSWORD too. Without these the shared persona helper read an
empty credential pair, skipped the Keycloak login step, parked on the
auth page, and the auth-marker assertion failed loudly with no clue
that the cause was a missing env declaration.
For web-app-pixelfed the previous indirection through
PIXELFED_PLAYWRIGHT_FIRST_USER_KEY / _SECOND_USER_KEY rendered the
same biber/administrator credentials under the role-local
LOGIN_USERNAME_{FIRST,SECOND} aliases. Drop the indirection and
consume BIBER_*/ADMIN_* directly in the role's spec so the env
contract matches the rest of the persona-driven roles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bridgy-fed, hugo, mig, and sphinx have no local auth surface
(static-site generators, an in-app module of web-app-dashboard, and a
federation-only bridge). The shared biber/admin persona helpers
otherwise park on a non-existent login form and fail loudly; declare
the explicit opt-out so they skip cleanly and surface a real
regression only when an actual auth surface appears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the preload condition so a provider role is loaded by the
service-loader when it sits in the round's group_names, not only when
some enabled consumer requires its shared service. In matrix-deploy
rounds where every consumer flips the service to disabled (e.g. a
variant collapsing email/oidc/matomo to false), the provider role is
still in the inventory and its own tasks rely on the preload token,
so the previous narrow `required`-only check left the provider
unloaded and asserts further down the chain failed.
Drop the now-obsolete sys-svc-mail ↔ web-app-mailu preload-ordering
TODO line: the assertion that fired when mailu sat in group_names
but no consumer asked for email is resolved by the new branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The env-keys lint previously matched only direct `process.env.<KEY>`
references. Persona helpers under roles/test-e2e-playwright/files/
personas/ read most env keys via a shared `readEnv("KEY")` wrapper, so
keys consumed only through the wrapper were flagged as unreferenced.
Extend the pattern so the wrapper counts as a real consumer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor commit `64ff6ecf` moved
`roles/sys-svc-compose/tasks/utils/network.yml` →
`utils/network/routine.yml` and `network_create.yml` →
`utils/network/create.yml`, and added two new include sites
(svc-db-openldap + web-app-prometheus + the wrapper-internal call).
ansible-lint statically resolves the `{{ playbook_dir }} | path_join`
include paths and reports each as a load-failure under the caller's
`tasks/roles/...` virtual subdir; the per-file allowlist needs the new
literal strings. Updates the three changed entries and adds three
new ones, none of which are real missing files at runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the network-create helper out of the all-in-one wrapper so roles
can pick the right entry point:
utils/network/create.yml network-only, derives name + subnet from
`network_role_id` (use when the role still
needs to do work between the network create
and the compose-up, or when pre-creating
foreign networks in a loop).
utils/network/routine.yml former wrapper, now delegates to create.yml
and re-flushes sys-svc-compose right after
(use for self-contained single-container
services).
A new lint
`tests/lint/ansible/test_network_create_via_util.py` forbids ad-hoc
`community.docker.docker_network` calls or `docker network create` /
`container network create` shell-outs anywhere in `roles/` outside
`utils/network/create.yml`. The existing 5 callers (mariadb, postgres,
ollama, openldap, web-app-prometheus) move to the new layout and shed
the now-redundant `docker_network_name` / `docker_network_subnet`
caller-side vars; that retires 5 unused role constants
(MARIADB_NETWORK / MARIADB_SUBNET / POSTGRES_SUBNET / OLLAMA_NETWORK /
OPENLDAP_NETWORK).
The `web-app-prometheus` task gains a pre-create loop over
`lookup('native_metrics_apps')` so the deploy unblocks the bootstrap
deadlock observed on test-deploy-server CI run 25634428082 for
web-app-gitea and web-app-matrix (`network <X> declared as external,
but could not be found`): prometheus is loaded as a shared service
*before* the target role, so its `external: true` references to the
target's network needed pre-creation to satisfy compose-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in one config option:
1. `SiteSetting.enable_local_logins = {{ ... | bool | string }}` emitted
Python's capitalized `True`/`False`, which Ruby parses as undefined
constants → `uninitialized constant True` aborts `./launcher rebuild`
(CI run 25634428082 deploy job for web-app-discourse).
2. `enable_local_logins` was set to the *raw* `disable_local_logins`
value, so when the config truly meant "disable", the live setting
was stuck on "enable", and vice versa.
Renames the field across `meta/services.yml`, `vars/main.yml`, and
`templates/config.yml.j2` to its positive form and applies De Morgan to
keep the original semantics:
disable_local_logins = oidc.enabled AND NOT ldap.enabled
local_login_enabled = NOT oidc.enabled OR ldap.enabled
Truth table is preserved across all four (oidc, ldap) combinations; the
LDAP-needs-the-local-mask path stays open.
The template now uses `| string | lower` so the rendered Ruby literal
is `true`/`false`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-formatter follow-up to c53d6ce67: ruff inlines the
`raise TypeError("...")` for `ldap_cmp_id` since the message fits on
one line. No semantic change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/lint/ansible/test_no_redundant_default_on_module_io.py which
flags `<reg>.{stdout,stderr,rc} | default(...)` inside `changed_when:`,
`failed_when:`, or `until:` lines. Those conditionals only fire after
the module has actually executed, so the fallback can never apply --
keeping it just hides bugs and noise.
Cleans up the 21 existing offenders across 14 role files. The taiga
upsert-admin task additionally gains a `not ASYNC_ENABLED and ...`
short-circuit so its async-poll-0 path no longer leans on a silent
empty fallback for stdout/stderr; with the short-circuit, the
`default('')` is genuinely dead code.
Cross-task references (a later task reading a registered result of an
earlier task that may have been skipped via `when:`) stay out of scope
and remain a legitimate use of `| default(...)`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the four sibling tasks gated on `(grp_mapper_id | trim) in
["", "null"]` (Render payload via set_fact / Create / Refresh / Set-id)
with a single `block:` carrying that `when:` once. The set_fact for the
JSON payload is dropped in favour of a new `ldap_roles_mapper_payload`
Jinja filter that deepcopy + overrides the canonical fields and
returns the JSON string directly to `printf '%s' … | kcadm create
components -f -`.
Fixes both `test_excessive_repeated_when_in_tasks_files` (4× duplicate
`when:` → 1×) and `test_blocks_with_when_and_sections_have_max_three_tasks`
(4 → 3 tasks under the block).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The four `Update DB host/name/user/password (idempotent)` tasks shared
identical grep-then-sed logic, but their YAML folded-scalar layout
landed `&& echo CHANGED;` on a separate line in some cases. Inside the
container's dash, `&&` at the start of a line is a syntax error, so
`Update DB name` aborted the deploy whenever the seed value already
differed from the desired one (CI run 25634428082).
Extracted the loop body into roles/web-app-espocrm/files/patch_config_key.sh
(POSIX sh, env-driven contract: CONFIG_KEY/CONFIG_VALUE/CONFIG_FILE,
escapes \, &, |, / before the sed rewrite) and collapsed the four
tasks into a single Ansible loop that pipes the script via
`container exec -i ... sh < {{ role_path }}/files/patch_config_key.sh`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Rule 14 (mandatory log inspection after every deploy cycle: confirm
the intended persona / per-service behavior is wired AND actually
executed) and threads it through the per-role flow + role-closure gate +
verification checklist. Existing assertions stay per "Preserving
existing tests"; the rollout extends, never replaces.
Matrix gains v0/v1/v2 columns sourced directly from each role's
meta/variants.yml (⏳ = untested, flips to ✅ once the per-variant flow
incl. log inspection passes; empty cell = variant not declared).
Drops every "re-derive from `infinito meta roles applications
complexity ...`" reference: the matrix itself is the source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Consolidates image declarations under a single schema: every role's
`<svc>.{image,version}` lives in `meta/services.yml` alongside compose
service entries. The `image` lookup now reads from disk per role,
`iter_role_images` collapses to a single source, and the mirror chain
(resolver + apply_mirror_overrides) drops the dead `images:` branch.
`test-e2e-playwright` resolves `TEST_E2E_PLAYWRIGHT_IMAGE` directly via
`lookup('image', 'playwright', 'ref')` in vars/main.yml; the runtime
set_fact + override-empty-string dance is gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forwards `PLAYWRIGHT_KEEP_ALL` from host env / GitHub Actions repo var
through `TEST_E2E_PLAYWRIGHT_KEEP_ALL` (inventory-overridable) into the
container and flips trace / screenshot / video to `on` for every test
when truthy.
SPOT for the propagation chain lives in
`docs/contributing/actions/testing/playwright.md#artefact-retention-`;
CI Configuration and the agent iteration loops link to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `matomo: biber is denied access at the admin surface` test
expects matomo's `/` to redirect through Keycloak — that requires
oauth2-proxy in front of matomo. Today
`roles/web-app-matomo/meta/services.yml` carries `oauth2.enabled:
false` (TODO at line 12 says "Enable oauth2 for matomo"), so the
URL returns matomo's own UI directly and the redirect-toContain
poll times out.
Path 2 fix: gate the test on `isServiceEnabled("oauth2")` so it
reports `skipped` until matomo's deploy actually wires the proxy.
The test stays in the spec as the canonical contract for "biber
cannot reach Matomo"; once oauth2-proxy lands in matomo's
services.yml the gate flips and the test starts running.
The 019 matrix note for matomo cross-references the parking +
points at the services.yml TODO for the future iteration.
Five fixes against the matomo deploy CI failure mode:
* The persona helpers ([admin.js], [biber.js]) now perform the
Keycloak login on either auth shape — oauth2-proxy gate (the goto
is intercepted and the page lands directly on
`openid-connect/auth`) AND in-app OIDC plugin (Login link → click
→ redirect). Previously the Keycloak step was gated on
`oidcEnabled`, which left oauth2-proxy-only roles like prometheus
with no auth path. The login-link regex also tolerates leading /
trailing whitespace (`/^\s*(log\s*in|...)\s*$/i`), unblocking the
dashboard's `" Login"` link whose accessible name carries an
icon-induced leading space.
* `roles/web-app-keycloak/templates/playwright.env.j2`: declare
`PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`.
The persona stubs in the keycloak spec exist purely to satisfy the
persona-naming lint; Keycloak IS the auth provider so neither
persona has an in-role app surface. The bespoke
master-realm / normal-realm tests already cover the contract.
* `roles/web-app-matomo/templates/playwright.env.j2`: same — matomo's
local login form is not OIDC / oauth2-proxy, so the generic helper
has no useful flow. The bespoke "matomo administrator" test covers
the admin journey.
* `roles/web-app-prometheus/templates/playwright.env.j2`: only
`PERSONA_BIBER_BLOCKED=true`. Prometheus is admin-only via
oauth2-proxy with `allowed_groups:
web-app-prometheus-administrator`; biber's session is denied at the
proxy and biber has no in-app surface to drive a logout from.
Administrator goes through the standard oauth2-proxy → Keycloak
chain. Also rendered the missing `PROMETHEUS_BASE_URL`,
`CANONICAL_DOMAIN`, and pointed `APP_BASE_URL` at prometheus itself
so the persona helper drives the role under test (not the dashboard).
* `roles/web-app-prometheus/files/playwright.spec.js`: read
`process.env.CANONICAL_DOMAIN` directly so the env-keys-used lint
recognises the new env entry as consumed.
The 019 matrix note for prometheus is updated to describe the
biber-only opt-out and the admin's oauth2-proxy chain.
Out of scope: the matomo CSP biber-deny test still fails because
`roles/web-app-matomo/meta/services.yml` has `oauth2.enabled: false`
(TODO at line 12 — wire matomo's oauth2-proxy gate). That is a
deploy-config change, not a test fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major restructure of the per-role matrix and surrounding prose so the
document reflects the current code state (Test A / Test B green
tree-wide, persona-naming + persona-strict + persona-required-envs +
dashboard-integration-scope lints in place, complexity command grew
the `total` priority signal):
* The matrix is now a single 71-row priority queue sorted by
`total` descending (the agent walks top-to-bottom). Strike-through
remains the role-closure progress marker, applied after a
full-cycle deploy plus passing Playwright spec.
* Drop the old per-row "missing flags" column; collapse to
`Role | total | has env | has spec | notes`. The `notes` column
carries role-specific contract context (PERSONA_*_BLOCKED opt-outs
for akaunting / bluesky, admin-only-skip rationale for matomo /
prometheus / keycloak); blank for everyone else.
* Drop the 10 fully-red rows (web-svc-* / web-opt-rdr-* without any
Playwright artefacts) — out of scope until they grow a spec.
* Fix the 27 stale-`❌❌` rows that actually ship a spec but no env
template (auth-less collapse pattern); they now read `❌✅`.
* Drop the now-redundant "Self-gate `# nocheck` list" section (Test
A enforces it; the eleven `[ ]` checkboxes were misleading).
* Drop the separate Tier 12-16 iteration-order plan; the matrix
itself is now the iteration order.
* Re-frame "Closure paths per matrix row" as forward-looking
guidance for future drift (no missing-flag rows currently).
* Trim the Pattern transfer learnings list to consumer-scope items
only; a new closing paragraph explicitly excludes the SPOT-owned
probes (dashboard tile reachability, prometheus scrape parity,
matomo tracker presence, prometheus / matomo deny + accept
checks) that Rule 13 places in the provider specs.
* Fix three "two persona scenarios" / "both" wordings that
contradicted Rule 3's "three persona scenarios" (`guest`,
`biber`, `administrator`).
* Tighten Rule 3's "Enforced by" column: the persona-naming lint is
the role-closure gate for *spec shape*; full role closure also
needs Test A, Test B, strict-mode lint, and a passing full-cycle
deploy.
* Tighten the `dashboard` line in Verification: per Rule 1,
consumers do not render `DASHBOARD_SERVICE_ENABLED=`, so coverage
for that service runs through the dashboard provider's
parameterised tile-reachability test (Rule 13).
* Add three Verification check items for the new persona /
dashboard-scope lints, and add Rule 1's dashboard-scope sub-rule
cross-link to test_dashboard_integration_scope.py.
* Translate the last German tokens in the matrix headers and prose
(Rolle / hat env / hat spec / fehlende Flags) to English.
Four additive changes to `infinito meta roles applications complexity`:
* Render two new columns for the one-hop view — `embeds_direct` /
`consumers_direct` — alongside the existing transitive `embeds` /
`consumers`. The direct fields are independent of `--level`: a
reader can always see what the role embeds / is-embedded-by
without its indirect tail.
* Render a `total` column that sums the four numeric columns. It
serves as a coarse "overall touch points" score; `--sort total`
surfaces the most-coupled roles.
* `--order {asc,desc}` (default `asc`) controls sort direction.
Applies to every `--sort` choice; `--sort total --order desc`
puts the hubs at the top of the table.
* `--filter SUBSTRING` (case-insensitive) keeps only rows whose
role name contains SUBSTRING. The complexity scores are computed
against the full role tree first, then filtering applies to the
rendered rows only — a role's transitive counts still reflect the
whole codebase.
The tuple shape grew by appending the new fields, so existing
index 0..4 assertions in the unit tests stay valid; new
assertions cover the direct-vs-transitive distinction and the
total computation.
Bonus: trailing-newline fix on `roles/web-svc-file/meta/services.yml`
(the autoformat surfaced a missing EOL after the recent dashboard
cleanup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the three persona-contract lints from
`tests/lint/ansible/roles/web-app/playwright/test_persona_*.py` into
their own `persona/` subdirectory and drop the now-redundant
`_persona` filename prefix:
* `test_persona_naming.py` -> `persona/test_naming.py`
* `test_persona_required_envs.py` -> `persona/test_required_envs.py`
* `test_persona_strict_mode.py` -> `persona/test_strict_mode.py`
The new `persona/__init__.py` re-anchors `PROJECT_ROOT` one level
deeper (`parents[7]`). Doc references in requirements 017, 018, and
019 now point at the new paths.
Renames are tracked via `git mv`, so commit history is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fold the recently-landed Playwright lint guards into the three
playwright-related requirement docs so the rules table and per-rule
"Enforced by" column point at the actual mechanical enforcement
rather than at "review (this requirement)".
* 019 Rule 1 now distinguishes the dashboard treatment for
`web-app-*` consumers (per-consumer flag exemption via
`*_TARGET_ROLES_JSON`, owned by the provider's own spec) from the
hard prohibition for non-`web-app-*` roles
(`test_dashboard_integration_scope.py` forbids any truthy
`dashboard.{enabled,shared}` declaration outside `web-app-*`).
* 019 Rule 3 / Rule 11 / Rule 12 now reference
`test_persona_naming.py`, `test_persona_required_envs.py`, and
`test_persona_strict_mode.py` as the enforcing lints.
* 019 "Closure paths" gains a dashboard-scope-exception paragraph:
paths 1 (render flag) and 3 (`# nocheck` annotation) collapse for
non-`web-app-*` roles; only path 2 (drop the entry) or a static
`dashboard: { enabled: false, shared: false }` declaration remain.
* 019 matrix: `dashboard` removed from the "fehlende Flags" column
of `web-svc-cdn`, `web-svc-libretranslate`, and `web-svc-simpleicons`
(no longer a closure target on these roles).
* 019 Verification block: four new green-tree-wide check items for
the persona / dashboard-scope lints.
* 017 + 018 each gain a Policy bullet anchoring the
`<persona>: <flow>` naming convention to 019 Rule 3 and the
`test_persona_naming.py` lint, and 018 explicitly forbids parallel
ungated `ldap login` tests (per 019 Rule 5).
* Touched-rule cleanup: replace existing em-dash clause separators in
Rule 3 and Rule 11 with `;` / `,` per the documentation style guide.
006 stays untouched: it is already a closed retro doc with the
`MODE_CI`-retired banner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`web-svc-collabora` and `web-svc-onlyoffice` carried explicit
`logout: { enabled: false, shared: false }` blocks. They opt the role
out of the universal-logout integration, but `false` is already the
default when the key is absent, so the block adds no information and
the matching `# nocheck: dynamic-flag` annotation only existed to
silence a lint that does not apply to a static-false block. Drop both.
Add `tests/integration/roles/test_dashboard_integration_scope.py`,
which fails when any non-`web-app-*` role declares a `dashboard:`
block in `meta/services.yml` or in a `meta/variants.yml` override
whose `enabled` or `shared` flag is truthy according to the canonical
`is_explicit_truth` helper (literal `True` *or* the
`"{{ '<role>' in group_names }}"` Jinja form).
The dashboard tile grid is reserved for end-user-visible web-app
surfaces. Shared services (`web-svc-*`), system roles (`sys-*`),
desktop roles (`desk-*`), and driver roles (`drv-*`) integrating it
would spam the grid with non-clickable infrastructure entries.
Drop the now-forbidden `dashboard:` blocks from the six web-svc-*
roles that previously gated dashboard tiles via the dynamic-flag form
(both in `meta/services.yml` and in their `meta/variants.yml`
matrix-pinning entries):
* web-svc-collabora
* web-svc-file
* web-svc-html
* web-svc-libretranslate
* web-svc-logout
* web-svc-onlyoffice
Also drop a stray `q` keystroke at the tail of
`web-svc-file/meta/services.yml` that broke YAML parsing once the
trailing `dashboard:` block was removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface the reverse direction of the shared-service dependency graph
in `infinito meta roles applications complexity`. Each row now also
counts and lists the application roles that transitively embed *this*
role (the "consumers" / "consumed-by" closure), with the same
`--level` depth cap as the forward direction.
* Forward closure (column `embeds`, list `services`) and reverse
closure (column `consumers`, list `consumed_by`) share a single
BFS over (forward, reverse) adjacency maps built once per
invocation.
* `--sort` accepts `embeds` (default), `consumers`, or `name`. The
former `points` label is renamed to `embeds` because the column
header reads as a sentence with the role name (e.g. `web-app-foo
embeds 12, consumers 78`), which is sharper than a generic
"points" score.
* `--format cli` (default) renders three numeric columns — name,
embeds, consumers — for a compact terminal view.
`--format json` emits the full payload with the resolved role
lists for each direction.
* Tests cover the chain case in both directions and verify that
`--level` caps the reverse-direction BFS the same way.
Promote `prefer-template` to error in the spec/persona JS rule block
and convert the nine remaining `a + 'literal' + b` URL-builders to
template literals. Behaviour-equivalent; brings stylistic consistency
with the rest of the codebase, which already uses template literals
throughout, and prevents new `+`-concatenations from sneaking past
the lint floor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All eight currently flag zero offenders in the codebase, so promoting
them costs nothing and raises the floor for new code:
* `prefer-const` — `let` is reserved for mutable bindings.
* `no-throw-literal` — only Error instances may be thrown.
* `no-self-compare` — catch `x === x` typos.
* `default-case-last` — `default:` must be the last switch arm.
* `no-eval`, `no-implied-eval`, `no-new-func`, `no-script-url`
forbid string-driven code execution shapes outright.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`make install-lint` failed on every test-environment matrix runner
(manjarolinux/base, debian:bookworm, ubuntu:latest, fedora:latest,
quay.io/centos/centos:latest) with:
npm not found; install Node.js/npm first to get markdownlint-cli2.
make: *** [Makefile:227: install-lint] Error 1
The two npm-driven installers (`install_markdownlint_with_npm` and
`ensure_eslint`) bailed out as soon as `npm` was missing instead of
following the same auto-install pattern used elsewhere in the
script (e.g. `ensure_command shellcheck` / `shfmt` / `ruff`).
* Add an `npm:<manager>` mapping to `install_command`. pacman /
apt-get / dnf / yum get the candidate list `npm nodejs`; brew
gets `node`. `install_package_candidates` returns on the first
candidate that installs, which covers every matrix distro
(Arch's `npm` pulls `nodejs` as a hard dep; Debian/Ubuntu's `npm`
Depends: nodejs; Fedora/EL9+ ship `npm` as a direct package; brew's
`node` bundles npm).
* `install_markdownlint_with_npm` and `ensure_eslint` now call
`ensure_command npm` when `npm` is missing, before failing.
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>
Adds tests/lint/ansible/roles/web-app/playwright/test_persona_naming.py
to fail the build whenever a `web-app-*/files/playwright.spec.js`
omits one of the three persona scenarios required by the SPOT
contract in docs/contributing/artefact/files/role/playwright.specs.js.md
without an explicit suppression.
Two suppression channels are recognised, both already documented in
the SPOT contract:
- PERSONA_<PERSONA>_BLOCKED=true rendered in the role's
templates/playwright.env.j2 (req 019 Rule 11 explicit role-
contract opt-out).
- Auth-less collapse exception: roles that ship no
templates/playwright.env.j2 are not orchestrated by
test-e2e-playwright and the persona contract does not apply.
Without one of those, every web-app-* role MUST contain
`test("guest: …")`, `test("biber: …")`, and `test("administrator: …")`.
Reason
------
The previous commit fixed seven roles that were silently violating
this rule. Without an automated guard, the same regression class
returns whenever a future role is added or an existing spec's test
title is renamed away from the `<persona>:` prefix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven web-app-* specs failed the Rule 3 contract from
docs/contributing/artefact/files/role/playwright.specs.js.md:
- bigbluebutton, decidim, discourse, matrix, openwebui, peertube
each shipped `administrator:` and `biber:` test titles but no
`guest:` test.
- keycloak shipped four detailed admin / biber tests under non-
canonical names (`master-realm super administrator…`, `normal-
realm administrator logs in through account interface…`, etc.)
and no `<persona>: <flow>`-prefixed scenarios at all.
None of the seven declared a `PERSONA_<persona>_BLOCKED=true`
opt-out in their `templates/playwright.env.j2`, and none qualify
for the auth-less collapse exception (every role has an OIDC auth
surface; keycloak IS the auth surface).
Changes
-------
- Each of the six guest-only-missing specs gains a single appended
scenario:
test("guest: public-landing → auth chain → never authenticated", …)
→ runGuestFlow(page);
- Keycloak gains all three persona scenarios (guest, biber,
administrator) using the shared `runGuestFlow` / `runBiberFlow` /
`runAdminFlow` runners. Existing detailed tests stay untouched
alongside the persona contract.
- The `./personas` import in each spec is amended with whichever
runners are now needed.
Verification
------------
- 420 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- 0 Rule-3 violators remain across all `web-app-*/files/playwright.spec.js`.
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>
Nine role specs (akaunting, baserow, bluesky, bookwyrm, flowise,
fusiondirectory, jenkins, joomla, kix) carried a 3-arg
`performKeycloakLogin(page, username, password)` shadowing the shared
4-arg `performKeycloakLogin(page, username, password, canonicalDomain)`
from `personas/utils/keycloak.js`. The local definitions were
functionally identical to `performKeycloakLoginForm` (added in the
previous commit); they were missed by the earlier `performOidcLogin`
sweep because they happened to live under the canonical name.
Changes
-------
- The 9 specs strip their inline definition, rename every call site
and comment reference `performKeycloakLogin` -> `performKeycloakLoginForm`
(function, callers, and inline prose), and pull
`performKeycloakLoginForm` from `./personas`.
Drift handling
--------------
- Cluster 1c681583 (5 specs) and 4b3fd133 (3 specs) used the
input-name selector strategy (`input[name='username']` family).
The canonical helper covers those via the `.or()` fallback in
every selector chain.
- Cluster ffa30940 (kix) used role-based selectors with an
exact-string `getByRole("textbox", { name: "Password" })`. The
canonical's case-insensitive regex `/^password$/i` matches "Password"
and any reasonable Keycloak theme variant (strictly more
permissive).
- Sign-in button: kix's `/sign in/i` becomes the canonical's
`/sign in|login|log in/i` plus the `input#kc-login` family via
`.or()` (strictly broader).
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 `performKeycloakLogin` definitions remain in any role
spec; the only definition lives in `personas/utils/keycloak.js`
alongside the 4-arg variant and the deny-expecting variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourteen role specs (bigbluebutton, discourse, fider, gitea, mailu,
matrix, mattermost, odoo, openwebui, peertube, postmarks, prometheus,
yourls, libretranslate) each carried an inline `performOidcLogin`
that drove the Keycloak login form. The bodies fell into four
clusters split between the role-based selector strategy
(`getByRole`) and the legacy input-name strategy
(`input[name='username']`); the existing shared
`performKeycloakLogin` already covered both via `.or()` chains but
also asserted post-login navigation, so it could not be reused
verbatim.
Changes
-------
- `personas/utils/keycloak.js` gains `performKeycloakLoginForm
(target, username, password)` — form-fill + submit only, accepts
`Page` OR `FrameLocator` so iframe-embedded Keycloak pages work
without branching at the call site. The selectors are the union
of all four prior cluster shapes (role-based getter chained with
`.or(input-name fallback)`, sign-in matched against
`/sign in|login|log in/i` plus `input#kc-login` family). The
existing `performKeycloakLogin` and
`performKeycloakLoginExpectingDenial` now delegate to the new
helper, so their public API is unchanged.
- The 14 specs strip their inline `performOidcLogin` and amend
`require("./personas")` with `performKeycloakLoginForm`. Every
call site and comment reference is renamed in lockstep.
Drift handling
--------------
- Cluster 1 (5 specs) expected the accessible name `"Password"`
exactly; the canonical regex `/^password$/i` is case-insensitive
and matches every prior name plus lowercase variants — strictly
more permissive.
- Cluster 1's sign-in button matched `/sign in/i`; the canonical
matches `/sign in|login|log in/i` plus the legacy `input#kc-login`
family — strictly broader.
- Cluster 1 awaited `usernameField.press("Tab")` without a `.catch`;
the canonical wraps the press in `.catch(() => {})` so an
unexpected Tab failure no longer aborts the flow.
- Cluster 2 / 3 used `expect(usernameField).toBeVisible({ timeout })`
for the visibility wait; the canonical uses
`usernameField.waitFor({ state: "visible", timeout })`, the
Locator-method counterpart of the same wait — functionally
equivalent.
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 `performOidcLogin` definitions remain in any role spec,
and 0 stale references to the legacy name in any context (call
sites and comments are renamed in the same pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nine role specs (bigbluebutton, dashboard, discourse, keycloak,
matomo, matrix, moodle, openwebui, wordpress) each carried inline
copies of the same six CSP helpers and the `EXPECTED_CSP_DIRECTIVES`
constant. The bodies were semantically identical; only message
wording, brace style, and one captured-field-set differed.
Changes
-------
- Extends `roles/test-e2e-playwright/files/personas/utils/csp.js`
(already home to `assertCspInjections`) with
`installCspViolationObserver`, `readCspViolations`,
`parseCspHeader`, `assertCspResponseHeader`,
`assertCspMetaParity`, `expectNoCspViolations`, and the
`EXPECTED_CSP_DIRECTIVES` constant. The helpers are re-exported
through `personas/utils/index.js` so specs that
`require("./personas")` get them via the existing aggregator.
- Strips the inline definitions from the nine role specs and amends
each spec's `require("./personas")` destructuring with whichever
helpers the body still references.
Drift handling
--------------
- moodle's reduced violation-record shape (only `violatedDirective`
+ `blockedURI`) is replaced by the canonical 5-field record.
Moodle's downstream consumer asserts `.toEqual([])` on the array
only, so the extra fields are diagnostic-only — no behaviour
change.
- dashboard's 2-arg `expectNoCspViolations(page, label)` call site
is updated to the canonical 3-arg form
`expectNoCspViolations(page, null, label)`. The shared helper
guards `if (diagnostics && Array.isArray(diagnostics.cspRelated))`
before checking the diagnostics arm, so passing `null` keeps the
exact pre-migration behaviour for that call site.
- bigbluebutton and matrix had subtly different failure-message text
in `assertCspResponseHeader` and `expectNoCspViolations`. The
canonical wording is more descriptive; passing tests are
unaffected, failing tests get clearer diagnostics.
Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` is
extended in two places: the invariants section names the full CSP
test surface and pins the "import from `./personas`, no inline copy"
rule, and the per-service catalogue's CSP / injectors row lists the
helpers by name.
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).
- `markdownlint-cli2`: 0 errors on the touched doc.
- 0 inline definitions of any extracted helper / constant 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>
Cross-service surface assertions for dashboard, prometheus and matomo
no longer run in every consumer role's persona. They are owned by the
provider's own spec, parameterised once over a `<NAME>_TARGET_ROLES_JSON`
manifest rendered into the provider's `templates/playwright.env.j2` via
the new `lookup('roles_with_service', '<svc>')` Ansible lookup.
Changes
-------
- New `plugins/lookup/roles_with_service.py` enumerates roles that
declare `services.<svc>.{enabled, shared}` truthy in the merged
applications config, returning `[{id, canonical_domain}, …]`.
- `roles/web-app-{dashboard,prometheus,matomo}/files/playwright.spec.js`
parse `<NAME>_TARGET_ROLES_JSON` and parameterise the per-consumer
assertion (dashboard tile reachability, prometheus `up=1`, matomo
SitesManager registration). The matomo spec gains an explicit
biber-denial test (request body validated against admin DOM markers
per the body-marker rule); the prometheus admin-reach test drops
the dashboard-iframe path in favour of direct goto.
- Persona helpers `roles/test-e2e-playwright/files/personas/{admin,
biber,guest}.js` enter at `${APP_BASE_URL}/` directly. The
`clickRoleTileFromDashboard` step and the `assertPrometheus*` /
`assertMatomo*` cross-service probes are gone; `utils/{dashboard,
prometheus,matomo}.js` are deleted. `utils/csp.js` switches to
literal `process.env.<KEY>` reads so the env-keys-used parity guard
recognises matomo's CSP injector consumption.
- 42 consumer-role `templates/playwright.env.j2` templates drop
`DASHBOARD_BASE_URL`, `DASHBOARD_SERVICE_ENABLED`, `PROMETHEUS_BASE_URL`,
`PROMETHEUS_SERVICE_ENABLED`. `MATOMO_BASE_URL` and
`MATOMO_SERVICE_ENABLED` stay because the per-role CSP injection
helper still gates on matomo's tracker host.
- `tests/integration/roles/test_playwright_env_services_match.py`
globally exempts `dashboard` and `prometheus` from the
services.yml-to-env parity rule.
- `tests/unit/plugins/lookup/test_roles_with_service.py` adds 14
unit tests for the lookup, including an explicit-contract test
that pins the over-inclusive behaviour on raw Jinja strings (the
lookup expects a templar-rendered payload).
- Docs: `docs/requirements/019-playwright-meta-services-parity.md`
rewords Rules 1, 3, 12 and adds Rule 13 describing the SPOT shift.
`docs/contributing/artefact/files/role/playwright.specs.js.md`
reflects the new persona shape and per-service catalogue, and is
cleaned per `docs/contributing/documentation.md` (no SPOT self-
labels, no req-references, em dashes replaced with complete
sentences, multi-clause lines split onto separate lines, broken
anchor `#two-personas-fixed-flow-` corrected).
Verification
------------
- 418 passed / 1 skipped / 0 failed across `tests/lint/`,
`tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- `markdownlint-cli2`: 0 errors on all touched docs.
- `make autoformat`: clean (FURB171 single-item-set inequality fixed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to 2986e8042 (introduces repo-root package.json with the
ESLint dev-deps and adds node_modules/ to .gitignore):
- `.github/dependabot.yml`: add `/` to the existing npm directories
list. The new package.json was uncovered, so
`tests/integration/workflows/test_dependabot_coverage.py` failed
("ecosystem: npm" required for /package.json).
- `.dockerignore`: regenerated by `make dockerignore` (the Makefile
target that mirrors .gitignore into .dockerignore plus a trailing
`.git`); now carries the new `node_modules/` line so build contexts
don't ship the local install tree into images.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>