compose_volumes merged extra_volumes first and then skipped every meta/volumes.yml entry whose key was already present, so the meta nfs value was lost with it. Nextcloud injects talk_recording_tmp through nextcloud_extra_volumes without an nfs key, and 5728502a19 marked that volume nfs: false. On swarm with the NFS backend the stack therefore bound it to /var/lib/infinito/nextcloud_talk_recording_tmp, while nfs_prep.yml renders the same lookup without extras, saw nfs: false and never created the subdir; every node rejected the task with 'no such file or directory'. The meta nfs value now carries over onto the injected or derived spec unless that spec sets nfs itself, so both renders agree and the scratch volume stays node-local.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frappe v16 limits the Server Script doctype to the Script Manager role and ServerScript.validate calls frappe.only_for('Script Manager'), so services.erpnext.server_scripts: true still left every integration on erpnext-api-bot with 403 Insufficient Permission for Server Script. The same switch now grants the bot Script Manager when true and revokes it when false; System Manager stays, the save runs only on a change, and an undeclared value fails before the site is touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's OpenProject seeder wait (#1302) compared DEPLOYMENT_MODE == 'swarm', which store-i18n's test_deployment_mode_booleans forbids; it now uses IS_SWARM_MODE. Main's test_webserver_worker_limits (#1297) rendered without host facts, so store-i18n's resource lookup refused to measure the controller; the fixture now carries processor_vcpus and memtotal_mb as every play does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLI test replays the published Production block in a throwaway target that shares the runner's disk. The replayed deploy knew only the default SIZE_PERCENT_CLEANUP_DISC_SPACE of 85 while the stack host runs with its own inventory value, so sys-ctl-hlth-disc-space failed in the target at 87% (Wazuh#1, CI run 40979252521). disc_space.sh writes the stack host's value into the block's provision --vars and fails on a non-integer value or a block without --vars; the published instructions stay unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 WordPress#1 (multisite, multi network): a request on a network sibling resolved to the clearnet site, because WordPress picks the site by Host before any mu-plugin runs and the network kept only clearnet base URLs per site. Operator decision: a real multisite per network with real child domains (option B).
infinito-network-origins.php is now also installed as wp-content/sunrise.php (SUNRISE=true), so the sibling host resolves to its own site before mu-plugins load; a guard constant stops the second load from mu-plugins. WORDPRESS_SITE_DOMAINS holds the canonical domains in the first domain's network, WORDPRESS_NETWORK_BASE_URLS carries every sibling of each site's base URL (sites ';', siblings ','), and INFINITO_NETWORK_BASE_URLS takes that list. Child sites are inserted by files/php/multisite/sites.php only where no site exists on the domain and path, replacing wp site list plus wp site create. The multisite variant's child domains move from server.domains to domains.canonical.
Covered by 29 Python tests and two PHPUnit suites; PHPUnit and a variant 1 deploy with an onion pass still have to run (store-i18n gate and deploy).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_run_streamed_outlives_a_deadline_while_it_keeps_talking patched RUN_SILENCE_SECONDS to 1.0 while the chatty child prints its first line only after the interpreter has started. run_streamed measures silence from process start, which is right for production (a command that never speaks must time out), so under xdist load on a full disk the cold start alone exceeded the window and the run returned 124 (reported from IAM's make quality).
The window is now 2.0 s against a 0.1 s cadence over 40 lines (4 s, still twice the window, so the test keeps proving it outlives a deadline while talking), and the child runs with -I -S to skip the site import. With a simulated 1.5 s cold start the old parameters return 124 and the new ones 0. pull.py is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an onion vhost the browser talks plain HTTP, so an upstream that marks its session cookie Secure (the app still believes in its canonical https origin) loses that cookie and the login loops. n8n carried its own proxy_cookie_flags n8n-auth nosecure for this; every other app on the onion pass was exposed to the same failure.
The plain-HTTP branch of nginx_ssl_header.j2 now emits proxy_cookie_flags ~ nosecure, which matches every cookie; TLS vhosts are unchanged. n8n's per-cookie line goes. Taiga and Semaphore keep their tested location-level flags, which replace the server rule for their location, and network_modes.md says so. test_nginx_ssl_header.py asserts the rule on a plain-HTTP vhost and its absence on a TLS one; without the template line the plain-HTTP case fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 WordPress#0 (swarm, variant 0, onion pick): the block editor canvas stayed transparent (rgba(0, 0, 0, 0) instead of --design-surface-1). The trace shows the canvas iframe requesting https://cdn.main.infinito.test/_shared/css/default.css and the onion page's CSP (style-src-elem 'self' http://cdn.<onion> ...) blocking it; the clearnet pass of the same spec was green. design.php writes WORDPRESS_DESIGN_CONFIG.tokens into the infinito_design option once for every network sibling, so an absolute CDN host pins the onion editor to clearnet.
The token sheet now uses the cdn lookup's relative_urls, rendering /_infinito/cdn/_shared/css/default.css. WordPress prefixes a relative style source with site_url(), which infinito-network-origins derives from the request host, and every generic vhost serves /_infinito/cdn/ (basic.conf.j2 includes cdn.conf.j2), so the canvas loads from the page's origin on both networks. Same approach as the Joomla TinyMCE fix 175e00218f. test_design_tokens.py renders the tokens through the real cdn lookup and fails on the old URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 Nextcloud#0: on the onion pass the biber OIDC logout cleared only the clearnet session, because Keycloak holds one front-channel URL per client and the injected logout sent no post-logout target for the onion network; and Collabora's aliasgroup1 listed only the clearnet WOPI host.
On a vhost outside the application's canonical network (and without an external issuer) the injected logout one-liner now adds client_id and post_logout_redirect_uri set to that network's logout conductor, and Keycloak registers every conductor sibling as post-logout redirect URI next to '+'. Collabora's aliasgroup1 lists the Nextcloud WOPI host of every network (one instance, union allowlist). network_modes.md gains a Network-Aligned Values table, and test_network_allowlist_sinks.py fails when a template names an allowlist sink without the sibling union.
Rendering is covered by 14 tests; the full gate and the runtime acceptance of the '##' post-logout list by Keycloak and the comma aliasgroup by Collabora still need a gate on store-i18n and an onion deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521, Baserow#1 (swarm, variant 1): the PASS 2 MCP probe got a 500 and the deploy refused the endpoint. In replica 3, worker 244 served MCP at 11:20:40 and then sat idle for 3824 s; the next request at 12:24:24 failed with "OperationalError: server closed the connection unexpectedly", and every later one, including the 13:09:10 probe, with "InterfaceError: connection already closed". Postgres stayed up throughout. The swarm service VIP (IPVS) drops TCP idle for 900 s.
BASEROW_DATABASE_URL now carries libpq keepalives (keepalives_idle=60, interval 10, count 6, tcp_user_timeout 120000), the same set as the OpenProject swarm fix. test_database_keepalive.py parses the rendered URL with libpq's parse_dsn and fails on the old URL.
Baserow 2.4.1 mounts its MCP Starlette app before Django's handler, so close_old_connections never runs there and a dropped connection stays dead until the worker restarts; that half is upstream. A server-side keepalive on svc-db-postgres that replaces the per-role options is tracked in #1298.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rsync 3.5.0 (Debian security update 3.5.0+ds1-0+deb13u1) refuses a
destination symlink owned by neither root nor the calling user.
svc-opt-ssd-hdd turns a volume's `_data` into a symlink to /mnt/hdd or
/mnt/ssd. The restore drill empties the volume and runs `compose create`;
docker repopulates it and lchowns the `_data` symlink to the image
directory's owner (e.g. www-data). The restore then failed with
"refusing to follow a symlink owned by an untrusted user" and
"change_dir#1 .../_data/ failed: Too many levels of symbolic links",
the drill left its subjects down, and the dependent checks timed out
(CI run 40979252521, ssd-hdd#0, on feature/store-i18n; recover.py is
identical on main).
VolumeRecovery now resolves the mountpoint before handing it to rsync:
os.path.realpath locally, `ssh <host> realpath -e -- <mountpoint>` for
--docker-host, so rsync writes into the real directory and never meets
the symlink. `--insecure-links` was not used because older rsync builds
on other distributions lack it. Two new unit tests (local symlinked
mountpoint, remote resolution); both fail against the previous code.
Root cause and fix by the i18n session (feature/store-i18n triage of
ssd-hdd#0), ported to main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A logo whose solid pixels share one colour (alpha >= 128, every channel within 24) is detected once per file and tinted to the text colour of each slot: frame_fill on frame slots, fill elsewhere, plus a prefers-color-scheme: dark variant in the SVG of page slots and the page text colour for the favicon. Edge pixels below half opacity are ignored so antialiasing does not count as a second colour. Multi-colour logos render byte-identical to before.
make design-logos (alias i8delo) renders every role with a design entry from one logo and base colour into an HTML page with day and night tiles, without a deploy. Slots render in four threads and roles in one process per CPU: the full preview went from 287.6s to 33.2s with identical output (diff -rq over 1229 PNGs). A GPU does not pay off for these PIL resizes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swarm run 2 of the 061 Nextcloud step failed on every worker with "Assert Talk has the TURN server its calls relay through". The TURN server comes from web-svc-coturn's group membership (meta/services.yml standalone_enabled), which only the stack host carries; workers never run Talk, so the assert judged hosts it has nothing to say about. It came in with 5728502a19 and exists only on this line, not on main.
The assert now runs where IS_STACK_HOST holds: every compose host and the swarm manager, unchanged. test_talk_turn_assert.py evaluates the block and task guards plus the assertion with Jinja; against the unfixed file only the worker case fails.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared OpenResty nginx.conf took worker_processes and worker_connections
from WEBSERVER_WORKER_*, which are the min() of svc-prx-openresty's limits and
the current play's application_id. The file renders once, in the scope of
whichever role loads the proxy first, so that role's limits capped every vhost:
on feature/store-i18n web-svc-mirror (pids_limit 64) loaded it first, every
vhost got worker_connections 64, and openresty raised 555 "64
worker_connections are not enough while connecting to upstream" alerts while a
single openwebui page fires ~150-170 chunk requests (CI run 40979252521,
homeassistant#1). Main carries the same wiring.
Add WEBSERVER_PROXY_WORKER_PROCESSES and WEBSERVER_PROXY_WORKER_CONNECTIONS,
computed from svc-prx-openresty's own openresty service only (same
resource_filter fallbacks, at least one worker), and render nginx.conf from
them. WEBSERVER_WORKER_* keep pairing the proxy with the play for app-internal
nginx such as Nextcloud's. The values can only stay equal or rise.
Found by the i18n session's triage of homeassistant#1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gate pass 5 flagged test_gpu_detection.py:31 in test_file_spot: the
`ROLE / "vars" / "main.yml"` chain hard-codes a role-file path the
utils.roles.mapping SPOT already names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to ae259a9f58 after an independent review:
- The 136-character task name broke the 80-character name lint
(tests/lint/repository/test_name_emoji_and_length.py). Names are short
again; the device/driver/host matrix lives in the role README.
- Both stats now always run and lookup('gpu') requires both registers,
instead of reading a skipped device stat as "no device".
- A bare host that shows /dev/nvidia0 without nvidia-smi stops the
deploy with a message naming the fix. It used to die on the missing
binary anyway; ae259a9f58 would have run its GPU services on the CPU
silently. A container in that state (a swarm lab node, the dev
container without its GPU override) still runs on the CPU.
- svc-ai-s1 decided its CUDA build from its own stat of /dev/nvidia0 and
nvidia-container-runtime while its runtime came from lookup('gpu'), so
a host with the toolkit but no nvidia-smi would have built a CUDA image
that gets no GPU. S1_GPU now reads lookup('gpu'), the one detection.
- The libretranslate-engine README and the s1 TODO describe the new
detection.
test_gpu_detection walks the real tasks over device, driver and
container; against the original 00_core.yml it fails in the
driverless-container, driverless-bare and device-and-driver cells.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local 061 Nextcloud swarm step failed on every lab node in
"Ask the driver which CUDA devices this host carries" with
"[Errno 2] No such file or directory: b'nvidia-smi'". The lab nodes are
privileged containers (compose/swarm/compose.yml), so on a workstation
with an NVIDIA card they see the host's /dev/nvidia0, but they carry no
driver. The device node alone made them GPU hosts, for the GPU include
and for lookup('gpu') alike, which would also have picked a -cuda image
and runtime: nvidia for a GPU service there.
The role now stats SYS_SVC_CONTAINER_GPU_DRIVER (/usr/bin/nvidia-smi)
first and stats the device node only where the driver exists; the
skipped register reads as "no device" in lookup('gpu'), and the include
requires a device stat that ran and found the node. The query runs the
same binary the detection probed. Hosts with driver and device (bare
metal with the driver, the compose dev container through the GPU
override) and hosts without a device (CI) behave as before.
test_gpu_detection walks the real tasks over the four driver/device
cells and fails on the old 00_core.yml in exactly the device-without-
driver cell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in feature/updater 8c14873225: lockdown-probe with the Bluesky
broker key-rotation fix (0a6b6f9130) and the occ-only trusted_proxies
(406998f43a), the ERPNext server_scripts switch, and main with the
hotfixes #1290 (BBB apps-akka READ_COMMITTED), #1291 (FusionDirectory
PHP_MEMORY_LIMIT) and #1293 (Matrix variant 1 force_verification false).
Conflict resolutions:
- web-app-erpnext: create_site.yml takes updater's include of
restart_frappe.yml; that file's compose/swarm gates use
IS_COMPOSE_MODE / IS_SWARM_MODE instead of DEPLOYMENT_MODE literals.
- web-app-mailu, web-app-stalwart vars: store-i18n's RUF from
users.platform_administrator plus the new RUA vars; dmarc.rua_user is
platform_administrator (on store-i18n `administrator` is the host
account), so RUA and RUF share the app mailbox. READMEs follow.
- web-app-stalwart 09_dns-records.yml: store-i18n's block form with the
alias-domain MX task, plus rua=mailto in the DMARC record.
- web-app-pretix: store-i18n's source build stays; the version bump
lands as source_version v2026.9.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_no_uncached_file_reads flagged four direct reads in test_server_scripts.py. The script comes from the project tree and is now read through utils.cache.files.read_text. The three reads of common_site_config.json stay direct and carry `# nocheck: cache-read tempdir fixture`: the file lives in a temporary directory and the script under test rewrites it within the same test, so a cached read would return stale content. Each read sits on its own line so ruff format keeps the marker beside it. Quality-high pass 16 ran green with this change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI run 40979252521 SuiteCRM#0 (swarm) picked the onion sibling and every
signed-in design spec failed on "the administrator owns a SuiteCRM
session" (100 times in the job log). The SAML ACS is
%env(SITE_URL)%/saml/acs and SITE_URL is the single canonical URL, so the
trace shows Keycloak posting the assertion to
https://suite.crm.main.infinito.test/saml/acs; the session landed on
clearnet and the onion host bounced back to /saml/login. Keycloak would
also have refused an onion ACS, because the SAML client listed only the
clearnet one.
- SuiteCRM: an auto_prepend_file (site_url.php, config-mounted) sets
$_ENV['SITE_URL'] to the network sibling the request arrived on,
matched only against the sibling URLs rendered at deploy time;
Kernel::setSiteURLEnv keeps an existing value. The canonical host and
CLI runs keep the installer's site_url. The mount target is read from
the volume registry.
- Keycloak: the SAML client's redirectUris come from
lookup('network_urls', <app>/saml/acs). Additive: clearnet stays first
and a single-network node renders the same one-entry list.
Executing the rendered site_url.php in PHP sets the onion URL for the
onion host (with and without :80) and leaves SITE_URL unset for the
clearnet host, a forged host and the CLI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 EspoCRM#0 (swarm) picked the onion sibling and failed
the OIDC addon spec plus every signed-in design spec: EspoCRM builds its
redirect_uri as siteUrl + /oauth-callback.php from the single canonical
URL, so the onion login popup returned to
https://espo.crm.main.infinito.test/oauth-callback.php (12 times in the
job log), oidc.js could not read the code across origins and the onion
host never got a session.
A data/config-override.php, which Config::load reads on every request,
now answers siteUrl with the network sibling the request arrived on,
matched against the sibling URLs rendered at deploy time and nothing
else. The canonical host and CLI runs see no override. The manager ops
write it through container exec with a compare-and-rename that only
reports a change when the content moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 Weblate#1 (swarm) never converged in its first pass:
every weblate, worker and beat task died on collectstatic with
"PermissionError: [Errno 13] Permission denied:
'/app/cache/static/weblate_fonts'". The per-container tmpfs from
a0bcd59150 sits on the weblate_cache volume, whose static mountpoint is
root-owned 0755 (nfs-volume-contents.txt: drwxr-xr-x 0:0), and runc gives
a tmpfs on an existing mountpoint that directory's mode, while Weblate
runs as uid 1000.
The three static tmpfs mounts now declare mode 1777, rendered as
tmpfs.mode 1023; test_static_root_per_container.py asserts it per
service.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 Matrix#3 (ansible flavor) failed the MDAD bootstrap
in the pass after the credential rotation at "Ensure Synapse user
registered - baibot". store-i18n runs the playbook with
setup-all,ensure-matrix-users-created,start, so the user creator runs
before start. After the reconfigure restart systemd had already booted
matrix-synapse on the previous pass's homeserver.yaml, which failed
against the rotated database password ("password authentication failed
for user matrix" in the nested journal) and crash-looped. The user
creator's `state: started` returned ok for the still-activating unit,
skipped its pause, and the registration found no container.
bootstrap.sh now stops an installed matrix-synapse.service right before
the playbook, so the user creator starts it on the configuration the
playbook has just rendered and waits for it. A first install has no unit
yet and is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
applications.web-app-erpnext.services.erpnext.server_scripts (bool, default false) now sets server_script_enabled on every deploy, so an inventory can turn ERPNext Server Scripts on and off again. The security check app needs them, and the validator aborted on its inventory with "Missing default for web-app-erpnext: services.erpnext.server_scripts" while core declared no such key.
Frappe v16 reads the key only from common_site_config.json, through a cached lookup (frappe/utils/safe_exec.py:86-88), and tests it with bool(). files/python/apply/server_scripts.py therefore writes the global file, never the site config, and stores the integer 1 or 0: a plain `bench set-config -g server_script_enabled 0` stores the string "0", which bool() reads as enabled. A value already stored as a string is normalised. The script reports changed only when the value differs, and only then are the Frappe processes restarted, so they drop the cached config.
The restart, the backend re-resolve and the gunicorn wait move from create_site.yml into 02_bench_bootstrap/restart_frappe.yml, unchanged apart from the task names, so the new site and the switch share one restart path for compose and swarm. Six unit tests cover enabling, disabling a string value, the string "0", an aligned value left untouched, the capitalised bool Ansible renders and a rejected value; storing strings instead fails three of them.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI run 40979252521 homeassistant#0 (swarm) failed in the pass after the
credential rotation: the owner realign rewrote the auth store and flushed
the recreate handler, which in swarm runs `docker service update
--force` and starts a new task container. The following owner check
still exec'd into the container id resolved before the flush and hit
"container eb0523a644de... is not running" 30 times, while the rescue
dump lists eb0523a644de exited and bbec666459ae up and healthy.
The resolve_host_cid include moves into tasks/utils/resolve.yml; 00_core
uses it as before and owner/realign.yml runs it again right after the
flush, so the check and the MCP tasks after it talk to the new
container. Compose keeps its stable container name, so nothing changes
there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings 20 commits from main: the DMARC aggregate report address (rua) for Mailu and Stalwart with its hardened checks, the Stalwart production facts and version pin update, the Pretix and Shopware image updates, and ruff 0.17.3. The merge was clean; the auto-merged YAML files carry no duplicate keys and the merged Python files are ruff-format clean.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The workspace swarm roundtrip for svc-db-postgres failed on every host
with "The lookup plugin 'tls' failed: NETWORK_MODE 'multi' needs
svc-net-tor with a node onion on this node" (CI run 40979252521, job
Workspace debian:13). roundtrip.sh (b2469ff948) pinned the swarm step to
multi whenever the validator accepts it and never looked at disable=,
while 03_roundtrip.sh passes a disable list that removes tor. The
hard-fail is the documented contract (network_modes.md: tor or multi on
a node without the provider fails the deploy); the request was wrong.
roundtrip.sh now pins clearnet when disable= holds the Tor key (read
from utils.github.variant.network.TOR_SERVICE), else multi when the role
takes it, else the rotation. 02_zombie.sh, which also disables tor,
passes network=clearnet instead of relying on the rotation drawing it.
TestRoundtripSwarmNetwork runs the real script against stubs; the old
pin logic fails three of its cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI run 40979252521 Taiga#0 picked the onion sibling and failed five specs
(test-login-oidc, test-themed-routes, network-leak, design:242/263).
- Login: TAIGA_COOKIE_SECURE renders True once clearnet has TLS, so
taiga-back marks sessionid and csrftoken Secure. Chromium drops them on
the plaintext onion, the /oidc/callback/ arrives without a session and
mozilla-django-oidc falls into login_failure() (gateway log: the 302 to
/login?type=oidc&error=None six times, no sessionid on the callback in
the trace). The plaintext sibling vhost now strips Secure from both
cookies via proxy_cookie_flags, wired as webserver_location_extra_
configuration like Semaphore; TAIGA_COOKIE_SECURE is pinned to the
canonical domain so the per-vhost tls lookup cannot flip it.
- Logo: the design sheet embedded absolute clearnet CDN URLs, rendered
once and served to both siblings. It now uses design.relative_urls,
served same-origin under /_infinito/cdn/; the spec resolves the logo
URLs against page.url().
A local compose deploy that picked the onion passed all five specs (19
passed); the onion vhost carries both proxy_cookie_flags lines, the
clearnet vhost none. test_onion_sibling.py renders the real vars through
Ansible's Templar; reverting TAIGA_COOKIE_SECURE fails two of its cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A custom role with a local source (`source.url: /opt/src/custom`) is copied with shutil.copytree, which skipped only .git and __pycache__. A dev checkout that Claude Code has worked in carries an empty `.claude/.cc-writes` (mode 0700, ignored by the global gitignore), unreadable for the runner uid, so `infinito administration deploy dedicated` aborted before the play with "Custom roles refused: ... [Errno 13] Permission denied". A git source never carries that directory, because it is staged through `git archive`.
The copy now skips `.claude` as well. The ignore list has one consumer, the local branch of stage(); git sources are unchanged. A new test creates `.claude/.cc-writes` at mode 000: before the fix it fails in shutil with the reported Permission denied, after it all 102 custom-role tests pass. Reported by the GUI lane on efd72474a6.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fresh compose deploy (compose-deploy-req061-4.log) spent 764.9 s in the plugin phase. Two parts of it did no work:
Enable stage after a fresh install: occ app:install runs enableApp and prints "<app> enabled" (core/Command/App/Install.php, stable34), so the app:enable that 03_enable.yml ran next only answered "<app> already enabled". The include, the exec, the classify, the skip notice and the queue cost 90.9 s for the 26 plugins installed fresh. nextcloud_install_status now reports enabled when occ exited 0 and printed an "<app> enabled" line; 02_install.yml then queues the plugin for the configure stage directly and runs 03_enable.yml only for an install that left the plugin off ("already installed" from a pinned archive or the recognize background job, or a future --keep-disabled). Queue order is unchanged.
Configure stage: 49 pending config:app:set calls ran as 49 occ processes in one sh -s and took 62.8 s, about 1.28 s of occ boot each. files/php/utils/occ_batch.php boots the console the way Nextcloud's console.php does (lib/base.php, OC\Console\Application::loadCommands, setAutoExit(false)) and runs Application::run once per call, so every call goes through the same command object, app loading, ConsoleEvent and exit code as occ, with one boot. The filter nextcloud_occ_config_set_batch builds the argv per entry; a unit test proves it equals shlex.split of the former shell line for typed, quoted, multi-line, empty and non-ASCII values. The calls travel as base64 JSON assigned in front of the script on stdin, so no value reaches a host or container argv; the first failing call stops the run and names appid/configkey on stderr, the task still reports changed on "set to".
Expected saving per fresh deploy: about 74 s (enable chain, net of the skipped include and the new set_fact, 0.64 s per plugin) plus about 60 s (configure exec down to one boot), about 134 s of 764.9 s. A converged mode=update deploy installs nothing and writes about 3 values, so it saves about 2.5 s; only a live deploy can confirm both numbers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test from feature/updater renders the real daemon.json.j2, which on
store-i18n also reads IS_SWARM_MODE, the NVIDIA runtime shim and the
daemon MTU. Its context lacked them, so both cases died with
"'IS_SWARM_MODE' is undefined". The context now carries them and derives
the mode booleans from DEPLOYMENT_MODE like test_sandbox_runtime does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
make autoformat output for three lines the feature/updater merge brought
in over 88 characters; matches feature/updater-lockdown-probe 88d138ba1c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
make autoformat rewraps the firewall-backend entry in test_userland_proxy.py, the _direct_media call in test_talk_direct_media.py and the _git listing context in test_files.py, which ran past 88 characters. The text is ruff's own output, the same store-i18n commits, so the next merge between the lines resolves cleanly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in feature/updater accb22da75, which carries main 5995766f58.
Conflict resolutions:
- web-app-fider: main's tenant bootstrap (01_tenant.yml) runs first on
the stack host, OIDC setup moves to 02_setup_oidc.yml, design to
03_design.yml; flat includes so main's gate test reads them.
- sys-front-tls test_nginx_ssl_header: the tls fake maps the domain term
store-i18n passes back to its app.
- group_vars 04_webserver.yml, sys-svc-proxy CSP header: store-i18n's
webserver_resource lookup plus #1256's WEBSERVER_SECURITY_HEADERS and
the security_headers.lua include after the CSP block.
- web-app-nextcloud: updater's decoupled Talk stack (Janus, signaling,
NATS, recording builds) replaces aio-talk; store-i18n's per-network
Talk backends move from the wrapper script into signaling.conf.j2 urls
(NEXTCLOUD_HPB_SIGNALING_BACKEND_URLS from network_urls, canonical
first); IS_COMPOSE_MODE/IS_SWARM_MODE booleans kept; the integration
OpenProject digest stays delegated to STACK_HOST.
- users lookup docs, token-store write_many, cache README, meta info
lint: both sides combined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a clearnet deploy with self-signed TLS, test-talk-recording.js failed three times: the call recording stayed at state 4 (starting audio) because the recording server could not notify Nextcloud. Its log showed CERTIFICATE_VERIFY_FAILED on POST /ocs/v2.php/apps/spreed/api/v1/recording/backend, although compose-ca sets REQUESTS_CA_BUNDLE to the mounted bundle. nextcloud-talk-recording's BackendNotifier sends through `session.send(prepared, verify=True)`, and Session.send() skips the environment merge that only Session.request() does, so requests falls back to certifi's bundle, which does not know the injected root CA. A probe in the recording container's network namespace verified Nextcloud with the mounted bundle (200) and failed without it.
When the CA is injected, the recording image now points certifi's cacert.pem at the mounted CA bundle (the system CAs plus the injected root), in the same build step as the Firefox certificate policy. Verification stays on. Without an injected CA, as with real certificates, the build arg is empty and the image is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in roles/web-app-gitea/tasks/01_manager_ops.yml: main (c59e2838d6, c2577560f8) moved the readiness probes inside the Gitea container on its hosting node, because a host port lands on the swarm ingress mesh; this line (381c3cd5bc) had reduced them to one wait for /api/v1/version = 200 and dropped the explicit `gitea migrate`, which raced Gitea's own migration on galaxyserver. The resolution keeps main's in-container probe as that single wait with `curl -fS` (200 required), after the resolve and with this line's 600 s budget scaled by lookup('timeout'), and drops main's app.ini wait, its migrate task and its post-admin readiness wait.
main's new tests/unit/python/roles/sys-front-tls/templates/test_nginx_ssl_header.py renders nginx_ssl_header.j2 without `domain`, which ssl_credentials.j2 needs on this line since 47160d755e; its render() now passes the vhost's domain.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>