Replace inline `networks:` blocks in every service of web-app-kix and
web-app-nextcloud with `{% include 'roles/sys-svc-container/templates/networks.yml.j2' %}`,
matching the convention the other ~70 roles already follow. The include
derives shared LDAP/DB/Ollama network attachments from `service_registry`
and the role's `services.*.shared` flags, so the hardcoded `openldap:`
attachment in kix's backend (and the missing shared-network handling in
nextcloud's proxy/HPB/whiteboard/recording services) become consistent.
Also fix a latent indent bug in web-app-nextcloud's HPB service: when
`NEXTCLOUD_HPB_NETWORK_MODE \!= 'bridge'`, `network_mode:` was rendered
at 2-space indent (service-name level) instead of 4 (service-property
level), which would have broken `docker compose config` in host/none
mode. Hidden until now by the `bridge` default.
Add `tests/integration/infrastructure/compose/test_network_includes.py`
to lock in the convention: forbids literal `networks:` keys in
compose.yml.j2 and requires exactly one top-level
`sys-svc-compose/networks.yml.j2` include per file (exempting templates
that exclusively use `network_mode:`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new steps wrapped around the existing push/create flow:
1. Dedupe before push: hash the local diff vs origin/<base-branch> and
compare against gh pr diff of every open PR (any author, any branch)
targeting the same base. On a match, exit 0 without pushing and
without creating a PR. This is what stops the same image-version
bump from being opened repeatedly.
2. Supersede after create: close every other open PR whose branch
starts with UPDATE_BRANCH_PREFIX with a "Superseded by #N." comment.
The prefix filter keeps the bot from ever closing repository-refs,
skills, or human-authored PRs, even though dedupe itself sees them.
CRLF normalization on both sides of the hash compare so a runner that
checks out the patch with native-CRLF translation still produces a
stable hash.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The maven ecosystem entry was added for a Bluesky Keycloak
event-listener SPI scaffold that has since been removed. Without a
pom.xml anywhere in the tree, every scheduled run fails with
manifest_not_found, producing a daily red Dependabot job.
Comment the block out in the same style as the other "no X found"
entries, with a note about why so a future re-enabler knows what to
restore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group the shell helpers under scripts/github/ into five subfolders
that match how the workflow files consume them:
runner/ host-runner hygiene (disk, swap, Docker daemon, diagnostics)
cancel/ run cancellation on PR-close and branch-delete events
resolve/ derivation of workflow inputs and outputs from repo state
update/ scheduled Update: Versions pipeline
release/ release-time gates
Drop the now-redundant subfolder prefix from each filename, e.g.
dump_runner_state.sh becomes dump_state.sh and cancel_pull_request_runs.sh
becomes pull_request_runs.sh. Each subfolder carries a README that
describes its scope per documentation.md rules; the top-level README
points at the subfolders without inventorying files.
Propagate the new paths through ten workflow files, three docs
(workflow.md, pipeline.md, secrets.md), and two tests. Fix a real
runtime bug in enlarge_swap.sh, which still referenced the
pre-rename dump_runner_state.sh path. Extend the inline-block-size
lint hint to describe the new subfolder convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workflow-driven update PRs opened with GITHUB_TOKEN do not start
further workflow runs, so the CI orchestrator never executed against
them. Each PR-opening job in update.yml now mints a short-lived
GitHub App installation token via actions/create-github-app-token@v2
and forwards APP_SLUG to open_update_pr.sh, which derives the
verified-commit identity from the App user.
The job-level permissions block drops to read-only since write access
is provided exclusively by the App token. Behaviour with the secrets
unset is fail-loud: the token step fails, the PR step is skipped,
and no PR with an unintended identity is ever created.
Requires the BOT_APP_ID and BOT_APP_PRIVATE_KEY repository secrets
documented in docs/contributing/tools/github/actions/secrets.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the previous two-file plugin pipeline into three single-responsibility
stages and group them under tasks/plugin/, with per-plugin hooks now sitting
under tasks/plugin/hooks/. Each stage owns exactly one OCC call (app:install,
app:enable, config:app:set) and one continuation decision driven by the
nextcloud_install_status filter.
Layout:
tasks/
├── 06_plugin.yml (entry; includes plugin/01_install.yml)
└── plugin/
├── 01_install.yml (occ app:install + classify)
├── 02_enable.yml (occ app:enable + classify)
├── 03_configure.yml (occ config:app:set + run hook)
├── hooks/ (per-plugin extension tasks)
│ └── user_ldap.yml
└── README.md (pipeline contract + extension guide)
The new 02_enable.yml mirrors the version-compatibility tolerance that
01_install.yml already had: when occ rejects the enable with the
"is not compatible with this version of the server" marker, the
failed_when clause downgrades the task to a non-fatal classification,
emits a debug warning, and skips the entire 03_configure.yml include via
a single set_fact-driven flag instead of repeating the filter call on
every downstream task. This unblocks deploys against a Nextcloud release
whose appstore lags briefly while the plugin's compatibility metadata
catches up (e.g. metadata, drawio, files_bpm in the NC 33 window).
NEXTCLOUD_CNODE_PLUGIN_TASKS_PATH in vars/main.yml is rebased to the new
tasks/plugin/hooks/ directory so plugin hooks resolve at the new location
without touching call sites.
README.md follows the role + docs README contracts: one sentence per line,
no em dashes, RFC 2119 keywords, no historical narrative, cross-links to
the filter plugin and the role vars file.
Satisfies tests/lint/ansible/roles/test_file_spot.py — the hard-coded
"roles/web-app-foo/tasks/main.yml" literals now go through the
utils.roles.mapping.ROLE_FILE_TASKS_MAIN SPOT.
Mirror scope.sh's is_markdown_or_rst skip-list in affected_roles.sh so a
PR that touches both a role and a doc file outside roles/<role>/ resolves
to that role only, not __ALL__. Covered by tests/integration/meta/workflows/test_diff_affected_roles.py.
Commit 1f6b5f894 fixed the mailu V1 guest 60-s timeout by adding inline
auto-detection skips to the shared persona runners (`\!oidcEnabled`,
`dashboardEnabled === "false"`), but those guards are exactly what the
persona strict-mode lint
(tests/lint/ansible/roles/web-app/playwright/persona/test_strict_mode.py,
Rule 11) forbids: auto-detection skips can silently disable tests
when an env flag is mistyped, so only the allowlisted opt-out tokens
(`PERSONA_*_BLOCKED`, `safeSkipUnlessEnabled` / `skipUnlessServiceEnabled`,
the auth-less collapse case) are permitted.
Re-fix in the lint-compliant shape:
- Drop the four inline `test.skip(...)` blocks from
personas/{admin,biber,guest}.js plus the `safeIsEnabled("dashboard")`
parity marker added alongside them.
- Add the missing `PERSONA_GUEST_BLOCKED` opt-out check to
personas/guest.js so the existing biber/administrator block pattern
also covers guest.
- Render `PERSONA_GUEST_BLOCKED` dynamically in
roles/web-app-mailu/templates/playwright.env.j2 — true exactly when
`services.dashboard.enabled` is false. Drops the now-unreferenced
`DASHBOARD_SERVICE_ENABLED=` line.
Behaviour for mailu V1 stays identical (guest persona skipped, just
via PERSONA_GUEST_BLOCKED rather than via persona-internal hostname
sniffing). Verified against the live stack with rerun-spec.sh across
all 13 currently staged Playwright roles — 46 pass / 44 skip / 0 fail,
0 deltas vs. the prior fresh-purged V2 deploy outcomes.
If another role later hits the same dashboard-frame routing failure
in a fresh-purge sweep, the fix is one analogous
PERSONA_GUEST_BLOCKED={{ ... }} line in that role's
templates/playwright.env.j2.
NC 33's login form labels the username textbox simply "Account name"
(no "or email" suffix), so the prior regex matched nothing and every
login-via-native-form spec timed out at the 5-s default. Broaden the
pattern to accept either label shape.
The biber-LDAP scenario also failed at the logout assertion: the
universal-logout patch from sys-front-inj-logout rewrites the click
target to Keycloak's logout endpoint, which in user_ldap-only mode
ends on Keycloak's "You are logged out" info page rather than NC's
/login. Replace the negative shell-absence check with a positive
poll on the Keycloak logout URL that matches what the universal-logout
patch actually does.
Same fix as the previous baserow/bluesky commit: the LDAP scenario
asserts the oauth2-proxy → Keycloak handoff and only renders a
Keycloak login form when both `ldap` and `oidc` services are deployed.
Skipping when oidc is off stops the false fail on the LDAP-only variant
where no redirect to Keycloak ever fires.
The LDAP scenario asserts the oauth2-proxy → Keycloak (LDAP-federated)
handoff, which only renders a Keycloak login form when both `ldap` and
`oidc` services are deployed. Gating solely on `ldap` made the spec run
under the LDAP-only variant (oidc=false, oauth2=false), where
`performKeycloakLoginForm` then 60-s-timed-out because no redirect to
Keycloak ever fires. Skipping when oidc is off keeps the scenario as a
combined-mode smoke test and stops the false fail on LDAP-only deploys.
Refines the explicit HTTPS default_server introduced in e9df1ae7b:
instead of aborting the TLS handshake (curl→000), complete TLS with
the primary-domain cert and return 421 Misdirected Request (RFC 9110
§15.5.20). When the cert SAN still covers the requested host but no
vhost is configured for it — the exact failure mode that
sys-svc-cln-domains produces between matrix-deploy variants — clients
now see a precise, RFC-correct signal instead of an opaque TLS error.
For hostnames not in any cert SAN (production with per-domain LE
certs and unrelated probes), curl still rejects the cert and reports
000, so the response is naturally dual-mode:
- SAN match + no vhost → 421 (deploy-incomplete signal)
- no SAN match → 000 (truly unknown host)
Updates scripts/tests/environment/08_deploy_performance.sh: the
matomo-only fresh-purged deploy keeps dashboard.infinito.example in
the shared CI cert's SAN list, so the assertion changes from
`assert_http_status 000` to `assert_http_status 421`. The matomo
assertion in 07_deploy_minimal.sh stays at 000 because matomo is
excluded from the cert SAN in that deploy's CA regen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a `listen 443 ssl default_server` block with `ssl_reject_handshake
on;` to default_server.conf.j2. Without this, when an SNI request hits
a hostname whose vhost has been purged (e.g. dashboard.infinito.example
after the fresh-purge sweep between matrix-deploy variants), openresty
silently falls onto whichever ssl-listening server happens to load
first and responds — bluesky-pds caught dashboard.infinito.example in
CI job 76714878217 and served its text/plain landing.
`ssl_reject_handshake on;` closes the TLS connection before any
certificate selection happens, so curl receives http_code "000" — the
exact contract the env-test `scripts/tests/environment/08_deploy_
performance.sh` already asserts after a matomo-only fresh-purged
deploy. Currently four devenv jobs (debian / centos / fedora /
manjaro) in workflow run 26089179898 fail with
`[FAIL] https://dashboard.infinito.example returned HTTP 301,
expected 000` because some downstream vhost answered instead.
Requires nginx >= 1.19.4 (openresty bundles 1.27 in this stack, so
the directive is available).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI run 26089179898 job 76714878217 (Deploy web-app-bluesky, variant 1)
failed at the mailu Playwright `guest: public-landing → auth chain →
never authenticated` test with 60s timeout × 3 retries. Root cause is
two layered defects:
1. Between Round 1 and Round 2 of the matrix deploy the fresh-purge
sweep removes the web-app-dashboard vhost and container (mandate-
fresh-purged on first contact with a new VARIANT, see commit
3f12fbe83). Round 2 in variant 1 deploys only matomo/mailu/keycloak/
bluesky; the dashboard.infinito.example vhost never comes back.
2. mailu's templates/playwright.env.j2 pins APP_BASE_URL to
`lookup('tls', 'web-app-dashboard', 'url.base')` unconditionally —
it has to, because the shared persona helpers route through the
dashboard main-frame Account menu. In variant 1 (dashboard disabled
for mailu) that URL is dangling: nginx's default_server falls onto
the bluesky-pds backend, which serves a text/plain ASCII banner.
readCspString's `<meta>` fallback in personas/utils/csp.js has no
explicit getAttribute timeout and waits on the test-level 60s.
The shared persona itself has no way to learn that APP_BASE_URL routes
through a dashboard frame whose service is disabled — biber + admin
sidestepped this only because mailu already blocks them via
PERSONA_BIBER_BLOCKED / PERSONA_ADMINISTRATOR_BLOCKED for an unrelated
silent-SSO escape. guest had no equivalent opt-out and timed out.
Decided by service flag, not by sniffing the response, so the skip
fires before any network I/O:
- roles/test-e2e-playwright/files/personas/guest.js: skip when
APP_BASE_URL's host starts with "dashboard." AND
DASHBOARD_SERVICE_ENABLED=false.
- roles/test-e2e-playwright/files/personas/biber.js +
personas/admin.js: same dashboard-frame skip + a new OIDC-required
skip (these personas drive the Keycloak round-trip; without OIDC
they cannot reach an authenticated surface). Skips placed
immediately before the dependent operation, not at the top, so the
reason matches the operation that would actually fail. parity-marker
safeIsEnabled("dashboard") added to the existing oauth2/logout/
matomo block so the env-gate parity guard recognises the new
consumption.
- roles/web-app-mailu/templates/playwright.env.j2: emit
DASHBOARD_SERVICE_ENABLED in the existing OIDC/LOGOUT/MATOMO_SERVICE
_ENABLED block. The other 14 dashboard-frame roles will pick up
this declaration when they hit the same variant-disabled failure.
biber + admin remain unconditional-blocked for mailu via the existing
PERSONA_*_BLOCKED flags; the new skips inside them are forward-looking
for the day the silent-SSO escape lifts and those personas are
un-blocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- utils/inventory/bundle_apps.py: replace the local REPO_ROOT
`Path(__file__).resolve().parents[2]` with the canonical
`from utils import PROJECT_ROOT`. Move `Iterable`/`Path` imports under
`TYPE_CHECKING` since they are now only referenced in type hints.
- Makefile: strip trailing newline from the redeploy-bundles target file.
Re-enable bbb, files_bpm, and metadata plugins. Their previous opt-out
existed only because the upstream Nextcloud appstore release lagged the
pinned server major, which made `occ app:install` fail and abort the
deploy.
Introduce a role-local Jinja filter `nextcloud_install_status` that
classifies the registered `occ app:install` result into a status dict
(ok/changed/runnable/already/incompatible). The install task now uses
this filter in `until`/`failed_when`/`changed_when`, so a plugin
emitting "is not compatible with this version of the server" terminates
the retry loop immediately, downgrades to a debug warning, and skips
the subsequent enable/configure path instead of failing the play.
Net effect: operators no longer have to toggle services.yml when an
upstream plugin temporarily lags the pinned Nextcloud version; the
deploy proceeds and the plugin auto-recovers on the next run once a
compatible release ships.
Mirrors the contract that 0ef2a3481 established for sys-ctl-hlth-webserver
(reads status_codes.default from meta/server.yml) so the CSP probe stops
flagging federation-only roles like web-app-bridgy-fed as unhealthy.
Previously, sys-ctl-hlth-csp invoked `node health-csp.js` against every
nginx vhost's `/` and treated any non-2xx/3xx as "Unable to reach". For
web-app-bridgy-fed, whose canonical surfaces are the federation-protocol
endpoints (/robots.txt, /.well-known/host-meta, /web/<actor>) and whose
Flask app deliberately serves 404 on `/`, the probe failed, systemd
retried 10x over ~10 min, and sys-service-terminator aborted the deploy
with rc=2 (CI job 76714878060).
The Playwright spec already encoded the contract: 4xx-at-/ is by design,
5xx is the only unhealthy signal. The fix in 0ef2a3481 surfaced that
contract via status_codes.default for hlth-webserver but did not extend
it to hlth-csp.
This change adds:
- A new csp_skip_domains filter (filter_plugins/csp_skip_domains.py)
that walks every selected application, reads
server.status_codes.default, and returns the canonical+alias domains
of any role declaring a code >= 400. Follows the
web_health_expectations pattern.
- HEALTH_CSP_SKIP_DOMAINS in vars/main.yml, computed once via the
filter.
- --skip-domain repeatable CLI flag in files/script.py that filters
the discovered nginx-config domain list before URLs are built.
Side benefit: web-app-listmonk (default: 404) and web-svc-html
(default: [403, 200]) are now also correctly skipped if deployed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generalises the postgres image build to be driven by a declarative
extension→library registry instead of a hardcoded POSTGRES_VECTOR_ENABLED
flag. Same Dockerfile.j2 now serves both the central svc-db-postgres
image (libraries aggregated across all deployed consumers) and per-
consumer images for shared:false postgres consumers whose extensions
need libraries not in the postgis/postgis base (e.g. pgvector).
postgres.yml.j2 switches between image: and build: based on whether
custom libraries are needed. New postgres_libraries lookup filters
base-image extensions out and returns recipes for the rest. Dedicated
task path extracted into dedicated.yml + dedicated_postgres.yml so the
block-with-when size limit stays satisfied and the postgres-type check
happens once.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shared:false is now a real path (apply_extensions via initdb.d in
sys-svc-rdbms). The static pin was a quick fix that's no longer
needed; each consumer follows the standard dynamic convention again.
Image-library availability (postgis/pgvector in vanilla postgres) is
the remaining shared:false constraint and stays a separate concern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sys-svc-rdbms now renders an extensions.sql into a per-consumer
.initdb.d/ directory and bind-mounts it into the dedicated postgres
container at /docker-entrypoint-initdb.d/. CREATE EXTENSION runs as
POSTGRES_USER (= bootstrap superuser) on first initdb so non-trusted
extensions succeed. Same declarative source (service_extensions
lookup, services.yml extensions field) drives both shared:true
(02_init.yml loop) and shared:false (initdb.d) paths. database lookup
exposes a new initdb_dir field; service_extensions lookup loses its
shared:true guard (shared:false is now a real path). Refactored
repeated lookups in sys-svc-rdbms tasks into vars/main.yml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The assertion `assertNotIn("{{", result["password"])` flaked when
`strong_password` happened to emit two `{` adjacent in the random
password. The intent is to verify the Jinja template was rendered,
so match a full `{{...}}` block instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- new utils.inventory.bundle_apps resolver aggregates and dedupes role
groups from inventories/bundles/{servers,workstations}/<name>/inventory.yml
- new make targets deploy-bundles (fresh-purged) and redeploy-bundles
(reuse-kept), driven by BUNDLES=... env, wrapped by
scripts/tests/deploy/local/deploy/{bundles,redeploy-bundles}.sh
- unit tests for the resolver under tests/unit/utils/inventory/
- document the new targets in docs/contributing/tools/make.md,
docs/contributing/actions/testing.md, inventories/bundles/README.md
- collapse multi-line Makefile comments to one-liners for consistency
- disable nextcloud bbb plugin auto-enable (commented out)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-app nginx gateways used proxy_pass with a literal docker container
hostname. nginx OSS resolves that hostname once at startup; if the
upstream container later restarts and gets a new IP (e.g. a partial
compose-up after credential change), the gateway proxies onto a dead
address until nginx itself reloads — the 502 we hit on taiga after a
reuse-kept replaced the taiga-back container.
Switch every per-app docker-hostname proxy_pass to a variable form so
the embedded docker DNS re-resolves at request time:
set $upstream_<rolename>_<purpose> http://<host>:<port>;
proxy_pass $upstream_<rolename>_<purpose>;
The full `resolver` directive (TTL + ipv6 toggle inherited from
NETWORK_IPV6_ENABLED) lives in one SPOT — NETWORK_DOCKER_DNS_NGINX_DIRECTIVE
in group_vars/all/08_networks.yml — so cache TTL and ipv6 stay
consistent across the stack.
In taiga the upstream `set` declarations hoist to server scope because
taiga-back is reused across /api/, /oidc/ and /admin/; in nextcloud
and kix each upstream is used exactly once so the declaration stays
local. Locations whose original proxy_pass stripped the location
prefix (`proxy_pass http://.../;` on /media/, /whiteboard/,
/standalone-signaling/) keep that semantics via an explicit
`rewrite ^<loc>(.*)$ /\$1 break;`, because variable-proxy_pass drops
the implicit prefix-strip.
kix's static files/kix-proxy.default.conf moves to
templates/kix-proxy.default.conf.j2 so it can reference the new
constant; the deploy task switches from copy to template accordingly.
Drive-by: scripts/tests/e2e/rerun-spec.sh docstring refresh — the
"only overwrites tests/playwright.spec.js" claim went stale when the
rerunner started restaging every role-local playwright/*.js.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The internal container port slot under <entity>.ports is now a
category-keyed map (matching the local/public shape) instead of a
single int. "internal" replaces "inter" because the latter is
ambiguous in English (could read as "between").
Affected:
- 8 role meta/services.yml (matrix, ollama, mig, shopware, oauth2-proxy,
gitea, listmonk, sphinx) and every caller (vars, templates, group_vars).
- web-app-matrix: 8008 hardcoded across compose.yml.j2, homeserver.yaml.j2,
tasks/01_docker.yml, and 7 mautrix bridges now reads
MATRIX_SYNAPSE_CONTAINER_PORT from services.synapse.ports.internal.http.
- web-app-matrix: admin username/password lookups consolidated as
MATRIX_ADMINISTRATOR_{USERNAME,PASSWORD} in vars/main.yml, sourced from
lookup('users', 'administrator').
- Tooling: cli/meta/ports/suggest, utils/meta/scan, integration scan,
migrate_meta_layout/role_builder updated to the new key name.
- Docs: design/role/services/layout.md, tools/ports-suggest.md,
requirements 009/021, image/origin.md, PR template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the runtime gate in vars/main.yml :: DASHBOARD_JS_FILES so the
no-OIDC variant no longer fails on missing oidc.js.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YAML folds the line break inside the double-quoted scalar into a
single space, so the resolved value is byte-identical to before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codify the rule that switching the live stack onto a previously-untouched
VARIANT must go through deploy-fresh-purged-apps, never a reuse target.
Reuse re-pins the stack onto stale volumes, DB rows and network aliases
from the previously-pinned variant and produces split-brain app state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each consumer (bookwyrm/mobilizon/discourse) now declares its required
postgres extensions in its own meta/services.yml under postgres.extensions
and pins shared: true. svc-db-postgres' 02_init.yml drops the hardcoded
postgis-gate and unconditional vector steps and loops over the new
lookup keyed by database_consumer_id. The lookup fast-fails if a role
declares extensions without pinning shared: true.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns with the rest of the repo's meta/variants.yml files which start
with the explicit '---' marker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bridgyfed.infinito.example serves HTTP 404 on / by design — the
canonical landing surfaces are federation-protocol endpoints
(/robots.txt, /.well-known/host-meta, /web/<actor>), not a generic
homepage. The Playwright spec encodes this contract (5xx is the
only unhealthy signal), but meta/server.yml never declared a
status_codes override. The sys-ctl-hlth-webserver systemd probe
therefore expected DEFAULT_OK = [200, 302, 301] from
web_health_expectations.py and consistently saw 404 — the unit
went into failed state, sys-service-terminator retried 10× over
~10 min, and the deploy aborted with "Final-run service failed:
hlth-webserver.7.0.0.infinito.nexus.service".
The unrelated openresty "[crit] connect() to unix:/var/run/docker.sock
failed (13: Permission denied)" warnings come from the Prometheus
Lua background poller in roles/web-app-prometheus/templates/nginx/
prometheus.conf.j2 and are pre-existing log noise — they neither
caused this failure nor were introduced by 55c1bceeb.
Follows the existing override pattern in roles/web-svc-html/meta/server.yml
and roles/web-app-listmonk/meta/server.yml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YAML folds the line break inside the double-quoted scalar into a
single space, so the rendered command is byte-identical to before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both tasks invoke register_new_matrix_user via compose exec, register
the result, and gate changed_when/failed_when on rc, stdout, and
stderr. With ASYNC_ENABLED=true the project sets ASYNC_POLL=0
(group_vars/all/00_general.yml:60), so Ansible runs the task as
fire-and-forget and returns {ansible_job_id, started, finished: false}
without rc — changed_when then crashes with "object of type 'dict'
has no attribute 'rc'" and the deploy aborts (rc=2).
These are short-lived status-check execs (~2 s each); async makes no
sense here. Aligns the matrix admin/bot creation with the existing
convention in roles/web-app-mastodon/tasks/06_administrator.yml,
where the create-account task runs sync (uses rc) and only the
follow-up approve task uses async/poll.
Fixes the variant-0/sync deploy failure in test-deploy-server for
web-app-matrix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In variant-1 (services.oidc.{enabled,shared}=false) the dashboard role
still deployed oidc.js unconditionally via the hardcoded
DASHBOARD_JS_FILES list. The runtime loader then ran
keycloak.init({onLoad: "check-sso"}), which opens a silent-SSO iframe
at auth.infinito.example. With no Keycloak client registered for the
dashboard in this variant, Keycloak returned its error page directly
(no 302 to silentCheckSsoRedirectUri), and that page carries the
realm-level CSP "frame-ancestors 'self'" hardcoded in
roles/web-app-keycloak/templates/import/realm.json.j2:1457. Chrome
blocked the iframe, produced a console error, and
test-no-console-noise.js failed.
The dashboard's menu template (header.yml.j2:22) already gates the
Login/Account items on DASHBOARD_OIDC_ENABLED. The JS file list now
matches that gate, so oidc.js is no longer rendered, shipped, or
loaded when OIDC is off. iframe.js stays unconditional — it is a
pure local postMessage listener with no network or CSP surface.
Fixes the variant-1/sync failures in: web-app-dashboard, web-app-fediwall,
web-app-moodle and the five test-development matrix jobs (all distros).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>