A tor-only CI row disables web-svc-coturn, which no onion can serve, but the
disabler switched off only service entries keyed coturn. Nextcloud's talk
entry is gated on 'web-svc-coturn' in group_names and variant 0 pins it true,
so Talk stayed on without a TURN server and 01_fullstack.yml's assert stopped
every Nextcloud#0 tor row.
When disable resolves to a provider role P, every entry whose base enabled is
exactly the presence test of P is written false as well, and the reconcile
releases those entries once P is no longer disabled. Negated, compound and
variable gates are left alone. Across all 755 planner row states this changes
one value an app sees: talk on Nextcloud#0 tor rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_context.sh runs under set -euo pipefail and tested membership with
`printf '%s\n' "${_names}" | grep -qxF <name>`. grep -q exits on the first
match; when printf still has lines to write it dies of SIGPIPE (141), and
pipefail turns the matched test into false. resolve_db_service then fell
through to the central <dep>_<dep> name or printed nothing, and
has_swarm_service reported a deployed service as missing, so
skip_if_no_swarm_service let the converge and chaos gates exit 0 untested.
The race grows with the stack's service count and with CPU load (store-i18n
gate pass 11: 'mariadb_mariadb' != 'magento_database').
The three tests read the listing from a here-string, which is no pipeline.
Two regression tests put the match first in a 10000-name listing and fail
on every run against the old helper. Root cause found by i18n 2.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruff S701 flagged the jinja2 Environment without autoescape in the single-boot rails test; select_autoescape(default_for_string=False) keeps the rendered Ruby stdin unescaped, the repository's pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DEV_PYTHON_REQUIRES is shorter than the old name, so ruff format joins the
dict comprehension in test_install.py onto one line, and the CI format
check would report the drift.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_no_raw_project_walk flagged tree.rglob in files_in; the walk scans the test's own temporary directory, the exception the lint documents, so it carries the project-walk nocheck instead of the cached project helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three separate 'rails runner -' calls each paid a full Rails boot (47 s, 42 s and 39 s on the echoserver production deploy). platform_admin.yml now runs one runner whose stdin is a small step driver (run_steps.rb) followed by the unchanged root-password, platform-admin and appearance scripts, each wrapped in its own InfinitoSteps.run block: every step keeps its own [CHANGED]/[FAIL] verdict prefixed with its name, exit(0) ends only that step, and the run fails naming every failed step. Rails 7.2.3.1 evals runner stdin at TOPLEVEL_BINDING inside an executor that passes SystemExit through; puts writes to the swapped $stdout while the logger writes to files. The design render and slurp move to tasks/utils/design.yml ahead of it, keeping the numbered includes ascending, and the file layout leaves feature/iam-profile-rbac's hunks on platform_admin.yml and 00_core.yml mergeable without conflicts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruff S701 flagged the jinja2 Environment without autoescape in the auth_oidc sync test; select_autoescape(default_for_string=False) keeps the from_string shell body unescaped, as other tests do, and autoformat wrapped one long filecmp call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shorter variable name from 0bdb14cc1d (#1358) lets the dict comprehension fit on one line; make autoformat in gate pass 6 collapsed it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entrypoint refills the shared code volume only when the release changes, so a volume bootstrapped at 5.3.0 by an image without auth_oidc never received the plugin from a later same-release image; galaxyserver's production deploy failed at the presence check although the image ships it (probe: /opt/moodle-source/public/auth/oidc exists). 04_oidc.yml now asserts the image ships the plugin, naming the image when it does not, and syncs it into the volume as the runtime user through a staging directory, only when diff -rq finds a difference, before the existing presence check. The OIDC upgrade.php run no longer hides a failed plugin install behind failed_when: false, matching the MCP and objectfs plugin tasks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Development worktrees carry empty .claude/.cc-writes directories in role source trees; the one-call unchanged check gave up on any directory without files, so the docs tooling copy fell back to per-file stat and copy round trips (about 56 s on the echoserver production deploy). file_checksums now also reports directories (isdir, mode), and the check asks for every leaf directory in the same remote call: it settles only when each leaf exists as a real directory and, when directory_mode is set, carries exactly that mode. With directory_mode unset the stock copy would not touch an existing directory either (file module with mode None). A missing directory, a mode mismatch, a file or symlink in its place, or a symbolic directory_mode with leaves still runs the regular copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime image pins lived in meta/distros.yml, which neither the docker
updater, the image mirror nor the vulnerability scan reads, so a pin would
only ever move by hand. The new role sys-os carries them in its
meta/services.yml under the distro id; utils/distros.py renders the
workspace matrix from there with the stdlib block reader, and
meta/distros.yml keeps the distro attributes only.
A new release now arrives as an updater pull request. follow: latest holds
debian, ubuntu and fedora to the release their latest tag points at, by
Docker Hub digest: Ubuntu moves between LTS releases only (26.10 shares the
digest of rolling, not latest) and Fedora skips rawhide (46 shares the
digest of rawhide, latest is 45). Arch stays rolling; CentOS stream tags
carry no number the updater can order.
dockerhub_repo and is_dockerhub move to utils/docker/image/ref.py beside
split_registry_and_name, so utils/update/follow.py can use them without an
import cycle and utils/update/docker.py stays under the size cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1347 passed the supported Python range to the remote install.sh as
INFINITO_PYTHON_REQUIRES. Every INFINITO_* name is a key of the generated
.env, so test_every_referenced_infinito_key_is_registered fails on main
for this one, which no registry entry can carry: its value is derived
from pyproject.toml's requires-python, the single source, and a default
in default.env would be a second copy of the range. The task environment
and the script now call it DEV_PYTHON_REQUIRES; behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every jmap_call.yml call pays about 1.5 s of fixed cost (container exec, curl, include, parse and assert tasks) before Stalwart does any work; the echoserver production deploy spent 98 s in 33 calls. Independent reads now share one request: Security, AllowedIp and Http in security.yml, Domain and MailingList in 15_aliases.yml, Application and OAuthClient in 02_webadmin.yml, Directory and Role in 06_oidc.yml. The rendered method calls and arguments are identical to the old requests, no JMAP call runs between a batched read and the facts it feeds, and the User-role assert still follows the directory writes. Writes stay in their own requests, because JMAP keeps processing later calls after a refused set. A refused read is now named by its object instead of c0. Projected saving: about 19 s per echoserver deploy, about 10 s on galaxyserver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Source trees in a development worktree carry __pycache__ next to their sources (web-app-docs python/: 76 .pyc beside 42 .py). They defeated the one-call unchanged check and were copied file by file over SSH; on the echoserver production deploy the docs tooling staging took 155.9 s at about 1.27 s per file. The plugin now walks the tree without __pycache__ for both the unchanged check and the stock copy, which reads _walk_dirs as a module global; the swap is restored in finally and each task runs in its own forked worker. Sourceless .pyc outside a cache directory stay in the copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Production hosts carry objects from before the compose project was named after the entity: galaxyserver's mariadb, openldap and postgres networks have no project label, and echoserver's openldap_data volume is labelled svc-db-openldap, the role id older deploys passed as -p. The pre-flight stopped both deploys although compose reuses such networks and volumes with a warning. A network named after its entity now passes without a label or with a declaring role id, still checked against its subnet; a volume passes with a declaring role id as well. A container does not, because compose cannot see a container of another project and fails on its name. A label naming another project still stops the deploy, and the volume message now says compose would hand the role another project's data instead of claiming it refuses the volume.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ingress guard read ran two remote shells per swarm deploy, each asking container info again (WordPress#0 run 41303726270: 66 s + 61 s over 64 calls each). One shell now prints 'port <json>' and 'node <address>' lines into swarm_ingress_read; the new swarm_ingress_sections filter splits them by tag and raises on any untagged line, and SWARM_INGRESS_GUARDED and SWARM_INGRESS_PEERS read their section, so the guarded set, the peers, the firewall digest and the ruleset stay the same. A test ties the --format tags in read.yml to the sections the filter knows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render_override gathered the metadata of every image (docker image inspect, a pull when missing, a docker run probe) before it checked wrap, and the swarm handler's --no-wrapper path threw the result away, because image_meta is read only behind 'if not wrap: continue'. The gather now runs only when wrap is true, so the swarm override is unchanged byte for byte. It pulled again the externals that registry_sync had just removed (WordPress#0 run 41303726270: 275 s over 64 calls).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pre-flight stopped every deploy on a filesystem above SIZE_PERCENT_MAXIMUM_DISK_USAGE, which blocked a 1.8 TB workstation at 99 % with 30 GB free while a 145 GB runner was fine. A filesystem now fails only when it is above the percentage AND offers less than SIZE_GB_MINIMUM_DISK_FREE (20 GB): disk_usage_errors reads df's available column, the preflight_disk_usage filter passes the floor, and disk.yml runs df -P -k so the block unit stays KiB even under POSIXLY_CORRECT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76adaeb9e8 wrote the regex_search group reference as '\1' inside the folded
task scalar. Jinja unescapes string literals, so the filter received \x01,
rejected it as an unknown argument and failed the dev-python task on every
remote host. The literal is now '\\1', and the unit test renders the task's
environment expression the way Ansible does instead of only checking that
the variable is named.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bf9c5be173 capped the bootstrap at 3.14 with constants inside install.sh, a
second copy of the range pyproject.toml already declares. requires-python is
now the single source (>=3.11,<3.15): install.sh derives its floor and cap
from it, in the repository or, on a remote host where the script runs alone,
from INFINITO_PYTHON_REQUIRES that the dev-python role reads from the same
file; any other form fails loudly. The Dockerfile copies pyproject.toml
before the bootstrap, and pip now refuses 3.15 for the package as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fedora 45 defaults to Python 3.15. ansible-core 2.21, which the unpinned
`ansible` extra still resolves to (ansible 14.5.0 pins ~=2.21.5), calls
dataclasses._is_type, which 3.15 removed, so the Workspace (fedora:latest)
job died before the first playbook (run 41491308729).
install.sh now picks and installs the highest Python between
PYTHON_MIN_MINOR and PYTHON_MAX_MINOR (3.11 to 3.14) on apt, rpm and
pick_python_bin, and fails loudly when only newer interpreters exist. On
Fedora 45 it installs python3.14. Raise PYTHON_MAX_MINOR once ansible ships
ansible-core 2.22.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved by combining both sides: utils/tests/swarm/run.py keeps store-i18n's re-export of run_watched from watchdog.py, and the #1330 storage roots (lowest_free_mb over / and /var/lib/containerd, the fullest root in the abort banner and the sample line, df over every root) move into watchdog.py; test_run_storage_roots.py now targets watchdog and expects the process-group kill. The seaweedfs 4.52 bump lands in roles/web-svc-seaweedfs, where store-i18n moved the engine; roles/web-app-seaweedfs keeps only the console.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The constructor now runs tasks/utils/constructor/preflight.yml before the package install and fails with a named remedy when: the running kernel has no module index on disk or the clock is not synchronised (chrony leap status, else timedatectl, outside containers); / or the docker root is fuller than SIZE_PERCENT_MAXIMUM_DISK_USAGE (90); a docker network contradicts the entity that declares it (name, compose project label, subnet outside its block, overlap with another declared subnet); a declared container lacks or a declared volume carries a foreign entity label; a docker default address pool overlaps the mesh, Tor or a declared subnet; a host process outside docker holds a public port of the deploy; or, in assert mode, a clearnet domain of the deploy does not resolve.
A volume without any label passes, since compose adopts it and installs before this change created the Synapse volume with docker run. web-app-discourse declares its launcher-started containers through networks.local.launcher, and web-app-matrix labels a new Synapse volume with its compose project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
swarmkit sleeps update_config.delay after the last slot too and then monitors for delay + 1s, so a one-replica service paid about 21 s per rollout for nothing to stagger (database stacks converged in 32 s instead of 8 s in WordPress#0's update pass of run 41303726270, about 400 s per row). A service whose replicas line is 'replicas: 1' now rolls with delay 0s; several replicas keep service_update_delay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BBB's upstream compose template publishes nginx's 48081/48082/48087 on 127.0.0.1 and on 10.7.7.1. On Arch, Docker's nftables backend with the userland proxy off writes one identical 'MASQ TO OWN PORT' rule per binding and the second fails with 'rule exists' (moby 60109b06fb, PR #54031, first released in 29.10.0), so compose up failed in Moodle#3 of run 41303726270. compose_mods keeps only bindings on services.bigbluebutton.upstream_host or without a host address; nothing in the platform uses the loopback ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The builder handed every queued marker straight to its worker pool, whose own FIFO let a background latest build run ahead of the deployed:de build a visitor waited on (40-81 min in docs#1 of run 41303726270). A marker is now claimed only while a slot is free. build_language dequeues a translation that is already current instead of rebuilding it for 2-3 h on a duplicate request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the swarm NFS volume a docs replica keeps a failed lookup until the parent's cached attributes expire, so a site another replica just renamed into place stayed invisible to it and it served the 202 'Building' page (lang en) after a sibling had answered 200 (docs#1 of run 41303726270). recheck_servable() lists the parent to force close-to-open revalidation and is called only on the path that would answer 202; ordinary page requests pay nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 2g the vulnerability-detection feed update thrashed the manager cgroup until the kernel OOM-killed wazuh-modulesd; Wazuh restarts no daemon, so the API answered every call with HTTP 500 (error 1017) and the API administrator grant failed in the update pass of run 41303726270 (Wazuh#1, swarm). The manager gets mem_limit 4g, and api_administrators.py raises '<METHOD> <path> answered HTTP <code>: <body>' instead of a bare 500 so a dead daemon shows up by name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mesh is IPv4 only (60-byte header, IPv4 pools), yet wg.conf handed
wg the endpoint name, and wg dials the first getaddrinfo answer, an AAAA
on a dual-stack name. A host with broken IPv6 then never handshook
(alphaserver, 2026-11-01). Resolve every dialled endpoint with getent
ahostsv4 on the dialling host, render that literal, measure the path MTU
against it, and fail before rendering when a name has no IPv4 address.
Also set the tunnel MTU on a live interface, since wg syncconf drops it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deploy bypasses ERPNext's setup wizard and with it install_defaults, so a site without the store's GUI fixtures had no Standard Selling or Standard Buying price list and every Item Price insert failed with "Could not find Price List: Standard Selling". company.py now creates both after the Company, insert-if-missing, with the wizard's selling and buying flags and the Company's own currency; Price List.on_update fills the Selling and Buying Settings defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ansible's copy action runs one stat and one file module per file of a
directory source even when nothing changed, about 0.6 s each over SSH.
The echoserver deploy paid it on 31 branding renders and the docs
tooling tree (~770 s and 169.5 s, deploy -9). Before falling through to
the stock copy, hash the tree locally and compare it with one
file_checksums call on the target; when every file matches in type,
content and mode, return the stock copy's unchanged result. Any
difference, symlink, empty directory, owner or symbolic mode falls
through to the stock copy unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate counted any stamp above zero as handshaken. wg syncconf keeps
the old stamp while it points the endpoint at a dead path, so a deploy
passed the gate over a broken tunnel (alphaserver, 2026-11-01: the
endpoint resolved to an AAAA the host could not reach). Read the host's
epoch in the same shell call as latest-handshakes and accept a stamp
only within WireGuard's REJECT_AFTER_TIME of 180 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deploy bypasses ERPNext's setup wizard and with it install_fixtures, so a site without the store's GUI fixtures had no root Item Group and every consumer creating one failed with "Could not find Parent Item Group: All Item Groups". company.py now seeds All Item Groups, All Customer Groups and All Supplier Groups as root groups, insert-if-missing like All Territories.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ansible-playbook splits each -e value through parse_kv on whitespace, so json.dumps' default ', ' separator broke PRIMARY_APPS=["a", "b"] into a raw parameter and the list never reached the play. Dump extra vars with separators=(',', ':') in the development deploy and the swarm matrix deploy, and test that a list extra var survives parse_kv.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers that build on the ERPNext site from their own repositories (web-app-cst-store, web-app-cst-security-check) now declare the roles they need on erpnext-api-bot in services.erpnext.api_bot.extra_roles instead of granting them themselves. Every deploy grants them beside System Manager and the Server Scripts role, creating an empty Role first when the site does not hold it yet, so this role stays the only writer of the bot's roles and keys. Port of the api-bot part of 067 from feature/iam-profile-rbac (3e5acbd104).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The small GitHub runner shape (AWS, 72G root) ran out of disk in swarm and compose rows (#1329: WordPress#2 of run 41261531965, Nextcloud#6 and #7 of run 41262286962) while an empty instance-store disk nvme1n1 sat unused. mount_spare_disk.sh formats and mounts it at /mnt, but only on a GitHub-hosted runner outside act, and only a whole unpartitioned disk without any signature, mount or swap use of at least 75G; every decline leaves a notice. relocate_docker_dataroot.sh then bind-mounts /var/lib/containerd from the spare disk and moves the Docker data-root to /mnt/docker-host (not /mnt/docker, which is the nested daemon's volume that dedicated.sh wipes). It refuses before stopping anything when images, volumes, build cache, foreign containerd namespaces or shims exist or daemon.json is unreadable, and rolls back the bind mount and daemon.json on any later failure or cancel. Swap goes on / once containerd lives on /mnt, dump_state.sh logs disk size, model and free space per storage root, and the swarm watchdog samples / and /var/lib/containerd every 10 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under SSO the OIDC directory is the authentication directory, and Stalwart refuses any password credential for its accounts ("Cannot set credentials for accounts in an external directory"). Every create carried one, so an account added after SSO was switched on never came into existence: the echoserver deploy failed on the keycloakbootstrap bot right after the 0.16 to 1.x migration, and a new person would have been skipped silently. Under SSO the create now carries no credentials and the password update is skipped; existing accounts come back as primaryKeyViolation with their id, as without SSO. Any remaining refusal fails the deploy. Closes the accounts entry of the role's TODO.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>