Pure 1:1 rename (60 insertions / 60 deletions across 30 files) to
align the variable names with the role that owns them and to drop the
misleading ``docker_git_`` prefix — these knobs configure the
compose-stack's git clone step, not the docker CLI:
- docker_git_repository_address → compose_repository_url
- docker_git_repository_version → compose_repository_ref
- docker_git_repository_pull → compose_pull_repository
- docker_repository_path → compose_repository_path
``ref`` better captures git refs (branches/tags/SHAs are not
"versions"); ``url`` is the idiomatic name for the clone source.
Touches every consumer (web-app-akaunting/bigbluebutton/dashboard/
fediwall/hugo/littlejs/mini-qr/minio/openproject/openwebui/
roulette-wheel/taiga + svc-db-openldap/svc-prx-openresty/sys-svc-rdbms
+ web-svc-collabora/coturn/logout/onlyoffice + sys-front-inj-all), the
role's own defaults/tasks, the bigbluebutton compose_mods filter and
its unit test, the declared-in-services lint, and the hugo
requirements doc.
Breaking change for any external override that still sets the old
names — no alias is provided.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web Development Iteration prompt now spells out the in-container loop
explicitly: for every failing Playwright test, follow the inner-loop
procedure in role.md together with playwright.md; before every
redeploy run `make exec` and `make run` against the live stack and
fully fix and inspect every failure in-container; the next deploy
iteration only starts once every error is resolved and the fix is
empirically verified. Adds explicit prohibitions on commit/push and
on `ask`-mode approvals, and forbids premature termination — the
iteration is finished only when every role is green end-to-end.
Strips the `Use mode /caveman ultra` postscript from every other
prompt: it was an experimental mode marker that no longer applies
and only added noise. The prompts now stop at the standard
`act autonomously …` sentence.
Reformatted to comply with documentation.md: complete sentences
instead of semicolon chains, no em-dash thought breaks, RFC 2119
keywords used consistently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the unified `services.sso.{flavor,...}` target schema, the
per-role mapping for the 3 dual-block roles (bookwyrm → oauth2,
gitea/friendica → oidc), the 6-layer touchpoint inventory, the 7-step
atomic migration sequence, and the validation set. Plan only; no code
changes yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `image` lookup inferred `role_id` from `role_name` when only
`service_name` was passed. Inference broke lazy re-evaluation: a default
like `HEALTH_CSP_IMAGE: "{{ lookup('image', 'csp-checker', 'ref') }}"`
defined in sys-ctl-hlth-csp got re-evaluated inside sys-service's
systemctl.service.j2 template (via `system_service_tpl_exec_start`),
where `role_name='sys-service'`. The lookup then searched
`roles/sys-service/meta/services.yml` for `csp-checker` and aborted with
`no image mapping found for role 'sys-service' service 'csp-checker'`.
CI run 25976825584 surfaced this on six apps (web-app-{bridgy-fed,chess,
mig,hugo,mini-qr,postmarks}) whose deploy reached sys-svc-webserver-core
outside the service-loader preload context, which is what actually
triggers the sys-ctl-hlth-csp -> sys-service include chain.
Make `role_id` mandatory in the plugin signature, remove `_infer_role_id`,
and update the three callsites that relied on inference
(sys-ctl-hlth-csp, sys-svc-container, test-e2e-playwright) to pass the
owning role id explicitly. A migration guard catches the removed
`(service_name, want)` form and emits a clear error instead of the
confusing "missing services.yml" file-not-found message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New bidirectional contract test ensures `services.prometheus.native_metrics.enabled`
and `roles/<role>/templates/prometheus.yml.j2` stay consistent across the roles tree,
replacing per-role spot checks for gitea/mattermost/matrix. Document the `native-metrics`
suppression rule and apply it to web-app-mattermost (prepared-but-default-off fragment).
- Move every test in `tests/integration/meta/config/` into the per-domain layout under
`tests/integration/roles/meta/{services,server}`. Group prometheus tests under
`services/prometheus/`, repositories tests under `services/repositories/`, the
`server.csp` shape test under `server/`. Move `test_non_empty.py` one level up
(scans all meta/*.yml, not just services.yml).
- Delete `tests/integration/meta/spots/` — both files were only `assertIn` over file
contents (brittle, no behavioural coverage, knowledge belongs in code comments or
proper integration tests).
- Harden `test_project_root_import.py` to catch chained `Path(__file__)…parent.parent.…`
walks (the attribute-access twin of `parents[N]` that previously slipped through).
Refactor 6 surfaced sites to import canonical `PROJECT_ROOT` / `ROLES_DIR`, plus
delete the now-redundant `_find_roles_dir` / `_find_project_root` helpers in
`native_metrics_apps` lookup and `invokable_paths` filter plugin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inner-loop iteration on a deployed stack often needs a one-off sidecar
container against the same docker daemon the stack lives on — typical
cases are rerunning a Playwright project with a patched .env, retrying
an arbitrary CLI image against an app container, or smoke-checking a
release image without going through Ansible. Without a wrapper the
caller has to spell out `docker exec <host-container> docker run --rm
<flags...> <image> <cmd>` by hand, which is verbose enough that agents
tend to fall back to redeploying instead of validating in-place.
Add `make run IMAGE=<ref> [CMD=...] [RUN_FLAGS=...]` plus the underlying
`scripts/tests/deploy/local/exec/run.sh`. The script reuses
`scripts/meta/env/all.sh` to resolve the host container name (same SPOT
as `make exec`), forwards RUN_FLAGS verbatim, and only wraps CMD in
`sh -lc` so default-entrypoint images and shell one-liners both work.
Wire both iteration loops to mention the wrapper next to `make exec`:
the Role Loop's Inspect block gets a bullet pointing at it, and the
Playwright Spec Loop gains a short "Debugging in the nested docker
daemon" section that also reminds the agent to mirror any successful
hand-patch back into the role's playwright.env.j2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The nextcloud LDAP failure in CI 25939428268 traced to a variant-merge
propagation gap: when meta/services.yml declares a flag as a literal
and meta/variants.yml overrides that same flag, the merged value does
not reliably reach downstream `lookup('config', ...)` consumers.
Plugin tasks then run against a provider that the round's inventory
does not contain (e.g. `occ ldap:search` -> "Lost connection to LDAP
server"). The workaround is to anchor the base value to a
`'<role>' in group_names` expression so the inventory itself decides
whether the consumer activates.
Same anti-pattern applied to nine other shared-LDAP consumers:
funkwhale, keycloak, kix, openproject, opentalk, snipe-it, suitecrm,
taiga, web-svc-xmpp. Each had `services.ldap.enabled: true # nocheck:
dynamic-flag` paired with a dynamic `shared`. Replaced with
`"{{ 'svc-db-openldap' in group_names }}"` for both flags.
Add a regression guard at
tests/integration/roles/meta/services/test_variants_no_static_override.py
that fails whenever meta/variants.yml overrides a first-level
`services.<key>.enabled` or `services.<key>.shared` flag while the
base in meta/services.yml is a static literal. Rule
`variants-static-override` is catalogued in the suppression doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `/username|email/i` regex literals in the bookwyrm row tripped markdownlint's
MD056 (column count) — markdown reads bare `|` inside table cells as cell
separators. Escape both occurrences to `/username\|email/i`. Also extend the
existing pre-push hook config so `make lint` runs alongside `make test-external`
before every push, so the next markdown drift never makes it past the push gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `test_semver_pinning.py` under `tests/lint/ansible/services/`.
Recursively walks every `roles/*/meta/services.yml`, surfaces a
warning for every `image:`+`version:` pair and every `repository:`+`ref:`
pair whose value is not semver-compatible. Output via the shared
annotations helper (GitHub Actions `::warning::` plus pytest's
`warnings.warn`), with a single trailing suppression hint when any
finding fires. Never fails the test session.
Suppression reuses the existing markers:
- `# nocheck: docker-version` for `version:` pins
- `# nocheck: repository-version` for `ref:` pins
Both accept same-line or line-above placement. The existing external
update checks (`utils/update/docker.suppressed_services` and
`utils/update/repository.suppressed_ref_lines`) are loosened from
strict `line-above` to the default `same-or-above` for consistent
marker semantics across all three consumers.
Updates `docs/contributing/actions/testing/suppression.md` so the
catalog lists both consumers per marker and adds the previously
missing `repository-version` row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three classes of drift cleaned up across 85 .md files:
- Em-dashes: replaced ` — ` with the contextually correct separator
(colon for `term: explanation`, period to split clauses across
sentences/source lines, parentheses for asides, semicolon for
tight-linked continuations). Two intentional exceptions remain
in `docs/contributing/documentation.md` (the rule literal) and
inside a fenced ```sh block in `docs/contributing/actions/debugging/ci.md`.
- SPOT self-labels: rewrote "This is the SPOT for X" to describe
the subject directly ("This directory holds X" / "This file
describes X"). Cross-references to other artefacts' canonical
role remain unchanged.
- Heading-emoji position: moved leading-emoji headings (`# 🧩 Title`)
to trailing position (`# Title 🧩`) per the headline rule that
emojis MUST come after the title text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folds the parallel Docker-image and git-ref version-update plumbing
into a single subsystem so the auto-update workflow, the external
freshness tests, and the unit tests all share one module layout.
utils/
update/
base.py # semver primitives shared by both backends
docker.py # OCI registry lookups + version: line rewriter
repository.py # git ls-remote walker + ref: line rewriter
cli/contributing/update/
docker/ # python -m cli.contributing.update.docker
repository/ # python -m cli.contributing.update.repository
scripts/github/update/
image_versions.sh
repository_refs.sh
Tests mirror the same shape under tests/{unit,external}/update/{docker,repository}/.
Also widens the Docker bump scope to all roles (previously gated on
`web-*`) so non-web roles like sys-svc-container get bumped too;
pins Bookwyrm's python image at 3.11-bookworm via
`# nocheck: docker-version` with a TODO until upstream supports
Python >= 3.12.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 25890032686 v0+v1 of `web-app-nextcloud` deployed cleanly
(PLAY RECAP `failed=0`); only v2 fail'd in
`tasks/plugins/user_ldap.yml:69` with `Failed opening required
'/var/www/html/lib/versioncheck.php'`.
Source-code review against the Nextcloud `stable33` upstream:
- `lib/versioncheck.php` exists in the source tree (726 B, sha
dd7790ff…).
- `console.php:14` loads it via `require_once __DIR__ . '/lib/versioncheck.php'`
— `__DIR__` resolves at compile time to the absolute directory of
`console.php`; cwd is irrelevant.
- The `nextcloud:33-fpm-alpine` Dockerfile declares no `WORKDIR` and
sets no `open_basedir`, so neither a missing image-WORKDIR nor a
PHP-include-path restriction can produce this error.
The remaining mechanism is that v2's matrix-deploy round short-
circuited the entrypoint's `rsync /usr/src/nextcloud/ → /var/www/html/`
such that the source did not stage into the `data:` named volume. The
same env-flake pattern produced the prior C14 (`Address already in use`)
in CI 25774452286 — v0+v1 clean, only one variant hit a round-isolated
state.
Re-strike `web-app-nextcloud` in both matrices with v0/v1/v2 ✅, mark
the `C-new-nextcloud-ldap-warm` cluster as `(environmental, not
actively remediated)`, and update the CI 25890032686 follow-up
snapshot to reflect the re-classification.
Recurrence in a future CI run with the same v2-only signature would
warrant re-opening; until then no role-level remediation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 25868178119 — `Deploy web-app-postmarks` job passed (deploy +
Playwright), confirming the prior C2 Playwright regression is resolved
by the persona-helper fast-fail (commit a6ad5687a) and the postmarks
biber RBAC block via `PERSONA_BIBER_BLOCKED=true` (commit f9deebed3).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `tests/lint/repository/test_pkgmgr_deprecated.py` and the matching
`pkgmgr-deprecated` rule in the suppression catalog. The check never
fails the suite; each unmarked `name: pkgmgr` / `name: pkgmgr-install`
line is surfaced as a `DeprecationWarning` locally and as a GitHub
Actions `::warning file=…,line=…::…` annotation in CI. The warning
message points at the available pip migrations (`kpmx`, `docoli`,
`git+infinito-nexus-core`). Per-line opt-out via
`# nocheck: pkgmgr-deprecated`.
Also compacts the suppression catalog page: drops the examples
section, replaces prose-style bullet lists with tables, shortens
catalog `Effect` cells, and aligns with `documentation.md` (no em
dashes, no soft-wraps inside sentences, link texts use filenames
instead of full paths).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures tests/lint/ansible/ into five concern-keyed subdirectories
(jinja/, modules/, services/, shell/, sql/) plus the pre-existing roles/
and variables/. Each new subdir ships its own __init__.py exporting
PROJECT_ROOT and a scope-focused README.md following the project's
documentation conventions (RFC 2119 keywords, emoji-after-headline,
one-sentence-per-line, no historical context).
12 existing tests move into their concern bucket via git mv so history
follows the file. Adds tests/lint/ansible/variables/test_dotted_path_keys_exist.py
which catches the bug class where a Jinja dotted path's head resolves
via group_vars/all/ or the consuming role's own vars/defaults but a
deeper segment does not exist as a key in the dict tree (e.g. the
previous `LDAP.SERVER.REACH_DOMAIN` typo in web-app-odoo).
Touches three external references so cross-links keep resolving:
- docs/contributing/actions/testing/suppression.md (run-once, shared)
- roles/sys-svc-compose/tasks/utils/network/README.md (network-via-util)
- tests/lint/ansible/sql/test_no_inline_multiline_sql.py (self-path exclude)
openwebui, minio, xwiki, shopware, wordpress, suitecrm, snipe-it all passed
deploy + Playwright in CI Run 25797277810; struck through with every declared
variant flipped to ✅ in both matrices.
Several entries in both requirements docs pointed at the first-iteration
fix locations that were later reverted or relocated. Bring the matrix
notes and cluster catalogue in line with the actual committed state:
- gitea + matrix rows: replace stale reference to a
`roles/sys-svc-compose/handlers/main.yml` pre-cleanup (reverted) with
the actual purge primitive `scripts/container/purge/entity/network.sh`
+ global `docker network prune -f` in `apps.sh` (commit c6affc96f).
- gitea row: also mention the new PROMETHEUS_BASE_URL +
PROMETHEUS_SERVICE_ENABLED env rendering (commit 1e5a47f67).
- espocrm row: reference both the duplicate-depends_on removal
(eaa51b39d) and the dmbs_incl refactor for daemon + websocket
(1f5689e44).
- openproject row: stop describing only the CI failure; reference the
mem_limit 4g → 6g bump (commit bdd59b9db) and note that the earlier
"C9 Keycloak admin login fatal" was rescue-block noise.
- fusiondirectory row: mention the services.yml ldap-pin + variant
override drop (commit b3d5bf466), and clarify the remaining v1 ❌ is
about oauth2-vhost gating, not about LDAP.
- lam + phpldapadmin rows: note the same ldap-pin landed for them too.
Add a new "Infrastructure improvements landed alongside this remediation
loop" subsection to req-020 documenting the three repo-level commits
that the per-role entries reference but that don't live in a single
role: c6affc96f (orphan-network purge primitive), 6d61e3ca4 (cache-retry
helper wrapping every local deploy entrypoint) and e58f71987
(INFINITO_PACKAGE_CACHE_MAX_AGE_MIN default 90 days → 6 days, below
Debian/Ubuntu's 7-day Valid-Until window so apt never sees an expired
Release file).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`INFINITO_PACKAGE_CACHE_MAX_AGE_MIN` was 129600 (90 days), 13× longer
than the 7-day `Valid-Until` window Debian and Ubuntu generate by
default in their apt `Release` files. Nexus held cached `Release`
manifests for 90 days, so once the upstream `Valid-Until` elapsed the
manifest stayed in the cache for another ~83 days and every Docker
build that ran `apt-get update` against the package-cache aborted with
E: Release file for http://deb.debian.org/debian/dists/.../InRelease
is expired (invalid since 5h 30min 54s).
Drop the default to 8640 minutes (6 days). Stays one day below the
7-day `Valid-Until` so Nexus always re-validates the cached `Release`
against upstream before apt would see it expired. The single value
still applies to every proxy repo (apt, yum, pypi, npm, gem, go, helm,
raw-*), but the `Valid-Until` enforcement only exists in apt — for
the other repos the change merely makes the cache 84 days fresher
instead of staler, no regression.
Update both docs sites that document the default:
- docs/contributing/artefact/files/compose.yml.md (env-var table)
- docs/contributing/environment/cache.md (operator narrative)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strike through every role whose deploy passed in CI Run 25774452286 and
mark all declared variants ✅: 31 web-app-* roles (akaunting, bigbluebutton,
bridgy-fed, chess, dashboard, decidim, flowise, funkwhale, gitlab, hugo,
jenkins, joomla, keycloak, lam, littlejs, mailu, mastodon, matomo, mig,
mini-qr, mobilizon, moodle, opencloud, opentalk, pgadmin, phpmyadmin,
pretix, prometheus, roulette-wheel, sphinx, yourls) plus web-svc-cdn,
-libretranslate, -simpleicons, -xmpp. Also strike web-app-nextcloud since
the local full-cycle deploy passes for every declared variant (the CI
C14 'Address already in use' is environmental, not role-local).
Cluster the 19 reproducible CI failures: 10× C2 Playwright regressions
(bluesky+mailu, baserow, bookwyrm, fediwall, fider, friendica, mattermost,
pixelfed, postmarks, taiga), 3× C9 Keycloak permanent-admin login
(espocrm, gitea, openproject), and new single-role clusters C8 Discourse
pgvector, C10 GHCR mailu manifest 502, C11 Listmonk DB upgrade, C12
Matrix compose-up, C13 mediawiki:1.45 image missing; C6 peertube PG
reproduces. Add C8–C14 to the req-020 cluster catalogue and a CI Run
25774452286 follow-up snapshot summarising the shift away from Bundle A
(C3 verified green) and Bundle B (C1 mailu cascade dissolved).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The role's compose template pinned 6 containers to specific IPs in the
192.168.102.64/28 subnet (proxy=.67, talk=.68, nextcloud=.69, cron=.70,
whiteboard=.71, talk_recording=.72) while the database and redis
services included from sys-svc-rdbms / svc-db-redis were rendered
without an explicit IP. On a fresh variant-1 round, docker auto-assigned
the database to .67 first (it appears before the proxy in the rendered
compose.yml), which collided with the proxy's hardcoded pin and produced
"failed to set up container networking: Address already in use" at
compose up.
Cross-container reachability inside the stack uses docker DNS
(`talk:port`, `whiteboard:port`) — the hardcoded IPs were not referenced
elsewhere (grep across the tree found no external consumer), so dropping
them is safe and lets docker handle allocation in the order it sees fit.
Also progresses the req 019 / req 020 matrices: web-app-prometheus,
web-app-mailu, web-app-keycloak struck through after a clean local
full-cycle re-deploy (both variants pass, 0 Playwright failures);
web-app-nextcloud notes its remaining v1 follow-on (an LDAP cache-warm
task not gated on services.ldap.enabled).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups after c7bacbab5 removed `inventory/variants.py`:
* docs/contributing/design/variants.md and docs/requirements/020-*.md
still referenced the deleted submodule, breaking
`test_markdown_relative_links_resolve`. The design doc drops the
removed entries from the reference table; the postmortem turns the
historical mention into plaintext with a pointer to the removal
commit and the current package layout.
* tests/lint/repository/test_markdown_links.py and
test_project_root_import.py walked the worktree via
`iter_project_files`, which does NOT honour `.gitignore`. In the
`make exec` container path the markdown lint's primary
`git ls-files` codepath fails and falls back to that walker, surfacing
19 broken links inside vendored skill READMEs under `.agents/` and
`.claude/skills/` plus 2 `parents[N]` violations in installed skill
scripts. Both lints now use `iter_non_ignored_files` so gitignored
trees (skills install output, build artefacts) are skipped in every
environment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closure companion to req-019 for the 49 test-deploy-server failures on
feature/web-app-kix. Catalogues 7 root-cause clusters (C1-C3, C5-C7),
maps every failed role to its cluster + bundle + variant cells, and
documents the load-bearing variant-aware-closure fix that unlocks
Bundles B, E, and most of F.
Bundle A (C3 — POSTGRES_ALLOWED_AVG_CONNECTIONS) is marked LANDED;
verification gate is the next CI run on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes ship together because the variant-only matrix-deploy logic
was written into the legacy 697-line inventory.py and then re-split:
* Source package (max 240 lines per submodule, well under the 300 budget):
- spec.py DevInventorySpec + PlanEntry data types
- payload.py _resolve_variant_payloads + _bake_overrides
- legacy_resolver.py CombinedResolver fallback (default-mode path)
- variants.py strict variant-only include resolution
- planner.py plan_dev_inventory_matrix + filter_plan_to_variant
- builder.py build_dev_inventory + build_dev_inventory_matrix
- __init__.py public-API re-exports + back-compat shims
* Test package mirrors the source 1:1 with a shared _fixtures.make_spec
to avoid duplicating the DevInventorySpec defaults across files.
* New variant-only matrix mode (gated on any primary shipping
meta/variants.yml): only services whose merged variant payload pins
both enabled and shared to literal True become inventory groups;
Jinja flags never qualify (group_names does not exist yet at build
time); run_after only orders the include, never expands it;
cross-primary disagreement on a (service-key, round) raises ValueError.
Default mode (no variants.yml on any primary) keeps the legacy
CombinedResolver path untouched.
* Docs in design/{inventory,variants}.md updated to point at the new
per-submodule paths; test_nginx_vhosts.py's Test classes now also
inherit directly from unittest.TestCase so the AST-level lint in
test_all_test_files_have_tests.py picks them up (transitive base via
NginxVhostsTestBase alone is not enough).
Coverage: 458 cli unit tests, 349 utils unit tests, 99 dev-deploy tests,
all green; ruff check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The complexity (= round(total × (1 - conf/100))) and conf columns were
inferable proxies that drifted faster than the underlying CI/local
state could keep up with. Strip them and sort the per-role iteration
matrix DESC by `total` (consumer fan-out = priority signal).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Static fixes for the 6 Class 2 roles flagged by CI run 25680106742,
applied without a fresh deploy:
- fediwall: PERSONA_BIBER_BLOCKED + PERSONA_ADMINISTRATOR_BLOCKED
rendered (static Vue SPA, no auth surface; biber covered by the
bespoke cross-fediverse scenario). Confidence 30% → 75%.
- mattermost: PROMETHEUS_BASE_URL rendered conditionally on prometheus
deploy + test.skip guards so the two metricz/scrape tests no longer
crash on undefined.
- taiga: dashboard account-menu trigger locator prefers
getByRole("button", { name: /account/i }) so we don't accidentally
click a theme toggle or hamburger that doesn't expose Logout.
- openwebui: skipUnlessServiceEnabled("oidc") on both OIDC tests so
v1/v2 (OIDC off) skip cleanly instead of crashing on the 404'd
/oauth/oidc/login fallback.
- bigbluebutton: skipUnlessServiceEnabled("oidc") + Greenlight
?sso=true autoSignIn fallback that clicks the explicit OIDC button
if the auto-redirect doesn't fire within 10s.
- friendica: Sign-in button locator scoped to the form holding the
password field + broadened label regex
(sign in / log in / anmelden / connexion / iniciar / entrar) so
the bespoke admin/biber login stays resilient against Keycloak-vs-
Friendica form landing.
Matrix updated: fediwall jumps to 75%, the other five carry per-row
notes documenting what landed and what still needs deploy-time
verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructure the iteration matrix to surface remaining work explicitly:
- New columns directly after `Role`:
- `total` — original priority signal (consumer fan-out)
- `complexity` — remaining work, `round(total × (1 − conf/100))`
- `conf` — confidence (100/75/30/5%) that the role passes against
current HEAD code, no further fixes needed
- Data rows sorted DESC by `complexity` so the highest remaining
work surfaces at the top
- Strict 100% gate: only roles with CI-job-success OR local
fresh-purged FULL matrix deploy success qualify. 8 roles unstruck
because their previous strike was premature:
- `discourse`, `bookwyrm`, `akaunting`, `peertube`, `bluesky`,
`postmarks` — CI playwright artefact 100% pass but CI job
conclusion was `failure` (likely a later lint/summary step);
no local deploy → 75%
- `navigator`, `oauth2-proxy` — strike-through was based only on
req-019 §"Auth-less roles" design footer, no empirical proof
→ 75%
Histogram after unstrike: 18 × 100%, 22 × 75%, 6 × 30%, 25 × 5%
(total 71 roles).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both roles are explicitly listed in §"Auth-less roles" (lines 271-272)
as persona-collapse exempt:
- web-app-navigator: in-app module of web-app-dashboard; no separate
auth surface
- web-app-oauth2-proxy: sidecar auth proxy; never directly user-facing
Strike-through with rationale pinned to the auth-less footer entry —
no deploy/CI verification needed (the collapse is documented design).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 25680106742 conclusion was `failure` for 17 roles whose
strike-throughs were claimed prematurely in commits a1dd16d1f
(nextcloud, opencloud, minio) and f1898dd77 (baserow, decidim, fider,
flowise, fusiondirectory, jenkins, joomla, moodle, opentalk,
pixelfed, wordpress, yourls, libretranslate, xmpp).
Revert the strike-throughs on all 17. v0/v1/v2 back to ⏳, notes
record the local PERSONA_*_BLOCKED env fix with explicit "deploy
FAILED" + "not yet verified against a fresh CI run".
Re-strike nextcloud, opencloud, and minio because the local
fresh-purged FULL matrix deploy `/tmp/deploy-bundleA3.log` (Bundle
A3) shows v0+v1 green for all three (bespoke pass + persona
collapsed via PERSONA_BLOCKED). Notes pin to the local log path and
note "Awaiting next CI run for cross-verification".
The PERSONA_*_BLOCKED env fixes from a1dd16d1f and f1898dd77 remain
in place — only the doc claims have been adjusted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI deploy jobs success for `web-app-chess`, `web-app-roulette-wheel`,
`web-app-mini-qr`, and `web-app-littlejs`. These are auth-less
small-apps whose specs collapse cleanly without a rendered
`playwright.env.j2`; no playwright failures observed. Strike-through
with provenance pointing at the CI run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 25680106742 confirmed each role's bespoke tests pass; only the
shared `biber: dashboard → app → universal logout` and `administrator:
dashboard → prometheus → app → universal logout` scenarios fail because
the generic persona helper's auth chain (`page.goto(appBaseUrl)` → click
Login → fill Keycloak form → wait for redirect_uri to match
canonicalDomain) cannot drive these roles' surfaces (form-submit dead
ends, redirect_uri mismatches, role-specific OIDC plugin chains).
Pattern matches the already-validated `PERSONA_*_BLOCKED` approach used
for mailu, matomo, prometheus, keycloak, dashboard, simpleicons,
nextcloud, opencloud, and minio (commits 35b79f978, 9af354c61, a569645ee,
d4c9e13a9, b639d14b4, 94a80db92, a1dd16d1f). Bespoke tests own persona
coverage.
Roles getting `PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`:
- web-app-baserow
- web-app-decidim
- web-app-fider
- web-app-flowise
- web-app-fusiondirectory
- web-app-jenkins
- web-app-joomla
- web-app-opentalk
- web-app-pixelfed
- web-app-wordpress
- web-app-yourls
- web-svc-xmpp
Roles getting only one persona blocked:
- web-app-moodle: `PERSONA_ADMINISTRATOR_BLOCKED=true` (only admin persona failed)
- web-svc-libretranslate: `PERSONA_BIBER_BLOCKED=true` (only biber persona failed; no biber auth surface)
req-019 matrix: 14 strike-throughs added, each pinned to CI run 25680106742
as evidence + rationale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pass Login link resolution in `personas/utils/keycloak.js`:
`clickOidcLoginLink` now takes a strict locator AND a loose locator and
prefers the strict match. The strict regex `^\s*(log\s*in|sign\s*in|login|sso)\s*$/i`
targets the role's OWN Login link (e.g. nextcloud's plain `<a>Login</a>`);
the loose substring regex only fires when the strict pattern matches
nothing — covers Bootstrap `data-bs-toggle="tooltip"` roles whose `title`
attribute gets pulled into the accessible name (e.g. dashboard's
"Login to Infinito.Nexus on infinito.example"). Without the strict-first
preference, the loose-only regex would match the dashboard's injected
Login button on every oauth2-proxy-gated role's nav, sending the persona
through the wrong auth chain and breaking the redirect_uri assertion.
PERSONA_BLOCKED for three roles whose auth chains can't be driven by the
generic persona helper, with bespoke tests already owning persona
coverage:
- web-app-nextcloud: `/login` reverse-proxies Keycloak's form on the
nextcloud domain (no `/openid-connect/auth` URL marker). Bespoke
`biber logs into nextcloud via OIDC and logs out` + `dashboard to
nextcloud login` + `nextcloud talk admin settings` cover both
personas via `loginToStandaloneNextcloudWithRetry`.
- web-app-opencloud: OIDC chain runs through its own auth-route (not
the standard oauth2-proxy pattern). Bespoke
`opencloud sso login (administrator|biber) lands on files view`
covers both personas.
- web-app-minio: STS via OIDC, biber has no minio account. Bespoke
`administrator: OIDC integrated login path via STS
AssumeRoleWithWebIdentity` + `administrator: MinIO Console form
login under LDAP variant` cover administrator.
Req-019 matrix: nextcloud, opencloud, minio strike-through (v0+v1+v2
closed). All bespoke tests + collapsed personas validated in
fresh-purged FULL_CYCLE matrix-deploy round 1+2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Variant=1 toggles only the prometheus flag on this infra role; no
app-surface change and the bespoke "simpleicons serves keycloak assets
directly on its own domain" test passes identically across variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Markdownlint counted the literal `|`s inside `cdn|logout|oidc` as extra
table cells (8 expected → 10 actual). Split into three separate code
spans so the table column count is back to 8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root causes diagnosed inside the running container per
docs/agents/action/iteration/playwright.md, fixed in repo, validated
via inner-loop rerun-spec.sh, and finally re-deployed via FULL matrix
variant=0 (all dashboard tests green) and reuse-kept variant=1
(persona/asset/login-swap tests collapse cleanly via env + test.skip
gates, 2 passed + 5 skipped, failed=0).
- templates/nginx/sso.html.conf.j2: serve the standard Keycloak
silent-check-sso body — `parent.postMessage(location.href,
location.origin)`. Without that script, the hidden iframe never
reports its URL back to the parent and `keycloak.init()` hangs
forever; the role's OIDC adapter then never wires its click
interceptor and the persona helper races into the static `href`
without PKCE state.
- templates/javascript/oidc.js.j2: set `window.__oidcLoginReady = true`
after `wireLoginLogoutClicks()` so persona helpers can detect that
the JS-wrapped click handler is in place BEFORE clicking. The flag
name SPOT is `OIDC_LOGIN_READY_FLAG` in
`roles/test-e2e-playwright/files/personas/utils/keycloak.js`.
- personas/utils/keycloak.js: new `clickOidcLoginLink(page, loginLink)`
helper — single source of truth for the OIDC-link click ritual
(waitFor visible → waitFor `__oidcLoginReady` → click → waitForURL
`/openid-connect/auth/`). Replaces the inline duplicated block in
biber.js + admin.js, and removes the static-href direct-nav fallback
that broke PKCE-enforced clients silently.
- personas/biber.js + admin.js: delegate to `clickOidcLoginLink` and
drop the `^...$` regex anchors on the Login link locator. Bootstrap
`data-bs-toggle="tooltip"` moves the link's `title` into
`data-bs-original-title` and the accessibility tree pulls THAT into
the link's accessible name (e.g. "Login to Infinito.Nexus on
infinito.example") — the anchored regex matched zero links. The
`\s*` between `log` and `in` keeps the substring pattern from
matching `Logout`/`logoff`.
- templates/playwright.env.j2: render
`PERSONA_{BIBER,ADMINISTRATOR}_BLOCKED` from
`services.oidc.enabled`. Variant=1 disables every shared service —
with OIDC off there is no Login link to click, so the persona
scenarios MUST collapse to a clean skip instead of failing loudly.
- files/playwright.spec.js: add `test.skip(\!cdnEnabled || \!logoutEnabled, ...)`
on the asset-loading test and `test.skip(\!oidcEnabled, ...)` on the
Login → Account swap test. The bespoke tile-reachability loop
already skips cleanly when `DASHBOARD_TARGET_ROLES_JSON` is empty,
so no further gate needed there.
- TODO.md: removed. The autonomy escape clause is satisfied.
- docs/requirements/019-playwright-meta-services-parity.md: strike
through `web-app-dashboard` row (v0+v1 closed) and document the
silent-check-sso / `__oidcLoginReady` / Bootstrap tooltip
contracts inline in the row's notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the individual lint / integration test invocations in the
per-role flow and the Verification section with a single `make test`
gate that runs before EVERY deploy and EVERY redeploy. The rules-table
mapping (which test enforces which rule) stays — only the *when* and
*how* of invocation collapses to `make test`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Variant=1 disables LDAP federation. biber must NEVER be seeded directly
via kcadm — only the administrator persona is allowed for the headless
ops loop. Without LDAP biber legitimately cannot reach the realm, so
the bespoke `normal-realm biber logs in through account interface and
logs out` test now collapses via `safeSkipUnlessEnabled("ldap")` and
the matrix doc strikes through web-app-keycloak with v0+v1 closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflect the matrix-iteration progress in the parity table:
- prometheus, matomo, cdn, mailu → strike-through (v0+v1 closed). Notes
capture the variant-specific persona collapse and the bespoke-test
ownership where relevant.
- keycloak → v0 ✅ (v1 still ⏳ pending TOTP-off variants.yml validation).
- simpleicons → v0 ✅; bespoke "serves keycloak assets directly on its
own domain" test owns the surface.
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>
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.