Replace literal `enabled: true / shared: true` flags across every
roles/*/meta/services.yml with a Jinja `"{{ '<role>' in group_names }}"`
form so the per-role services map becomes the single source of truth
for shared-service co-deployment, on a per-host basis. Inventory build
already runs through the templar before reaching the static `is True`
check, so runtime behaviour is unchanged; the new form only opens up
static-analysis and CLI tooling to the group-aware semantics.
Mechanism + lint:
- New integration test tests/integration/roles/meta/test_services_
dynamic_flags.py enforces the dynamic shape per top-level service
entry. Registered service keys must reference the registry-resolved
provider role; covered keys (e.g. oauth2 -> web-app-keycloak via
the new `covers:` block) reference the covering role.
- Two suppression placements documented in docs/contributing/actions/
testing/suppression.md: per-flag inline `# nocheck: dynamic-flag`
for databases (`enabled: true` stays literal, `shared` resolves
dynamically) and block-level above the key for entries whose both
flags legitimately stay literal (`css`, `javascript`, role-local
`redis`, `recaptcha`, `hcaptcha`, role-local containers with
`shared: false`).
Registry + helpers:
- utils/service_registry.py: new `is_explicit_truth(value)` helper
accepts `True` literal OR a Jinja string carrying `in group_names`.
Adopted by `resolve_service_dependency_roles_from_config`,
`ServicesResolver._is_enabled_shared`, `test_web_app_email_
integration`, `test_prometheus`, `test_universal_logout`, and the
run_after-explicit guard so static analysis keeps resolving deps.
- New `covers:` field on a primary service entity declares
transitive equivalents (web-app-keycloak.covers: [oauth2]).
Exposed via `build_role_to_covered_keys` and
`build_covered_key_to_role`, replacing the previously hard-coded
`_TRANSITIVE_SERVICE_EQUIVALENTS` / `_SPECIAL_REFERENCE_ROLE` maps
in the run_after and dynamic-flag tests.
Bulk migration:
- All 80 roles/*/meta/services.yml files migrated to the dynamic
form. DB consumer keys (mariadb, postgres, ldap) keep
`enabled: true # nocheck: dynamic-flag` literal with a dynamic
`shared`. Block-nocheck applied to css/javascript/recaptcha/
hcaptcha and role-local containers (e.g. mailu's admin/imap/smtp/
antispam/antivirus/oletools/webdav/fetchmail/webmail/resolver/
front, nextcloud's talk).
- web-app-prometheus's primary entity now declares
`provides: prometheus` + dynamic flags so the registry recognises
it as a service provider. Consequence: tasks/01_core.yml split out
with `- include_tasks: utils/once/flag.yml` as the first task and
tasks/main.yml reduced to the standard run_once-guarded loader.
- Seven minimal no-login apps (web-app-hugo, -bridgy-fed, -chess,
-littlejs, -navigator, -mini-qr, -mig) gained an explicit
`logout: { enabled: false, shared: false }` opt-out (with
`# nocheck: dynamic-flag` block-marker) to satisfy the universal-
logout schema check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/lint/repository/test_domain_primary_spot.py which fails when a
file constructs a hostname via a literal `<sub>.{{ DOMAIN_PRIMARY }}` outside
the role's `meta/server.yml` (or `meta/variants.yml`) `domains.canonical` /
`domains.aliases` blocks. Every other reference MUST flow through the
project's lookup plugins so the canonical/aliases entries stay the SPOT.
Mechanics
- Regex matches `<word>.{{ DOMAIN_PRIMARY }}` patterns; wildcard fragments
(`.{{ DOMAIN_PRIMARY }}`, `*.{{ DOMAIN_PRIMARY }}`) and bare
`{{ DOMAIN_PRIMARY }}` references are intentionally NOT flagged
- meta/server.yml and meta/variants.yml: a state-machine line scanner
exempts list items and dict entries inside `canonical:` / `aliases:`
blocks (handles compact, expanded, list, and dict YAML styles)
- README.md is exempt; docs/ and files/ subtrees inside roles are skipped
- Per-line opt-out via `# noqa: domain-spot` (or `# nocheck: domain-spot`)
- File reads route through utils.cache.files.read_text and the helpers
themselves are wrapped in lru_cache so repeated path lookups during
the traversal pay the parse cost once
Repo cleanup
- 11 role meta/server.yml files now reference Keycloak, Collabora,
OnlyOffice, Nextcloud, and their own canonical hostnames through
`lookup('domain', '<role>')` (and `lookup('config', '<role>',
'server.domains.canonical.<key>')` for the dict-canonical roles
Matrix and Bluesky) instead of duplicating the SPOT
- `roles/web-app-mattermost/tasks/main.yml` keeps a single
`# noqa: domain-spot` on the legacy domain cleanup task; the
hardcoded historical hostname must persist there to remove stale
on-disk nginx configs from the rename
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds tests/lint/repository/test_domain_primary_spot.py which fails when a
file constructs a hostname via a literal `<sub>.{{ DOMAIN_PRIMARY }}` outside
the role's `meta/server.yml` (or `meta/variants.yml`) `domains.canonical` /
`domains.aliases` blocks. Every other reference MUST flow through the
project's lookup plugins so the canonical/aliases entries stay the SPOT.
Mechanics
- Regex matches `<word>.{{ DOMAIN_PRIMARY }}` patterns; wildcard fragments
(`.{{ DOMAIN_PRIMARY }}`, `*.{{ DOMAIN_PRIMARY }}`) and bare
`{{ DOMAIN_PRIMARY }}` references are intentionally NOT flagged
- meta/server.yml and meta/variants.yml: a state-machine line scanner
exempts list items and dict entries inside `canonical:` / `aliases:`
blocks (handles compact, expanded, list, and dict YAML styles)
- README.md is exempt; docs/ and files/ subtrees inside roles are skipped
- Per-line opt-out via `# noqa: domain-spot` (or `# nocheck: domain-spot`)
- File reads route through utils.cache.files.read_text and the helpers
themselves are wrapped in lru_cache so repeated path lookups during
the traversal pay the parse cost once
Repo cleanup
- 11 role meta/server.yml files now reference Keycloak, Collabora,
OnlyOffice, Nextcloud, and their own canonical hostnames through
`lookup('domain', '<role>')` (and `lookup('config', '<role>',
'server.domains.canonical.<key>')` for the dict-canonical roles
Matrix and Bluesky) instead of duplicating the SPOT
- `roles/web-app-mattermost/tasks/main.yml` keeps a single
`# noqa: domain-spot` on the legacy domain cleanup task; the
hardcoded historical hostname must persist there to remove stale
on-disk nginx configs from the rename
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reworks tests/lint/ansible/roles/test_compose_resource_limits.py from
warn-only-on-every-role to a strict failure scoped to actual
compose-service declarations:
1. Only roles that ship a `templates/compose.yml.j2` are scanned.
Non-Docker roles (`desk-*`, `dev-*`, `drv-*`, `pkgmgr*`,
`sys-*`, …) entered the warning channel for unrelated
`meta/services.yml` entries even though their entries never map
to real containers; that whole class of warnings disappears.
2. Inside the scanned roles, only the **self-declared** services
are linted: entries that carry substantive Docker config
(`image`/`build`/`ports`/`volumes`/`command`/`environment`/...)
OR a primary entity backed by a `files/Dockerfile` /
`templates/Dockerfile.j2`. Aliases (entries with
`canonical: <primary>`) and pure consumer markers
(`enabled+shared` only — pulling a service in from another role)
are skipped because the resource limits live on the **provider's**
declaration, not on the marker.
3. Failures are hard `self.fail` again so regressions block CI.
Concretely: `web-app-mailu/meta/services.yml.{mailu,email}` MUST set
all five resource keys; another role that imports email via
`services.email.enabled+shared: true` is a marker and is exempt.
Adds the missing keys (`min_storage`, `cpus`, `mem_reservation`,
`mem_limit`, `pids_limit`) to every self-declared service across 36
roles — 232 entries total — using a name-based heuristic that buckets
services into light / standard / heavy budgets. Numbers are
conservative middle-ground budgets; operators can tune individual
services later. Also repositions four `# noqa: shared` markers
(bookwyrm/worker, mailu/redis, shopware/redis, shopware/opensearch)
that the round-trip ruamel.yaml dump shifted into the wrong service
block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`oidc_login_auto_redirect=true` is the happy path on /login: the
pulsejet/nextcloud-oidc-login boot hook bounces straight to Keycloak.
Under cold first-deploy load that hook can lose its `header('Location:...')`
when an earlier hook has already emitted output, and /login renders the
NC chrome with the password form hidden but the OIDC alt-login button
(`<a href="/apps/oidc_login/oidc" class="oidc-button">`) visible.
Treat that button as a valid entry point so the spec stays green
regardless of whether auto-redirect actually fires. The existing
`kind === "social-login"` branch in `loginToStandaloneNextcloud` already
clicks through to Keycloak and waits for the credential form, so adding
the candidates is the only required wiring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous `production-fpm-alpine` floated to whatever Docker Hub tagged
production at deploy time, which produced non-reproducible Playwright runs in
CI: the same commit deployed against a slightly different NC build raced the
`pulsejet/nextcloud-oidc-login` boot-hook against earlier hooks emitting output,
losing the `header('Location:...')` and serving 200 on /login instead of the
expected 302 to Keycloak. Pin to the 33 major so cache, plugin compatibility,
and login flow are reproducible across host and CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two failures surfaced once the metadata-plugin fix stopped aborting
the Deploy web-app-nextcloud CI job before the Playwright suite ran:
✘ nextcloud talk admin settings (3 retries, 53s each)
✘ dashboard to nextcloud login (3 retries, 60s each)
Both failures funneled through `clickAllTalkTestServerButtonsAndVerify`
expecting the regex
`/OK:\s*Running version:\s*\S+[\s\S]*?OK:\s*Running version:\s*\S+/i`
to match `document.body.innerText` — i.e. two consecutive
`"OK: Running version: <ver>"` markers. The diagnostic from a fresh
deploy showed the actual rendered Talk-admin state:
Warning: Running version: 2.0.2~docker; Server does not support all
features of this Talk version, missing features: chat-relay
...
OK: Running version: 0.1 (the recording backend)
i.e. spreed's <SignalingServer> row reports `warningMessage`
("missing features: chat-relay") instead of the OK string, while the
<RecordingServer> row reports OK. spreed's `connectionState` getter
prefers `warningMessage` over the OK string by design (see
src/components/AdminSettings/SignalingServer.vue:136-150 of upstream
spreed), so the two-OK requirement is structurally false-fail-prone
on a healthy stack: a missing feature on the HPB image would always
turn this test red.
Fix the spec, not the deploy:
- Wait for `span.test-connection` to attach before counting buttons.
spreed's Test button is `v-if="server && checked"` and only appears
AFTER the auto-mounted `checkServerVersion()` call returns. Polling
too early found zero buttons, so `clicked.length > 0` could only
pass on a lucky race.
- Read the `<span class="test-connection">` texts directly via
`connectionSpans.allInnerTexts()` instead of pulling
`document.body.innerText`. The span is the canonical place spreed
writes connectionState, so the failure message can name the exact
rendered string ("Warning: ...", "Status: Checking connection",
"Error: ...") instead of dumping the whole admin chrome.
- Require ONE OK marker (recording row) instead of two. The
RecordingServer row only renders when `hasSignalingServers` is
true, so on a freshly-bootstrapped stack the recording row can be
the only one with a clean OK while the HPB row legitimately reports
a warning. Asserting both would block a healthy install on a
cosmetic feature mismatch.
- Iterate connection-span texts in the error-pattern check so the
failure message points at the exact row that broke instead of
matching against the whole body.
Verified locally via
`SERVICES_DISABLED='matomo,email' make deploy-reuse-kept-apps APPS=web-app-nextcloud`:
✓ nextcloud talk admin settings (23.8s)
✓ dashboard to nextcloud login (29.7s)
✓ biber logs into nextcloud via OIDC and logs out (4.7s)
PLAY RECAP: ok=1728, changed=131, failed=0, rescued=0, skipped=643.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`#user-menu button` is non-strict on Nextcloud 32+: the wrapper
`#user-menu` contains both the trigger button (aria-label "Settings
menu") AND the submenu's own buttons once it has been opened. The
biber OIDC logout test in roles/web-app-nextcloud/files/playwright.spec.js
then strict-mode-violated with:
locator.click: Error: strict mode violation:
locator('#user-menu button') resolved to 2 elements:
1) <button ... aria-label="Settings menu" ...>
2) <button ... class="button-vue button-vue--size-normal
button-vue--icon-only button-vue--vue-secondary">
Pin the locator to the trigger via its aria-label, with `> button` and
`.first()` fallbacks so localized aria-labels still work.
Surfaced once the metadata-plugin failure stopped aborting the deploy
before the test phase ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream Nextcloud "Metadata" app is not compatible with the
currently pinned Nextcloud server version, so `occ app:enable metadata`
failed every Deploy web-app-nextcloud CI job with:
App "Metadata" cannot be installed because it is not compatible
with this version of the server.
Disable the entry in roles/web-app-nextcloud/meta/services.yml until a
compatible release ships, with a comment so a future maintainer knows
why and when to flip it back on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Touches wordpress, nextcloud, yourls, and dashboard. Drops redundant
| default(false) where the value is contracted-set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The req-008/009/010 refactor's mechanical migration left several gaps
the test suite could not catch because the test suite itself was
silently broken in spots. Audit-and-fix pass closes those.
1. Production bug: backup discovery filter
==========================================
roles/sys-ctl-bkp-docker-2-loc/filter_plugins/find_dock_val_by_bkp_entr.py
still read `app.compose.services` from the materialised application
payload. After req-008 the file root IS the services map (no
`compose.services` envelope), so the filter walked an empty dict and
silently returned `[]` for every backup discovery call. Rerouted to
the new `app.services` path. Test fixtures updated to match.
2. External test broken by source_file mismatch
===============================================
tests/external/docker/test_image_versions.py:61 filtered for
`ref.source_file == "config/main.yml"`, but
utils/docker/image/discovery emits `meta/services.yml` since req-008.
The filter rejected every candidate → `entries == []` →
`self.assertTrue(entries)` failed. Switched the filter to the new path.
3. Five lint/integration tests silently iterating empty dicts
=============================================================
The following tests still read `cfg.get("compose").get("services")`
(the legacy compose-wrapper path). After req-008 every meta/services.yml
file root IS the services map, so these tests have been iterating
empty dicts and quietly passing without checking anything:
* tests/lint/ansible/test_service_shared_consistency.py:72
* tests/integration/backups/test_enabled.py:33
* tests/integration/backups/test_no_stop_required.py:35
* tests/integration/docker/test_services_image_version_valid.py:28
* tests/integration/oauth2_oidc/test_mutual_exclusive.py:26
All five rerouted to the file-root convention. test_service_shared_consistency
immediately surfaced 24 missing `# noqa: shared` markers (see below).
4. Lost suppression markers (51 total)
======================================
The migration script (tasks/utils/migrate_meta_layout.py) used
ruamel.yaml round-trip which preserved most comments but lost
trailing-comment markers on inline list items (a known ruamel edge
case). Restored verbatim from main:
* 13 `# nocheck: url` markers on meta/server.yml CSP-whitelist entries
(web-app-bluesky, web-app-flowise, web-app-matomo, web-svc-onlyoffice).
These cover CDN root URLs that legitimately return 404 for the bare
domain (Google Fonts, Matomo CDN, BlueSky API endpoint).
* 1 `# nocheck: docker-version` on web-app-moodle/meta/services.yml
(LTS pin for OIDC compatibility).
* 20 `# noqa: shared` markers on meta/services.yml — only the markers
that are still NEEDED. Initial restoration mirrored all 24 markers
from main, but 4 of them turned out redundant against the current
shape: bigbluebutton/postgres, bookwyrm/postgres, discourse/postgres,
magento/mariadb already declare `shared:` (true or false), so the
lint rule does not fire and the marker is dead weight. Skipped them.
* 13 `# noqa: email` markers on meta/services.yml — every covered
`email:` block has `{enabled: false, shared: false}`, exactly the
pattern the email-integration lint expects to suppress.
5. Test-fixture cleanups for backwards-compat clarity
=====================================================
Several unit tests still wrapped synthetic application payloads in
`{"compose": {"services": {...}}}`. Harmless because the production
code paths under test were already migrated, but misleading for future
contributors. Reshaped to match the post-req-008 file-root
convention:
* tests/unit/plugins/filter/test_node_autosize.py
* tests/unit/plugins/filter/test_docker_service_enabled.py
* tests/unit/utils/manager/test_inventory_manager.py (5 occurrences)
* tests/unit/cli/deploy/development/test_inventory.py
* tests/unit/roles/sys-ctl-bkp-docker-2-loc/filter_plugins/test_find_dock_val_by_bkp_entr.py
Validation
==========
* Unit: 1572 passed.
* Lint: 56 passed.
* Integration: 161 passed (1 skipped).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
req-011 — descriptive role-level metadata
* Move logo / homepage / video / display out of
meta/main.yml.galaxy_info into a new project-owned
roles/<role>/meta/info.yml (file-root convention, no `info:` wrapper).
* 78 role meta/main.yml files migrated via ruamel round-trip
(key-order / quoting / inline-comments preserved). Roles with none
of the four fields are left untouched (no empty info.yml created).
* web-app-dashboard/lookup_plugins/docker_cards.py reads logo +
display from meta/info.yml; description / galaxy_tags continue to
come from galaxy_info (Galaxy-spec fields).
* docker_cards.py now routes ALL its file reads through the
process-wide caches: load_yaml_any for vars/main.yml,
meta/main.yml, meta/info.yml; read_text from utils.cache.files for
README.md. The orphan `import yaml` is gone.
New utils/cache/files (renamed from tests/utils/fs)
* The process-wide filesystem cache (read_text, iter_project_files,
iter_project_files_with_content, _all_project_files) is not
test-specific; it now lives under utils/cache/files.py next to the
other process-wide caches. 34 consumer imports rewritten across
tests/ and docs/. The empty tests/utils/ package is removed.
* New unit suite tests/unit/utils/cache/test_files.py — 12 cases
covering read_text, iter_project_files (skip-dirs, extensions,
exclude_tests, exclude_dirs, walk-cache invariants), the combined
iterator, non-UTF-8 silent skip, and _reset.
utils/cache redundancy cleanup
* applications.py + users.py + cli/create/inventory/services_disabler.py
drop their direct yaml.safe_load calls and their private
_load_yaml_mapping/_load_yaml_variant_list copies; everything routes
through utils.cache.yaml.load_yaml / load_yaml_any. The dead
utils.cache.base._load_yaml_mapping is removed.
* users.py:_load_store_users (tokens.yml read) likewise migrated.
* _load_yaml_variant_list moves out of base.py to applications.py
(its only consumer) and is renamed _load_variants_overrides — also
routes through the cached loader now.
* utils.cache.yaml._reset_cache_for_tests is renamed _reset (kept as
alias) for parity with the per-domain reset helpers.
* utils.cache.files grows a _reset() that clears its two LRU caches.
* utils.cache.__init__._reset_cache_for_tests now orchestrates ALL
six caches (applications, users, domains, base, yaml, files) so
tests get clean state across cache layers — needed because
applications/users now share the yaml cache and would otherwise
serve stale parses across reset boundaries.
* utils.cache.files imports PROJECT_ROOT from base instead of
redefining it.
* tests/unit/utils/cache/test_data.py + test_base.py: obsolete
TestLoadYamlMapping / TestLoadYamlVariantList classes deleted
(covered by test_yaml.py + test_variants.py).
* README.md updated for the new module list (files, yaml) and the
six-module orchestrator.
Galaxy schema lint
* tests/lint/repository/test_role_meta_main_galaxy_schema.py: split
galaxy_info field requirements into hard-required {author,
description, license} and soft-recommended {min_ansible_version,
platforms}. Soft-recommended -> emit a GitHub Actions ::warning::
annotation via utils.annotations.message.warning (titled
"meta/main.yml galaxy_info" with file= pointing at the offender),
no test failure. Rationale: min_ansible_version + platforms are
only enforced by Galaxy when *publishing*, which Infinito.Nexus
does not do.
* Galaxy lint goes green (was 226 offenders before req-011 +
license_url/repository/documentation strip; was 62 before this
relaxation; now 0 hard offenders, 62 soft warnings).
* roles/svc-opt-keyboard-color/meta/main.yml: backfill `license`
(only role missing the hard-required field).
New lint
* tests/lint/repository/test_role_meta_info.py — enforces meta/info.yml
file-root convention, the four-field allowlist (logo, homepage,
video, display), and shape (logo.class string, homepage/video
non-empty strings, display bool). Also fails if any
meta/main.yml.galaxy_info still carries one of the four migrated
fields.
Docs
* docs/contributing/design/services/layout.md: new section
"Descriptive Role-Level Metadata — meta/info.yml" with allowed-
fields table, optionality rule, materialised path, and consumer
note. File-Layout table grows the meta/info.yml row;
meta/main.yml row gets the explicit "no logo/homepage/video/display"
note. req-011 added to header and Related Pages.
* utils/cache/README.md: files.py + yaml.py rows added; six-module
reset orchestrator documented.
Validation
* Unit: 1573 passed.
* Lint: 55 passed.
* Integration: 161 passed (1 skipped) via pre-commit hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These three sub-keys lived under `galaxy_info:` purely as a workaround
because Galaxy silently ignores unknown keys (same pattern as the
`run_after` / `lifecycle` migration in req-010). The new lint
`tests/lint/repository/test_role_meta_main_galaxy_schema.py` rejects
them, and no consumer in `plugins/`, `utils/`, `cli/`, `filter_plugins/`
or `roles/` reads any of them, so the strip is safe.
Round-trip via ruamel.yaml so the remaining `galaxy_info:` block keeps
its key order, quoting style and inline comments.
224 files touched. Remaining lint offenders after this commit are still
present (`logo`, `homepage`, `video`, plus a handful of roles missing
required Galaxy fields like `min_ansible_version` / `platforms` /
`license`); those are a separate decision and not folded into this
change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).
req-008 — role meta layout
* Move every role's metadata under meta/<topic>.yml with file-root
convention (no wrapping key matching the basename):
- schema/main.yml + config/main.yml.credentials → meta/schema.yml
- users/main.yml → meta/users.yml
- config/main.yml.server → meta/server.yml
- config/main.yml.rbac → meta/rbac.yml
- config/main.yml.compose.services + every other top-level key
of config/main.yml inlined under <entity>.<key>
→ meta/services.yml
- config/main.yml.compose.volumes → meta/volumes.yml
* Drop the legacy schema/, users/, config/ directories from every role.
* meta/schema.yml: support nested credential keys, optional `default:`
field (Jinja literal, not rendered/validated/regenerated), implicit
`algorithm: plain`.
* Materialised paths drop the `compose.` prefix:
compose.services.<e>.<…> → services.<e>.<…>
compose.volumes.<k> → volumes.<k>
<former-top-level>.<…> → services.<entity_name>.<former-top-level>.<…>
* Update every Python consumer (utils/cache, utils/manager, utils/applications,
plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
and task file to the new paths.
* No legacy fallback. Repository-wide grep for `compose.services.` /
`compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
* Add tests/lint/repository/test_role_meta_layout.py guarding against the
old paths returning, and an explicit lint guard that meta/variants.yml
files do not carry a `compose:` wrapper.
req-009 — per-role networks/ports
* Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
dns_resolver}.
* Delete group_vars/all/09_ports.yml entirely; ports now live on the
service entity that exposes them at
roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
* Rename + int-cast every former top-level `services.<entity>.port`
field to `services.<entity>.ports.inter` (single int).
* Always-category-keyed maps for ports.local / ports.public; relay
spans use {start, end} integers.
* Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
of truth for the per-<scope>.<category> band ranges, consumed by both
the new CLI helpers and the lint test.
* New CLI helpers (cli meta ports suggest, cli meta networks suggest)
walk the live role tree gap-first then increment-fallback, with
deterministic output and non-zero exit on capacity overflow.
* cli create role consumes both helpers when scaffolding a new role.
* Lint extensions: bare-int port shape rejection, host-bound port
collision check (single ints + relay span integers, inter excluded),
band-membership check with documented allow-list for the legacy
web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.
req-010 — run_after / lifecycle migration
* Move galaxy_info.run_after and galaxy_info.lifecycle out of
meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
lifecycle}; <primary_entity> is the value returned by
get_entity_name(role_name).
* For multi-entity roles whose primary entity has no compose service
(web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
gen roles), create a dedicated metadata-only top-level entry under
<primary_entity>.
* Introduce utils/roles/meta_lookup.py with get_role_run_after /
get_role_lifecycle that resolve <primary_entity> once and read from
meta/services.yml; degrade gracefully when meta/services.yml is
absent OR the field is absent; raise MetaServicesShapeError when the
file exists but is malformed.
* Update every consumer (utils/service_registry, utils/invokable,
cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
use the helper.
* Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
run_after/lifecycle never on a non-primary entity; empty run_after
lists rejected.
Also folded into this change set
* services_disabler now writes `services.<svc>.{enabled,shared}` into
host_vars instead of the old `compose.services.<svc>.{...}` path; the
discover/find pair walks meta/services.yml and only treats entries
carrying enabled/shared/provides as provider declarations (so
metadata-only entries from req-010 do not create spurious provider
rows).
* service_registry skips `run_after` deps whose target was filtered
out via SERVICES_DISABLED instead of failing the whole load.
* cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
consume the new applications.<role>.services.<svc>.image shape
(mirror.md updated to match).
* utils/domains/list.build_applications_from_roles now reads
meta/server.yml directly (it had been silently returning {} since
config/main.yml stopped existing).
* utils/roles/resource_validation.filter_roles_by_min_storage reads
meta/services.yml.<entity>.min_storage instead of the legacy
compose.services path.
* web-app-bluesky pre-clones the social-app build context BEFORE
sys-svc-compose so the build context is on disk by the time
sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
* 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
web-app-wordpress, web-app-keycloak) lose their `compose: services:`
wrapper so deep-merge over meta/services.yml lands on the new
file-root shape.
Validation
* Unit tests: 1571 passed.
* Lint tests: 50 passed.
* Integration tests: 161 passed (1 skipped).
* Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
discourse,mattermost}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- playwright.spec.js: add "biber logs into nextcloud via OIDC and logs
out" scenario covering the non-administrator persona required by the
MUSTs; parameterize loginToStandaloneNextcloud on username/password,
add a logoutStandaloneNextcloud helper, and wrap biber's first login
in a retry so the LDAP first-login-provisioning delay does not flake.
- playwright.env.j2: expose BIBER_USERNAME and BIBER_PASSWORD so the
spec can read biber credentials from the rendered env.
- docs/contributing/.../playwright.specs.js.md: codify the MUST that
existing scenarios may not be deleted while optimizing a spec.
- roles/web-app-nextcloud/docs/LDAP.md: document the biber first-login
caveat against fresh Nextcloud + LDAP deployments.
Validated via scripts/tests/e2e/rerun-spec.sh web-app-nextcloud:
3 passed (talk admin settings, dashboard to nextcloud login, biber).
Expose compose.services.{oidc,ldap}.enabled as NEXTCLOUD_OIDC_ENABLED and
NEXTCLOUD_LDAP_ENABLED in the Playwright env, and derive a single
`nextcloudLoginFlavor` in the spec:
- OIDC + LDAP -> "oidc_login" (pulsejet/nextcloud-oidc-login,
auto_redirect hands straight to Keycloak)
- OIDC only -> "sociallogin" (nextcloud/sociallogin shows a
"Log in with Keycloak" entry first)
- no OIDC -> "native" (no Keycloak handoff; NC credential form
filled with the direct admin password)
loginToStandaloneNextcloud now builds its initial-state candidate list
per flavor instead of accepting every possible login UI shape, and the
native flavor uses NEXTCLOUD_DIRECT_LOGIN_PASSWORD rather than the
Keycloak user password.
Verified via reuse-kept redeploy (make deploy-reuse-kept-apps
APPS=web-app-nextcloud SERVICES_DISABLED="css matomo email"): both
Playwright scenarios pass (2 passed, 26.3s).
Moves the SYSTEM_EMAIL_* resolution logic out of group_vars/all/03_email.yml
into a dedicated Ansible lookup plugin, so every role template reads outgoing
mail settings via lookup('email') (global) or lookup('email', application_id)
(per-app). Per-app compose.services.email is merged on top of the resolved
defaults; SYSTEM_EMAIL_* variables remain usable as an inventory escape hatch.
Also splits the email docs: administrator-facing configuration stays under
docs/administration/configuration/, plugin contract and extension rules move
to docs/contributing/design/services/email.md. services.md is relocated to
services/base.md so the services/ subtree can host per-service design pages.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Role-local lookup_plugins/ only autoloads when the role executes, but
oidc_flavor is called at config-merge time from
roles/web-app-nextcloud/config/main.yml during the 01_constructor stage.
Ansible could not resolve the lookup and failed the merge.
Relocate the plugin to plugins/lookup/ (the path declared in
ansible.cfg) so it is discovered before role execution, and move the
unit tests alongside it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reintroduce the conditional OIDC-plugin selection that was lost when commit
77a0e16ea removed the _applications_nextcloud_oidc_flavor helper. Ship a
role-local lookup plugin at roles/web-app-nextcloud/lookup_plugins/oidc_flavor.py
with resolution order: explicit compose.services.oidc.flavor > oidc_login if
LDAP enabled > sociallogin. Wire all consumers (plugin toggles + oidc.config.php)
through lookup('oidc_flavor') and default compose.services.oidc.flavor to "" so
the LDAP-aware default takes effect (None would trip the non-empty-config
integration guard). Add 12 unit tests covering override, LDAP on/off,
whitespace, non-string, missing-app, and input-validation paths; evict
stubbed utils.applications.config leaked by the keycloak redirect_uris test
before loading the plugin so discovery-order no longer matters. Seed missing
__init__.py in tests/unit/roles/web-app-peertube and
tests/unit/cli/meta/applications/ressources so the repo-wide init-file guard
passes again.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce a single global flag in group_vars/all/10_networks.yml and wire
the previously hardcoded IPv6 settings in nextcloud, bigbluebutton, and
matrix through it. Matches the host-level `make disable-ipv6` policy so
containers no longer stall on AAAA-first lookups (e.g. Nextcloud's
DnsPinMiddleware) while keeping a single knob to flip when v6 is wanted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The appstore-fetcher caches a 300s failure cooldown on any fetch
error. With retries:6/delay:30 the whole 180s retry window stayed
inside one cached-empty catalog, so plugins that transiently 404
('not found on the appstore') never recovered. Clearing
settings/appstore-fetcher-lastFailure before each attempt forces a
fresh catalog fetch on retry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove silent tolerance for `occ app:install` errors — both
"not compatible with this version of the server" and
"not found on the appstore" now propagate as task failures.
Rationale: these are config or upstream issues (typo, removed app,
NC-version mismatch), not transients. 6× 30s retries still absorb
real flakes; residual failures deserve to surface, not to ship a
"deployment" that silently lacks features the config lists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure whitespace + comment adjustment: add spaces around the `| int`
filter pipes and pull the explanatory comment to its own line instead of
trailing. No functional change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce a single global flag in group_vars/all/10_networks.yml and wire
the previously hardcoded IPv6 settings in nextcloud, bigbluebutton, and
matrix through it. Matches the host-level `make disable-ipv6` policy so
containers no longer stall on AAAA-first lookups (e.g. Nextcloud's
DnsPinMiddleware) while keeping a single knob to flip when v6 is wanted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Beyond the existing error-string absence check, require the Talk admin page
to render at least two "OK: Running version: X" markers after the test
buttons are clicked. Those markers come from the auto-run reachability
checks that spreed fires on mount for the HPB signaling server and the
recording backend. TURN/STUN are left to the error-absence check because
the Playwright container can't rely on WebRTC UDP reachability to coturn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces inline `applications`/`users` merging with dedicated lookup
plugins and runtime hydration. Introduces `add_csp_hash` filter,
integrates sort into `find_dock_val_by_bkp_entr`, splits the Keycloak
one-shot realm import into `05a_realm_import_run.yml`, and fixes the
Mastodon administrator task YAML. Removes obsolete `csp_hashes` filter,
`sys-token-store` Jinja filter, and empty CLI setup modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the Talk admin Playwright check to also click every "Test server" /
"Test this server" button on the admin page and fail if any of the known
spreed error strings (Cannot connect, No working ICE, Server seems to be a
Signaling server, Testing server seems to be broken) appear afterwards. This
verifies the HPB signaling / STUN / TURN endpoints are actually reachable,
not only that the configured values are rendered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rewrite loginToStandaloneNextcloud to drive the OIDC flow (Nextcloud -> Keycloak
-> Nextcloud) instead of the native credential form, which is hidden by
oidc_login_hide_password_form. Add section-divider and WHY comments to make the
spec easier to read.
Provide defaults for the compose.services.oidc.flavor lookup so oidc_login stays
enabled when the key is absent, and default user_ldap to enabled when the LDAP
service config is missing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single source of truth at compose.services.oidc.flavor. Removes stray
root-level keys, deletes the _applications_nextcloud_oidc_flavor helper,
and switches all consumers (plugin toggles + PHP template) to direct
lookups. Also fixes inherited precedence bug on plugin.enabled filters
and corrects OICD/floavor typos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- plugins/lookup/config.py: honor default by setting skip_missing_app=not strict
so lookup('config', app, path, default) returns the default when the app is
missing from the applications dict, instead of raising during early constructor
phases. Fixes 5 test-development jobs.
- roles/web-app-nextcloud/files/playwright.spec.js: replace unsupported
.toSatisfy() with a boolean check inside expect.poll() + .toBe(true).
Fixes Deploy web-app-nextcloud.
- roles/sys-svc-container/files/container.py: when the target image has no
ENTRYPOINT, inject the internal CA as a volume plus NODE_EXTRA_CA_CERTS,
SSL_CERT_FILE, REQUESTS_CA_BUNDLE and CURL_CA_BUNDLE instead of falling
back to a plain 'docker run' without CA trust. Fixes npm
UNABLE_TO_VERIFY_LEAF_SIGNATURE inside Microsoft Playwright images for
moodle, prometheus and onlyoffice deploys.
- tests/unit/roles/sys-svc-container/files/test_container.py: cover the new
no-ENTRYPOINT CA-injection path.
Note: web-app-mini-qr 'lookup ghcr.io: no such host' is not addressed here
and likely needs a separate investigation of CoreDNS forwarding on the CI
runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 07_admin_identities.yml: resolves LDAP-backed admin identities
for OCC calls that require the actual NC username, not the LDAP DN
- vars: add NEXTCLOUD_HPB_TURN/STUN_ACTIVE_CONFIG selecting standalone
or onboard TURN based on enabled flags; add TALK_ADMIN_URL
- config: talk container gated on spreed plugin flag
- env.j2 + playwright.env.j2: expose active TURN/STUN config and
Talk admin URL for container env and E2E tests
- playwright.spec.js: extend with Talk TURN/STUN E2E assertions
- schema: add spreed.enabled validation
- coturn compose: move relay port range before TLS block so ports are
always set regardless of TLS mode
- docs/requirements: document TURN relay requirement
- test_resolver: cover new resolution paths
Resolve conflicts:
- docs/agents/action/iteration.md: keep both new bullets (SERVICES_DISABLED
proposal + make test requirement)
- docs/contributing/design/services.md: deleted (renamed to
docs/contributing/code/services.md in this branch)
- group_vars/all/20_services.yml: keep deletion from this branch (req-001)
- tests/lint/ansible/roles/test_service_core_first_task_run_once.py:
use build_service_registry_from_roles_dir (req-001) with repo_root()
from main for robust path resolution
Fix broken relative links caused by rename and deletion of files:
- docs/contributing/design/architecture.md: services.md → ../code/services.md
- docs/contributing/design/exceptions.md: load_frontend_service.yml →
load_service.yml
- docs/requirements/001-service-registry-refactoring.md: fix paths for
unit.md, integration.md, principles.md, documentation.md, AGENTS.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete group_vars/all/20_services.yml; all service metadata now lives
in each role's config/main.yml under compose.services.<name>
- Rename generic 'database' service key to explicit 'mariadb'/'postgres'
across all app roles; remove legacy type: field
- Add utils/service_registry.py and plugins/lookup/service_registry.py
as the single source of truth for service discovery
- Add utils/database_service.py to centralize DB service resolution
- Delete service_load_enabled filter and service_should_load lookup;
replaced by service lookup plugin
- Update sys-utils-service-loader to run in constructor stage and use
generic load_service.yml; remove load_frontend_service.yml
- Add _service_loader_preloading guard to sys-svc-compose,
sys-svc-webserver-core and sys-ctl-alm-compose to prevent
health-checks/alarms from firing during preload phase
- Replace manual load_app.yml calls for shared DB/Mailu with assert-
based preload validation in sys-svc-rdbms and sys-svc-mail
- Update networks.yml.j2 templates to resolve LDAP/AI networks via
service_registry instead of hardcoded role names
- Migrate all tests to new service discovery API; delete obsolete tests
- Update docs/contributing/code/services.md to reflect role-local model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace legacy filter with the canonical lookup plugin across
nextcloud config, group_vars, dashboard template, and vars
template. Switches Talk TURN from onboard to standalone coturn
by default; gates talk container on spreed plugin flag.
Adds lint test to catch remaining get_app_conf usages.
- Apply documentation.md rules across staged files (emojis after headlines, RFC 2119 keywords, correct link text, Credits format)
- Remove expired ChatGPT share URLs from 39+ role READMEs and templates
- Remove orphaned text after link deletion: bare # @see, {# @see #}, Discussion: #}, empty Chat Conversation sections, dangling "Conversation is here available.", "Refer to the conversation with ChatGPT ()", "See ChatGPT Conversation", "ChatGPT Session Transcript" references
- Remove empty ## Further Resources and ## Credits sections where no links remain
- Fix URL reachability test: treat 401/403/405 as ok, 429/500/503 as warn, Timeout as warn; add # nocheck: url suppression (inline and line-above)
- Add lint test for broken relative and repo-root-absolute markdown links
- Add # nocheck: url to AUR git clone URLs, CSP allowlist entries, and other non-browser URLs
- Restore docs.infinito.nexus/setup in documentation.md MUST NOT example
- Fix Mobilizon geospatial endpoints (Addok, Photon) reverted to original values with # nocheck: url
- Add integration test rule: MUST NOT write unit tests for integration test logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add tests/lint/repository/test_markdown_links.py — a hard-fail lint
test that scans all git-tracked markdown files for broken relative and
repo-root-absolute links. Skips external URLs, anchor-only links, fenced
code blocks, and placeholder targets.
Apply documentation.md rules to all staged files: add emojis after
headlines in docs files, remove emojis from role README headings,
standardize Credits sections to the canonical format, remove empty
sections and orphaned sentences referring to missing documentation files,
fix raw URLs and incorrect link text across role READMEs and docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'email' key (role: web-app-mailu, type: frontend) to SERVICE_REGISTRY
- Declare compose.services.email with enabled: true and shared: true in all
33 app roles that reference web-app-mailu or use SYSTEM_EMAIL_* variables
- Add integration test test_mailu_service_dependency.py that enforces this
constraint for any future role additions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add shared: true to logout, oauth2, and greenlight services wherever
enabled: true was set without a shared declaration
- Add # noqa: shared comment before role-local internal containers
(redis, worker, web, opensearch, search) that are intentionally
not shared
- Add missing enabled: false to ollama in svc-ai-ollama (shared was
declared without enabled, violating the new consistency rule)
- Add integration test test_service_shared_consistency to enforce:
Rule 1: enabled=true requires shared=true or a # noqa: shared comment
Rule 2: shared key requires enabled key to be present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ensures web-svc-cdn is loaded whenever any app has css or javascript
injection enabled, by registering css and javascript as service keys in
20_services.yml (both mapping to web-svc-cdn). Adds shared: true to all
css/javascript compose service entries across app and svc role configs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a SPOT for all frontend service loading in sys-front-inj-all/tasks/01_services.yml,
driven by a new generic `service` lookup plugin that resolves services by key or role name
and returns {id, role, enabled, shared, needed}. Needed = enabled AND shared (direct or
transitive). The canonical key→role mapping moves to group_vars/all/20_services.yml
(previously scattered hardcoded rules). Removes per-role service loads from bigbluebutton,
nextcloud, and dashboard. Renames 01_dependencies.yml→01_services.yml and
08_services.yml→08_daemons.yml. Adds flush_handlers after each service load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The colorscheme-generator produces --color-rgb-XX-YY tokens. All role style
files were using the non-canonical --color-XX-rgb-YY alias format. Taiga had
a local alias block mapping the old format to the correct one; that block is
now removed as it is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>