Autoformat output on the test PR #881 added: the redis_session_path call
exceeded the line width, so its arguments move onto their own line. The
token sequence is unchanged, so the assertion still proves what it did
before, that a password carrying &, =, ?, # and a space survives the
connection URL intact instead of splitting it into extra parameters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With three swarm replicas the PHP session files on the shared NFS volume
were read as empty under concurrent requests of one browser, so SuiteCRM
answered 403 on /api/graphql and dropped the session. Sessions now live in
Redis with session locking that waits like flock did; the phpredis version
is pinned in services.yml with its own update source, and the engine lookup
renders the save path.
Refs #879
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pin that names its own upstream in an update: block was still collected
by the image updater, which raises it to the newest tag of its registry.
For a component of a release bundle that tag is not the bundle's, so the
two updaters would propose different versions for the same line and such
a pin had to carry nocheck: docker-version.
The image updater now skips a version that declares a source for itself.
The source resolver alone bumps it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`native_metrics_target` was a Jinja filter, so it could not read
DEPLOYMENT_MODE: filters see no Ansible variables. It returned
`<services.<key>.name>:<port>` unconditionally, which is `matrix-synapse:9000`
for matrix. Compose serves that name; swarm does not, because `docker stack
deploy` ignores `container_name` and the alias never exists. This repository
says as much itself, through `lookup('compose_only', 'container_name', ...)`.
The target was therefore scraped and refused, and `playwright.spec.js:274`
reported `web-app-matrix: 1 matching target(s) reporting up != 1`, meaning one
matching target whose `up` value is not 1.
It becomes a lookup that delegates host resolution to `scrape_target`, which
was already mode-aware, so exactly one plugin knows the difference between the
two modes. The port has to be handed over explicitly: matrix exposes metrics on
9000 while `services.synapse.ports.internal.http` is 8008, so the existing
internal-port lookup cannot serve this case on its own.
`scrape_target` now refuses `port` together with an explicit `port_kind`.
Before, `port` won silently and the error text still named the
`ports.internal.<port_kind>` path that the call had never consulted.
`native_metrics_apps` drops an app that pins itself to compose through
`compose_mode_force` while the cluster runs swarm, because the scrape target
would then name the wrong object. Only the literal case: matrix declares that
override as a Jinja expression over its flavour, which this lookup cannot
resolve from the scraping role's variable scope, so variant 0 on swarm stays a
latent mismatch.
The two comment lines claiming `container_name:port` went with the change;
after it they state the opposite of what the code does.
Verified by mutation: with the swarm branch of `scrape_target` disabled, the
new test emits `matrix-synapse:9000`, the exact string CI reported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
default.env declares INFINITO_DOMAIN and every Python test is expected to
reach it through default_domain_primary(). e2beab9f1c converted nine sibling
tests to that shape and widened the hardcoded-primary-domain lint to the test
trees; this file was written after that pass and kept its own copy of the
value, which the widened lint will reject.
The literal and the SPOT agree today, so only the source of the value moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every INFINITO_* read from a .py or .sh file must be declared in default.env
and applied by a handler under utils/env/handlers/, so the registry lint
rejected the fixture this test exports to prove the templar rendered a raw
DOMAIN_PRIMARY. Registering it would be wrong twice over: it is not a project
default, and the companion lint would then demand a handler for it.
The template string, the export and its cleanup move together, because the
lookup resolves the name from the environment and a mismatch would render
empty and assert against ".".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lookup read DOMAIN_PRIMARY straight from the templar's available
variables, which hold it unrendered. The generated inventory defines it as
"{{ lookup('env', 'INFINITO_DOMAIN') }}", so whitelist_domains was written as
the literal ".{{ lookup(...) }}" and matched no host at all: oauth2-proxy
answered every absolute redirect with "domain / port not in whitelist", which
left pihole's sign_out unable to reach the Keycloak end-session endpoint.
utils/cache/domains.py already renders the raw value through
render_ansible_strict; do the same here, guarded on the value carrying a Jinja
marker so a literal domain is untouched. render_ansible_strict also refuses
output that still holds "{{", so an unresolvable primary domain now fails loud
instead of becoming a junk scope.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A generated lockup drew its title in the primary color with an
on-primary outline. On a header or sidebar that a role maps onto the
frame token, that put primary-colored text on a primary-toned ground.
A slot now takes `frame: true` in the role's design entry. The asset
action receives a second color pair from the palette, the on-frame
color as fill and the frame color as outline, and the renderer picks
the pair per slot. Slots without the flag render as before.
Keycloak calls the asset action directly and already rendered its
brand slot in the on-frame color, so it passes that color for both
pairs and keeps its output.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The entrypoint removed the old tree before it copied the new one. A
copy that failed left the volume without a working Moodle, and every
restart removed again instead of recovering. It now copies into a
staging directory inside the volume and swaps the trees once the copy
is complete, so a failed copy leaves the old tree in place.
An image that ships an older release than the volume records no longer
replaces it and exits with a message instead. Two instances on
different releases would otherwise overwrite each other's tree.
upgrade.php reports "No upgrade needed" on stderr, so the task counted
every run on a current site as a change. It reads stderr now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MCP Adapter 0.7.0 for WordPress refuses a request that follows
initialize without the MCP-Protocol-Version header: 'Invalid Request:
MCP-Protocol-Version header is required for a 2025-06-18 session'. The
contract probe never sent it, so the CLI test of web-app-wordpress
failed at tools/list.
The probe now keeps the version the server answers initialize with and
sends it on every later request of the streamable HTTP transport, which
the specification asks of a client since 2025-06-18.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
58 commits, five conflicts. Four of them are the same disagreement: main
stops deriving memory parameters from the host's total RAM and takes them
from the container's own limit through the service_memory_mb lookup, while
this branch had routed the same values through its resource lookup. Main's
direction wins each time, because the new
tests/lint/ansible/services/test_memory_parameters_use_lookup.py requires it
and because a value read from the host drifts from the limit its service
declares.
group_vars/all/17_resource.yml keeps this branch's lookup('resource', ...)
form for the parameters that stay, and loses POSTGRES_SHARED_BUFFERS,
POSTGRES_WORK_MEM and POSTGRES_MAINTENANCE_WORK_MEM, which svc-db-postgres
now derives per container in its own vars. Main's side of the hunk also
redefined RESOURCE_CPUS, RESOURCE_MEM_RESERVATION, RESOURCE_MEM_LIMIT and
RESOURCE_PIDS_LIMIT, which already stand above the conflict as lookups;
taking it whole would have duplicated four keys.
web-app-nextcloud drops the two php values it derived from host_mem_mb.
templates/env.j2 already sets PHP_MEMORY_LIMIT and
PHP_OPCACHE_MEMORY_CONSUMPTION from the container limit.
web-app-wordpress is adjacency only: both sides added one line in the same
place, WORDPRESS_MULTISITE_ENABLED here and WORDPRESS_PHP_MEMORY_LIMIT on
main. Both stay.
web-app-zammad takes the updater's elasticsearch bump to 9.5.5, which
svc-db-elasticsearch already carries, and resolves its compose template the
way 774ff8c2d2 did: the search keys live in templates/env.j2, because an
inline environment: block is what compose-environment-from-env-file rejects.
Main's new path.data joins them there.
Six keys of plugins/lookup/resource.py lose their last caller with the
postgres parameters above and are now unreferenced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pin moved from 4.5.11 to 5.3.0, and the role fitted neither a new
install nor an existing one.
Moodle 5 serves from public/ and routes every other path through
r.php. nginx now uses public/ as its root, hands unknown paths to the
router and passes PATH_INFO for scripts with slash arguments, and
config.php declares the router as configured. The paths the role checks
and writes (version.php, auth, webservice, the OIDC plugin) moved below
public/, the URL of the OIDC plugin stays auth/oidc. The source archive
no longer contains vendor/, so the image installs the Composer
dependencies it declares. The OIDC plugin is pinned to v5.2.1, the
newest release.
An existing site kept its old code: the entrypoint filled the code
volume once and never again. It now records the release it copied and
replaces the tree when the image carries another one, and a new task
runs admin/cli/upgrade.php. Plugins that were installed through the web
interface are not carried over, the README says how to ship them.
A site installed with 5.3 forces login by default, which 4.5 did not,
so its start page answers 303 to the login page and the webserver
health check refused it. The role declares 200 and 303 as the codes
that check accepts: 200 is what an upgraded site keeps answering.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Redis sidecar whose consumer declares no services.redis.mem_limit
runs in a container that inherits the consumer's primary entity limit.
Since the lookup replaced the old filter, maxmemory followed that limit
and rose from 204 MiB to between 819 and 3276 MiB in 17 roles.
lookup('service_memory_mb', ..., provider=<role>) caps the result at
the same share of the provider's own limit when the application does
not declare one for that service. A declared limit is not capped, and a
container smaller than the provider keeps its own share. The Redis
sidecar passes provider='svc-db-redis': those 17 roles get 409 MiB, the
seven with a 256 MiB container stay at 204.
The lookup now rejects unknown options and a provider that is no role
or declares no limit for the service, so a misspelled option fails
instead of silently lifting the cap.
The memcached sidecar's literal -m 64 takes the same lookup and keeps
its value.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three conflicts, all from main sizing memory parameters off the container
limit while this branch moved container environments into one file per role.
plugins/filter/memory_filters.py and its test are deleted on main, replaced
by the service_memory_mb lookup. This branch only renamed the filter out of
its get_ prefix and fixed the docstrings' reStructuredText, so nothing of
substance is lost; the deletion is taken. Verified first that no consumer
survives: jvm_max_mb, jvm_min_mb and redis_maxmemory_mb appear nowhere in
roles/, group_vars/, tests/, plugins/, cli/ or utils/ once the merged tree
is in place.
web-app-zammad's search service is the only real conflict. 37e80b957e moved
discovery.type, xpack.security.enabled and ES_JAVA_OPTS from the compose
template into templates/env.j2, where an inline `environment:` block is what
the compose-environment-from-env-file lint rejects; a7db89ac0b changed the
heap from a literal 512m to half the container's limit. Both intents hold,
so the keys stay in env.j2 and the heap now reads ZAMMAD_SEARCH_HEAP_MB,
which the merged vars/main.yml already defines through the new lookup.
web-app-magento and web-app-shopware keep their inline environment blocks:
both carry an explicit nocheck naming the reason, that their single env.j2
is shared with containers the search daemon's keys must not reach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lookup('service_memory_mb', application_id, service_name, share) returns
a share of a service's mem_limit in whole MiB. It resolves the limit the
way sys-svc-container resolves the container's own: the named service,
then the role's primary entity, then RESOURCE_MEM_LIMIT.
It replaces the three filters of memory_filters, each with its own unit
parser and its own idea of the limit: Redis maxmemory at 0.8 of the
limit, and the Confluence and Jira heap at 0.7 and 0.35.
The central Redis (409 MiB), Confluence (5734/2867) and Jira (4300/2150)
keep their values. A Redis sidecar without a mem_limit of its own does
not: the filter fell back to a fixed 256m, which gave 204 MiB, while the
lookup takes the limit that container really runs with, the primary
entity's. That moves 25 roles to between 409 and 3276 MiB.
The heap clamps of the old filters are gone: the 1024 MiB floor, limit
minus 1024, the 12288 cap and Xms bounded by mem_reservation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two lint suppressions shipped with the multisite OIDC work. Both are replaced
by the thing they were standing in for.
settings.php claimed an exemption from the mirrored-test rule because every
call it makes belongs to the WordPress runtime. The six functions stub in a
few lines, so the claim does not hold. SettingsTest.php declares WP_CLI and
the globals over a state holder and includes the real payload per case:
* a single site is written without switching blogs
* every site of a network is written, switch_to_blog paired with restore
* a site whose write does not persist is named by blog id
* every site is still visited before that failure is reported
* a payload that is not a map halts, and so does an absent one
The fourth case is the one worth having: a `break` on the first failure would
pass the other three and silently stop writing.
Neither existing house pattern fits. `eval` suits the tag-less tinker snippet
of web-app-akaunting, `proc_open` the standalone script of web-app-joomla;
this is a tagged file that needs a runtime, so it is stubs plus `include`,
which also avoids the `nocheck: eval` the akaunting test carries.
The installer test's cache-read exemption rested on a cached read serving a
stale call log. Measured, that risk is confined to the suite and sits around
3e-13: the log lives in a fresh TemporaryDirectory per run, mkdtemp verifies
non-existence, and each path is read once. What complying actually costs is
roughly 25 dead entries in an 8192-slot LRU, so the read goes through
utils.cache.files.read_text.
Proven by `make test-unit-php`: 32 tests, 68 assertions. The first version of
this test passed nothing at all -- it sits one directory deeper than the
other PHP tests, so dirname(__DIR__, 6) resolved to tests/ and the include
found no file. Without the test that would have stayed invisible.
The suite drives stubs, so it proves the payload's control flow and not that
WordPress behaves as stubbed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Multisite subsite served `/wp-login.php` with no SSO entry point even once
the OIDC settings reached every site (e1901fb88e), because the plugin was
never running there:
wp plugin activate <name> --path=<docroot>
Without `--network` that activates the main site only, so a subsite had the
settings and no plugin. `--network` appears on no branch of this repository,
so the gap was everywhere.
`services.wordpress.multisite.enabled` now resolves once, in vars/main.yml,
and feeds the activation, the multisite conversion gate and playwright.env.j2,
which until now recomputed the same lookup inline. The conversion gate also
drops a `| default(false)` on a key meta/services.yml always declares.
The installer moves out of Ansible into files/shell/install_plugin.sh. Eight
tasks interlocking through register/when/until/failed_when read as one linear
script, and the contract arrives as environment assignments. The container
address stays an inline shell assignment rather than an `environment:` entry
because in swarm it is the literal text `"$(resolve-container-id ...)"`, which
only the shell evaluates; the body streams over stdin because swarm workers
carry no role checkout (03ed52eea5).
Moving the logic across languages would have dropped the WP-CLI 255 contract
to shellcheck coverage, since the suite has no shell tests. The unit suite
therefore now drives the real script against a stubbed `container` wrapper
instead of evaluating the task file's `when:` expressions, which exercises the
behaviour rather than the text describing it.
A mutation pass over that suite -- eight mutations, each required to turn its
own guard red -- found two defects before any deploy:
* `set -e` is suspended inside an `if` condition, so a probe folded into
`if needs_install; then` turned an unreadable version into "nothing to
do". The version read is now a standalone call that aborts with 67.
* a bare `return` propagates the status of the preceding test, so
`[ -n "$WP_PINNED_VERSION" ] || return` aborted the script whenever an
unpinned plugin was already present -- the path every redeploy takes.
The second hid behind a vacuously passing check, which is why the suite gained
exit-code assertions where it previously only counted calls.
This commit carries a fix and a refactor together. The `--network` fix lives
inside the extracted script's activate(), so separating them would mean
reverting the extraction, committing the flag, and reapplying it.
No deploy has run any of this, in compose or in swarm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflicts:
- roles/web-app-pihole/meta/services.yml: main dropped the unused redis
entry, this branch changed the javascript entry directly below it. The
redis entry is gone and the javascript entry keeps the flag bound to
the SSO flag of the role and its nocheck reasons.
- README.md: the generated role table keeps the renamed web-svc-design
role and lists Pi-hole without Redis and without the CDN.
The role diagram of Pi-hole is regenerated, because both sides removed
an edge and the index of the red link moved.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The guard required a register on every async kcadm job and nothing more.
A result that is registered but no longer handed to async_reap_jobs,
directly or through the list it is collected into, passed it, although
the reap then neither polls that job nor sees it fail.
The test now follows each registered result to an async_reap_jobs
expression, by variable name with word boundaries, so kc_declared_client
does not pass for kc_declared_client_jobs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stub answered every absolute name, so a guard that asked for the
backend twice and never for the exporter passed both tests. The stub now
records the names it is asked for, and the tests require the backend and
the exporter.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
COLLABORA_WOPI_HOST read the scheme and the port with
lookup('tls', 'web-app-nextcloud', ...) and the host with
lookup('domain', 'web-app-nextcloud'). The tls lookup aligns an app term
to its consumer: for a clearnet Collabora it answers for the clearnet
sibling of an onion-primary Nextcloud, while the domain lookup keeps the
onion name. Such a deployment renders https://<onion>:443, an origin
Nextcloud does not use, so the allowlist does not match its WOPI calls.
Nextcloud pins overwritehost and overwriteprotocol to its primary domain,
so the allowlist now asks the tls lookup for that domain instead of for
the app.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since the targets take their scheme from the domain they probe, two
switches of the http_ready module no longer fit. Both still followed the
TLS state of the Prometheus vhost.
fail_if_not_ssl refused an http target, an onion vhost for example, as
soon as Prometheus itself was served over TLS. It is false now. The
module does not follow redirects, so the scheme of the target URL already
decides the transport.
insecure_skip_verify switched certificate checks off whenever Prometheus
itself had no TLS, also for an https target. Checks now stay on, except in
a self-signed deployment whose CA is not mounted into the Prometheus
containers, where the exporter has nothing to verify against.
The per-role test of the spec parsed every answer of the query API as
JSON. An answer that is not OK now counts as "no probe yet", so the poll
keeps waiting instead of ending on a parse error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The check listed the torrcs without the limit through "grep -L ...; true".
With no torrc under /opt/chutney/net/nodes, or with one that cannot be
read, grep printed nothing, the trailing true turned its error into
success, and the test reported that every node runs one worker.
The check now counts the limit per file with "grep -cH" and reads the exit
status. A status above 1 or an empty answer fails the test, and a file
with a count of 0 is named as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The variant-3 bootstrap aborts on an upstream guard:
TASK [custom/matrix-bot-baibot : Fail if admin patterns list is empty]
You need to define a required configuration setting
(`matrix_bot_baibot_config_access_admin_patterns`)
which takes the whole playbook down and surfaces only as the generic
"matrix-mdad bootstrap failed inside the container". Nothing asks for that
setting directly: at the pinned MDAD ref 40179e6d4a it is derived from
matrix_admin, which matrix-base defaults to the empty string. The template
rendered every other baibot setting and never set the one they hang off.
matrix_admin could not simply be written out, because "@localpart:server" was
assembled by hand in nine places and the two flavors had already drifted apart
over it: the compose flavor built it from MATRIX_SERVER_NAME while the MDAD
template computed its own onion conditional off DOMAIN_PRIMARY, skipping the
services.matrix.server_name key the role declares. group_vars/all/14_about.yml
had drifted further still and used the raw clearnet name, so on an onion
deployment it published a contact handle that resolves nowhere.
The shape now lives in the mxid filter, which rejects an empty part or a
localpart that already carries a sigil and lowercases the result because
Synapse refuses anything else. The homeserver name lives in the
matrix_server_name lookup, which answers the Tor node when the role is
published under .onion and the declared name otherwise. mxid joins the two, so
every caller names only an account, needs no role variable in scope, and gets
an id that matches whichever network is published. MATRIX_SERVER_NAME now reads
that lookup rather than repeating the conditional, which leaves MATRIX_ONION_NODE
without a consumer, and it goes.
Rendering the replaced Jinja against the new resolver over eighteen input
combinations disagrees three times, each on input no inventory produces - a
whitespace-only Tor node, an uppercase .ONION domain, one with a trailing space -
and each time the new answer is the correct one.
Separately, Nextcloud#12 dies in the same role with
/bin/sh: 1: sudo: not found
because matrix-synapse-usage-exporter pins self_build: true upstream and its
clone task becomes, which the runner image could not do. sudo joins its package
list; the container is already privileged, so this grants nothing new.
Static throughout: the baibot bootstrap had reached ok=196 of roughly 540 tasks
and the usage-exporter self-build never got to its docker build, so both fixes
clear the task that failed and nothing behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>