Twelve jobs of run 37865281419 failed on the docs language spec:
tests/test-languages.js:25:7 the deployed working tree is published in de
with a language switcher (54.6m)
Error: Expected the de site of the deployed working tree to be built
The spec polls /deployed/de/ until the suite budget runs out, so a build that
hangs is reported as a spec timeout with nothing to read. None of the five
subprocess calls the build makes carried a timeout, so any of them could hold
the queue open forever:
library.py _git() rev-parse, tag --list and remote update --prune
library.py fetch() git clone --mirror
i18n.py each generator of generate_commands
i18n.py sphinx -b gettext
builder.py _run() the sphinx build of one version
The two git paths reach the network, which is where an unbounded call hangs
without failing.
`_run` streams its child's output into the build state, so it cannot take
`communicate(timeout=)` without losing the progress that `_append_log` and
`_save_state` record, and Popen has no timeout of its own. A threading.Timer
kills the child instead. The kill lands in the existing returncode branch, so
the build is reported through `_failed`, which already writes the log tail into
the state file.
RUN_TIMEOUT_SECONDS is set to catch a hang, not to beat the spec's clock: no
measurement of a successful de build exists, so a tighter value risks turning a
slow build into a failing one. With one, the value belongs below the poll
window the spec scales to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`disable=lam` aborted the local deploy of web-app-openbao with "`disable`
conflicts with the current inventory state", and web-app-moodle carries the same
construction in two of its variants.
The writer and the conflict check disagreed on scope. apply_services_disabled
walked `find_roles_with_service`, the roles whose meta/services.yml declares the
key, so neither role was in that set for `lam` and nothing was written.
find_services_disabled_conflicts walks every deployed app's host_vars instead,
found the `lam` block the variant had baked, and refused the run. Dropping
`lam` from the list is not an answer either: the round then deploys web-app-lam
although ldap is off.
The writer now also flips a key it finds in host_vars under an app whose role
does not declare it, and only the flags that are already in that block. It never
creates a key or a flag, which is what `_declared_toggles` exists to prevent:
compare_application_keys rejects a host_vars key the role contract does not
carry. Overwriting a value the variant already wrote introduces nothing, so that
rule is untouched; test_skips_app_whose_role_does_not_define_service still
passes and pins it.
The alternative the TODO proposed, declaring `lam` in meta/services.yml with the
`'web-app-lam' in group_names` flags, is the worse trade: a consumer entry is
resolved through the service registry, so it would bind web-app-lam and its
whole closure into every round that carries openbao or moodle, for a key both
roles read only to gate a Playwright spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A role that exports an outbound proxy for one purpose applies it to every
HTTP call the app makes, so an internal dependency reached by name is proxied
too. web-app-pixelfed exported the Tor SOCKS proxy container-wide for its
onion OIDC issuer and its S3 client then failed with
cURL error 97: cannot complete SOCKS5 connection to seaweedfs
for six minutes per attempt, until sixty retries had consumed the whole
suite budget and the job was killed. web-app-espocrm ships a byte-identical
proxy block and configures its S3 endpoint from the same lookup.
web-app-nextcloud sets the proxy globally in proxy.config.php and listed two
internal hosts in proxyexclude, neither of them the object store it reaches
by hostname.
9d91fbea83 had tried to retire the per-app proxies in favour of onion routing
over DNS, and 20ddda8c1c restored them for the OIDC issuer, so a broad proxy
is the current design and the exemption list is load-bearing.
lookup('proxy_excluded_hosts', application_id) is now that list: the loopback
forms plus the object store host when the role stores objects. It is a lookup
and not a group_vars value because no group_vars file resolves
application_id, so a global would have depended on which role happened to be
running. Nextcloud keeps its hand-written list of public hosts; only the
internal half is derived.
The lint gained a second rule for the class rather than the symptom: the
nextcloud list named no loopback literal at all, so the literal rule could
never have fired on it. A variable whose name marks it a proxy exclusion list
must render the lookup, unless it only composes other exclusion variables, and
a ..._PUBLIC_... list is exempt because it names no internal service.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both provisioners follow nextCursor for workflows and step skip for API
keys until a short page arrives. A cursor n8n repeats, or a key listing
that ignores skip, would keep that loop running forever and hang the
deploy instead of failing it.
workflows_named now remembers every cursor it followed and exits with
"FAILED listing workflows: nextCursor repeats" when one comes back.
api_keys_labelled exits when a non-empty page brings no key id it has not
already seen. Normal paging is unchanged.
Four regression tests drive both scripts against a fake API that repeats
its cursor or ignores skip; without the guards exactly those four fail.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tests/lint/repository/structure/test_python_file_size.py caps a Python file at 500 lines and asks for a split rather than a raised limit, and engine.py reached 522 once the network claim took a lock. engine.py keeps the engine client, the labels, EngineError and the compose backend at 273 lines; swarm.py takes the swarm backend at 258.
The methods moved verbatim, so nothing changes behaviour. swarm.py imports the labels and the error from engine while engine imports nothing back, which is what the first attempt got wrong: a Pool mixin, following the build_queue.py precedent, would have made the two modules import each other. server.py splits its import, and agents.py reads only labels so it is untouched.
The pool tests follow the code into test_swarm.py, the mirrored path test_mirrored_unit_tests.py asks for, and test_engine.py keeps the transport, connect and compose cases.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
c4ea94c778 read the 404 from POST /networks/<id>/connect as the swarm allocator not having realized the overlay yet, and retried for realize_timeout. 03ed2f1ca1 then recorded the real constraint: a swarm service task cannot be attached to a network it was not created with, so the retry could only burn 131 to 133 seconds per attempt and backend.create was never reached (run 37252453141, job 111587113070).
ensure_pool therefore creates MAX_RUNNING overlays up front and adds the missing ones to its own service spec, which costs one reschedule of this task per deploy and none afterwards. network_for then hands an agent the lowest overlay nobody holds, and gives back the one it holds already; _scale writes that network in the same update that lifts the replica count, so claiming a slot adds no further restart. A pool network carries the broker and at most one agent, so the isolation the README promises holds.
The claim is taken under a lock and remembered until the agent stops, and a service counts as holding its networks from the moment the engine records its desired replica count rather than from the moment a task reports running. Without both, two owners starting their first agent at once are handed the same network and reach each other: four concurrent claims against the unguarded form all returned the first slot. Because the claim and the agent keys live in one process, ensure_pool refuses to start when the broker itself is scaled past one replica.
Compose keeps attaching at runtime, because a bridge network is local and realized at create. connect stays for that path, with its docstring corrected to name which mode calls it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7916c0177e wrote the cap as {{ IAM_USERNAME_MAX_LENGTH }} inside the {% set _base_attrs %} block. A set body is already a Jinja expression, so the nested delimiters are a syntax error rather than an interpolation, and jinja2 answers TemplateSyntaxError: expected token :, got }. realm.json.j2 includes the component unconditionally, so every realm render raised, not just the username path: that single typo is the whole of the 9 unit and 7 integration failures the gate reported.
The test context gains the value because render() builds its environment with StrictUndefined, which raises on an absent name instead of rendering empty, and a new case asserts the rendered validations.length.max equals it so the cap cannot fall back to the Keycloak default of 255 unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`make quality-high` rewrote these files on every branch: the SuiteCRM
README and the root role table lacked the Redis service #881 added, the
session path test was not formatted, and an OpenTalk secret description
from #873 was not wrapped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
n8n 2 returns ten API keys and a hundred workflows per page by default. A
stale managed key behind the first page survived, and a managed workflow
behind it was created a second time. Both scripts now follow every page,
narrowed by label or name, and keep the exact match in the script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
n8n 2 pages GET /rest/api-keys as {data: {items, counts, totals, owners}},
ten keys at a time, and matches `label` as a substring, so walking the body
raised "'str' object has no attribute 'get'". It also enforces API key
scopes, so activating the MCP server workflow needs `workflow:activate`.
Both scripts now read every list and create response through one
normalisation that unwraps the page, look the managed key up by label, and
name an unexpected shape by its keys only. The normalisation is taken over
from a392e2dd49 on feature/store-i18n.
Co-Authored-By: Claude Opus 5.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>
ruff format rewraps the read-back line in test_repository.py, and ruff
check reports the nested with in test_fetch.py as SIM117. Both fail
lint-python.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An http_regex source read one document, so a tag whose parts upstream
keeps in two files could only carry the second part as a literal. Every
pin also fetched its document on its own: a failed request left one pin
of a release behind while the others moved.
url now takes a list whose bodies are searched as one text, and every
document is fetched once per run, so all pins that read it resolve against
the same snapshot. The HTTP reads move into utils/update/fetch.py.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The repository updater assigns ref: lines to entities by position: every
entity that pins a value takes the next line carrying that value. A ref
that declares its own source was dropped before it took its line, so a
second entity pinning the same value was given the sourced entity's line,
and an update of it would have rewritten the wrong ref.
A sourced ref now takes its line first and is skipped afterwards, the
same order the nocheck marker already follows.
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 or, for a ref, by the repository updater. Those raise
it to the newest tag of its registry or repository, which for a component
of a release is not the release's value. A tag built from several upstream
values, such as <freeswitch>-<bigbluebutton>, could not be resolved at
all: the resolver takes one capture and keeps a pin on its suffix.
The image updater now skips a version and the repository updater a ref
that declares a source for itself. An http_regex source accepts a template
over the pattern's named groups, and a templated pin moves whenever the
assembled value changes, its suffix included.
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>