subprocess.run now passes check=False (ruff PLW1510), and the stub-log read sits in its own short assignment so the formatter no longer moves its nocheck onto a closing paren.
🎯 Fix: 99 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitLab#3 of fork run 42305502796 died at the deploy cap (update pass rc=-15 after 7525 s; both passes may use 19020 s of the 20400 s budget). The round was storage-constrained by a hair (declared 92.1 GiB against 91.6 GiB free), so the prune handler ran after every compose flush: 55 prunes, 2926 s, up to 116 s each. The job log's host sampler (1928 free-disk samples) shows that those 55 prunes reclaimed only 13.2 GiB in total.
prune.sh now reads the free space of the Docker data root and leaves Docker data alone at or above compose_prune_free_floor_mib (30720). A probe that fails falls through to the prune. Replayed on the sampler series, the floor fires 3 of the 55 prunes (302 s, saving 2624 s) and keeps the lowest free space at the observed 22.8 GiB. Across the eight red rows of the run, the largest growth between two flushes is 34.7 GiB, at the first image pulls only while ~90 GiB are free, and at most 15.3 GiB later. The unconstrained NC#7 already bottoms at 7.9 GiB without any prune. The handler reports changed only when it prunes. test_prune.py covers skip, prune, failed probes and a missing floor; against the old script the skip and the missing-floor cases fail.
🎯 Fix: 85 %
💥 Break: 5 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 81
🚦 Gate: ⛔ fix 85 % < 90
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every shared Redis consumer authenticates as the default user with REDIS_PASSWORD (plugins/lookup/engine.py). The rotation gate between PASS 1 and PASS 2 changes that password, the engine restarts with only the new one, and every consumer deployed later in the pass keeps the old secret. In fork run 42305502796, Nextcloud#7 direct then crash-looped Nextcloud and Weblate from 02:50:31 until the deploy cap killed the job (about 2600-2700 WRONGPASS lines per node, host CPU pressure from 25 % to 80 %).
00_core now reads the password hashes a healthy engine accepts before the restart and adds the replaced ones back after it. Once every server role has redeployed, tasks/stages/02_server.yml retires them before the end-to-end tests: only the current password is left, the ACL log is reset and every client is disconnected. After the tests, a verify step in the test runtimes fails the pass on every authentication Redis refused since the retire, so a consumer that never picked up the new secret still fails loudly. The rotation gate is unchanged. The README no longer describes per-consumer ACL users that never existed.
🎯 Fix: 88 %
💥 Break: 6 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UNICORN_WORKERS was hard-coded to 8. On a 4 vCPU / 16 GB CI runner that holds about 4 GB of web-worker RSS beside every other application of the round; the Nextcloud#7 rescue snapshot of fork run 42305502796 shows 8 workers of about 500 MB and 6.8 GiB of swap in use. The count is now clamp(min(2 * vCPU, RAM // 4 GiB), 2, 8): upstream's setup-wizard rule of two per vCPU up to 8, capped at one worker per 4 GiB of host RAM because the host is shared. The CI runner gets 3; hosts with at least 4 vCPU and 32 GiB keep 8. Only the env changes, so the launcher recreates the container without a rebuild (03_launcher.yml:50 drops env from the bootstrap digest).
🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/lint/repository/yaml/test_no_direct_calls.py rejects direct
yaml.safe_load calls; test_casc_oidc_scopes.py from 68c9aa5b3a had one.
It now uses load_yaml_str. The lint and both scope tests pass.
🎯 Fix: 97 %
💥 Break: 1 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 96
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
containerd inside the matrix-mdad runner runs with LimitNOFILE=infinity, so every nested container without its own --ulimit inherits 2147483584 open files. uwsgi in matrix-synapse-usage-exporter sized its fd table from that and pinned 16 GiB in CI run 42239638356; ccaa075b54 capped that one container. The inner daemon.json now sets default-ulimits nofile 1048576 soft and hard, so any other nested service that sizes a table from RLIMIT_NOFILE stays bounded too. A container's own --ulimit still wins, so the exporter keeps its 65536.
Rendered daemon.json passes dockerd --validate (29.8.2); a string limit is rejected (negative control). The new unit test pins integer Soft/Hard within 65536..1048576 and fails on the base without the key. No live nested-dockerd run: the operator merges sidetrees straight into store-i18n, whose CI proves it; break 6 % accepted by the operator.
🎯 Fix: 92 %
💥 Break: 6 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ⛔ break 6 % > 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main run 42235551402 (5d78f3ae7d), job 128223280156 Moodle#2 (swarm): 03_ownership.yml:24 'find -exec chmod 0644' failed with hundreds of 'No such file or directory' under public/lib/yuilib. moodle_code is one NFS export shared by three replicas and cron, all on release 5.3.0. The bootstrap lock lives on that export and does not serialize across NFS clients (workers log 'flock: 9: Input/output error'). So a second same-release bootstrap swapped the tree in while the deploy walked it; the rescue listing at 02:22:34 shows neither .bootstrap.stage nor .bootstrap.done, the window between the stage rmdir and the sentinel write.
Port of two store-i18n commits by Stage; 5c3749c437 and 7bc5822421 are already on main:
- moodle_bootstrap_code_dir re-reads the sentinel after its copy and stands down when another container finished the same release (cherry picked from commit db1a27b3f2).
- The ownership walks wait until the stage is gone and the sentinel holds the expected release, prune the stage and batch through -exec +; stage and sentinel paths come from vars/main.yml via ARG/ENV (cherry picked from commit 9a5965b87c).
The rationale header the port added to moodle-entrypoint.sh and a stray comment line in 03_ownership.yml are dropped. test_moodle_entrypoint: 8/8 green; without the stand-down test_a_sibling_that_finishes_first_keeps_its_tree fails.
🎯 Fix: 88 %
💥 Break: 6 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: ~100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main run 42235551402 (5d78f3ae7d), job 128223280276 Jenkins#0: the OIDC redirect ended at /securityRealm/finishLogin?error=invalid_scope. With a wellKnown server configuration and no scopesOverride, oic-auth requests every entry of the discovery document's scopes_supported (OicServerWellKnownConfiguration). Keycloak rejects the whole request when one scope is not a default or optional scope of the client. The requested list minus the scopes of the infinito.nexus client leaves exactly kc-admin-api, a Keycloak built-in assigned only to admin-cli and security-admin-console.
casc.yaml.j2 now pins scopesOverride to 'openid profile email'; groups keeps arriving as a default client scope. The new unit test renders casc.yaml.j2 and the shared client template default.json.j2 and asserts every requested scope is one the client carries; without the override both tests fail.
🎯 Fix: 91 %
💥 Break: 7 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: ~100 %
🏆 Score: 85
🚦 Gate: ⛔ break 7 % > 5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MDAD's synapse-usage-exporter runs uwsgi with an HTTP router that writes an 8 byte slot per possible file descriptor, sized from RLIMIT_NOFILE. Inside the matrix-mdad runner the nested daemon hands its containers the kernel maximum of 2147483584, so the exporter pinned about 16 GiB of RAM and swap and the swarm watchdog aborted WordPress#0 and Nextcloud#7 in run 42239638356. The addon now declares config.nofile 65536 and the role passes it to MDAD as --ulimit=nofile=N:N for that container; a local compose deploy shows the exporter at nofile 65536 and 38 MiB.
🎯 Fix: 97 %
💥 Break: 2 %
💸 Cost: 1 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 100
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4c682fbdc1 grew the when-gated write block of write_many.yml to four children, which test_no_oversized_ansible_block and test_block_size reject, and its integration test read the plays' views uncached. The hand-over and the writer's own merge now live in tasks/utils/hand_over.yml, included from the block with no_log applied once; the order and the lockstep behaviour stay the same (re-proven on ansible-core 2.20.2 and 2.21.5). The integration test reads through utils.cache.files, and a unit test pins the masking of every task that carries the merged store.
🎯 Fix: 95 %
💥 Break: 2 %
💸 Cost: 104 %
⏱️ Time: 107 %
🛡️ Security: 100 %
🏆 Score: 84
🚦 Gate: ⚠️ cost 104 % > 100, time 107 % > 100: ask the operator before the commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Swarm workers loaded sys_token_store_users before the manager wrote new tokens in pass 1, so they rendered msmtprc with an empty password (CI run 42128590251, WordPress#0). write_many.yml now notes once, before the block, whether the merge changed the store; delegates the merged store to every play host that shares the writer's STACK_HOST; and re-sets the writer's own merge afterwards, so a peer writer's hand-over cannot overwrite it. Verified with lockstep plays on ansible-core 2.20 and 2.21.
🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 108 %
⏱️ Time: 116 %
🛡️ Security: 100 %
🏆 Score: 71
🚦 Gate: ⚠️ cost 108 % > 100, time 116 % > 100: ask the operator before the commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chat integration and Mermaid booted Rails twice each, once to apply and once to read back (10.9 s and 11.1 s for the read-backs on echoserver 94767d4). Each verify script now runs after its apply script in the same rails r and starts with SiteSetting.refresh!, so it reads what the database holds, as 2b11a7e9fe does for the site settings. The test stub keeps a dropped write in memory, so the read-back test fails without the refresh.
🎯 Fix: 90 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gate pass 2 on store-i18n 9154a03fed failed test_jobs_run_concurrently_within_the_cap_and_keep_their_own_results at 2.7046 s against a 2.7 s bound: five interpreter starts under full host load. The peak of 2 read from the job trace already proves the overlap and the cap independent of load, and test_a_freed_slot_is_refilled_without_waiting_a_poll covers prompt refills, so the wall-clock bound goes.
🎯 Fix: 95 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
py/partial-ssrf on PR #625 (agents.py:195, :250, server.py:123): the chat body's model now selects its entry from AGENT_PLATFORMS through agents.pick, so the agent host is built from the server-side key; a list or dict model answers 404 instead of a TypeError. The relay forwards only /v1/chat/completions and /v1/models with the broker's key; any other path answers 404 and logs relay_refused (the two paths are the ones the relay logs show in use).
py/empty-except (agents.py:201, :258): the health wait names its last error in the timeout, and the model-list fallback logs agent_model_fallback through the log function the server passes in.
Tests cover pick, six refused relay paths and both error paths; each fails against the old code.
🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
py/missing-call-to-init on PR #625 (test_engine.py:110): _ConnectEngine never ran the base initializer; it now does, and a new test checks the base state.
🎯 Fix: 99 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 98
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
py/overly-permissive-file on PR #625 (run_parallel.py:287): tor.lock was created 0o644; the scheduler and wait_onion_reachable.sh both run as root under become. The new test sets umask 0 and fails at 0o644.
🎯 Fix: 95 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: ~100 %
🏆 Score: 92
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- py/http-response-splitting (server.py:87): _redirect percent-encodes CR
and LF in the Location header
- js/xss-through-dom (versions.js:82, languages.js:25): both switchers
navigate to the URL they built instead of select.value
The new cases in test_server.py, versions.test.js and languages.test.js
fail without the fix and pass with it.
🎯 Fix: 92 %
💥 Break: 3 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_no_uncached_file_reads and test_no_direct_yaml_calls failed test-lint in run 42056917806. env.j2, manager_ops.yml and the realign script now come from utils.cache; the before/after read of the tempdir SQLite file keeps a direct read with the cache-read opt-out, since a cached read would hide a write.
🎯 Fix: 97 %
💥 Break: 1 %
💸 Cost: –
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 96
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fix/tests/store-i18n-lint-red (1205c4a2ae, 95/1): clears the raw-walk,
project-root and env-key lint failures store-i18n would have pushed
- fix/sys-stk-front-proxy/body-filter-fold (4984f506c7, 92/2): the
body_filter Lua is written in the vhost's single stack_host_contents
call (about 184 s echoserver and 174 s galaxyserver per deploy,
projected), and stack_host_contents compares bytes
🎯 Fix: 92-95 %
💥 Break: 1-2 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 90-94
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every front-proxy vhost wrote its body_filter Lua through a separate
stack_host_template task in sys-front-inj-all: two remote round trips when
unchanged (stat + file) and four to five when changed, 191 s over 54 vhosts
per echoserver deploy and 179 s over 48 per galaxyserver deploy. The Lua
now joins http.conf and the vhost in the Deploy task's stack_host_contents
call, written first as before, rendered through template_shallow with
application_id and domain pinned to the vhost.
- sys-front-inj-all/tasks/main.yml no longer writes the Lua; the new
standalone.yml includes it and keeps the separate write for matomo's
re-inject path, which has no vhost Deploy task
- stack_host_contents compares bytes instead of a universal-newline text
read, so content with a CR stops reporting changed on every run and an
existing non-UTF-8 file is replaced instead of crashing the module
- a new required diff_paths argument keeps --diff for the Lua only; vhost
and http.conf content stay out of the diff
A Lua-only change still reports changed and fires reload-openresty once;
the served probe and the fallback redeploy keep their old triggers. Check
mode is unchanged.
Tests: the real stack_host_template action against the fold expression
across 99 roles and TLS/Tor/injection scenarios, byte-identical, with
trim and newline negative controls; owner, group and mode parity with the
copy module; byte compare with CR and non-UTF-8 files; the wiring of both
callers. The saving (about 184 s and 174 s per deploy, net) is projected
until the next deploys.
🎯 Fix: 92 %
💥 Break: 2 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 90
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- test_no_raw_project_walk: the Synapse single-writer test and the
compose single-writer lint walked role task dirs with rglob; both now
filter the cached iter_project_files walk (the same 1283 role task
files), and the Synapse test asserts it found any
- test_project_root_import: copy_batch's traversal guard rejected '..'
through a banned literal; it now compares posixpath.normpath("/" + name)
with "/" + name, which under the preceding conditions holds exactly when
no part is '..', and new cases cover an interior a/../b and a
nested/../../ escape
- test_used_keys_declared: the resolve test embeds the old task text,
whose 'INFINITO_DESIGN' is a JS marker, not an environment key; marked
with a same-line nocheck on the operator's approval
🎯 Fix: 95 %
💥 Break: 1 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- feature/ci/wordpress0-playwright-parallel (de40211272, 88/6, merged on
the operator's word below the gate): roles that opt in through
playwright.parallel (WordPress) run independent Playwright suites in
parallel under a CPU-pressure gate, with dependency deferral, an onion
cap and Tor lock, crash counting and per-job artifacts; roles without
the opt-in run as before. The next push's WordPress#0 CI run is its
live proof.
🎯 Fix: 88 %
💥 Break: 6 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5; merged on the operator's word, CI is the live proof
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WordPress#0 runs 18 Playwright suites one after another and exceeds its
CI time budget. A role that opts in through playwright.parallel in its
meta/tests.yml (WordPress: [3, 1, 1] per variant) now hands its suites to
an async scheduler, run_parallel.py, after staging:
- a suite starts only when its dependencies finished here (Keycloak first)
and the host's CPU pressure, avg10 and avg60, stayed at or below 36 for
30 s; the first container always starts and running ones are never
killed
- onion suites share their own cap of 3 and a Tor lock with a
recreate-pending marker; a probe that times out on the lock re-probes
the current circuits and, failing that, leaves only that app to the
serial path
- an app whose dependency was dropped is deferred, and every deferred app
gets the serial first pass before the existing retry, in round order
- each job gets --oom-score-adj=1000 instead of a memory limit; crashed
pages, closed browsers and killed workers are counted and named in the
per-app timing line
- each job's result.json, stdout and stderr land in run-parallel/ and are
archived with the reports
- the async ceiling is the serial worst case per app and reachable
network, curl's per-attempt max time included (now
TEST_E2E_PLAYWRIGHT_ONION_PROBE_MAX_TIME); it stays above the CI step
timeout and never raises a budget
Roles without the opt-in run exactly as before; the onion probe script
with an empty lock behaves byte for byte like the previous one.
Tests: 126 role unit tests and 30 negative controls cover the gate
window, admission order, onion cap and lock, marker cleanup, the bounded
wait, dependency deferral, crash counting, artifact collection, the
ceiling for the real WordPress#0 app list, and serial equivalence. No CI
run has exercised it yet; the next push is its live proof.
🎯 Fix: 88 %
💥 Break: 6 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 83
🚦 Gate: ⛔ fix 88 % < 90, break 6 % > 5; committed and merged on the operator's word, CI is the live proof
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The realign task booted pgAdmin's whole Flask application on every deploy,
about 97 s on galaxyserver at cpus 0.2, even when nothing had drifted. A
stdlib pre-check now reads pgAdmin's SQLite configuration database
read-only and skips the boot only when exactly one active, unlocked
internal account with the Administrator role carries the declared e-mail
and its stored pbkdf2_sha512 hash recomputes from the declared password the
way Flask-Security 5.8.3 hashes it (NFKD, HMAC-SHA512 with the salt from
the keys table, libpass's round and salt limits). Every other state, an
unreadable or locked database, an unknown hash scheme, or a configuration
database moved out of SQLite by environment, falls through to the
unchanged create_app path.
The administrator password no longer reaches the host's /bin/sh argv
through the task environment: it arrives base64-encoded on the first stdin
line and is passed into the container by name only, so any byte sequence
survives and no fragment can reach stderr.
Tests: the pre-check agrees with converge against passlib; hash, role, key,
lock and duplicate-account drift, an over-long salt and a configuration
database override fall through; the rendered task runs through /bin/sh
with a password holding a newline, quotes, $, a backslash and unicode;
negative controls for each guard. The saving is not measured by a timed
deploy yet.
🎯 Fix: 90 %
💥 Break: 4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- perf/plugins/template-shallow-shared (f974ab3832, 90/3): template_shallow
moves from web-opt-rdr-domains to plugins/lookup, and the nine template
lookups of the design, dashboard, logout and matomo injectors render
through it instead of deep-copying every task variable per call
🎯 Fix: 90 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 87
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ansible-core's template lookup deep-copies every task variable on each
call. The role-local template_shallow of web-opt-rdr-domains, which hands
the builtin a top-level copy whose values stay shared, moves to
plugins/lookup so every role can use it. The nine template lookups of the
front injectors switch to it: six in sys-front-inj-design and one each in
sys-front-inj-dashboard, sys-front-inj-logout and sys-front-inj-matomo.
None of the switched templates or the lookups they call writes into the
task variables.
Tests:
- the plugin renders like the builtin, never deep-copies the caller's
values and leaves the caller's variables unchanged
- for every role with design sources, clearnet and onion vhosts, with and
without a logo, the design facts are identical to the builtin's, or it
fails with the identical error
- the dashboard, logout and matomo code renders identically
- the redirect loop of web-opt-rdr-domains still renders through the
shared plugin
The saving per deploy is not measured by a timed deploy yet.
🎯 Fix: 90 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 87
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- perf/ci/wordpress0-arm64-pass2 (7b412ffa76, 90/4): the Matrix
ansible-flavor Synapse vhost has one writer, so it no longer changes
and restarts OpenResty in every swarm pass (WordPress#0)
- fix/sys-dns-cloudflare-records/none-want-group (d96fe45650, 93/3): a
record set with an entry the converged check cannot judge stays
pending instead of flipping
🎯 Fix: 90-93 %
💥 Break: 3-4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86-90
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the ansible flavor the Synapse vhost had two writers: the shared
front-proxy include rendered proxy.conf.j2 for the Synapse domain, and a
second task rendered flavor/ansible/synapse.conf.j2 to the same file
right after it. The two contents differed, so the file changed on every
deploy and notified restart-openresty, which in swarm is a
service update --force of OpenResty in every pass (WordPress#0, fork run
41926377524).
proxy.conf.j2 now includes the ansible-flavor Synapse template for the
Synapse domain itself, and the second task is gone. The rendered vhost is
identical to the former final content across TLS, Element Call,
clearnet and onion, so the 8448 listener and the Matrix RTC locations
stay. A real change now goes through the front-proxy's reload, which
reaches OpenResty in swarm since 7e5da9422c.
🎯 Fix: 90 %
💥 Break: 4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fix/svc-prx-openresty/reload-mode-force-leak (7e5da9422c, 90/4):
OpenResty handlers notified from compose-forced roles (matrix, BBB)
resolve OpenResty's real address in swarm, so their reloads no longer
skip, and /logout inside their vhosts no longer points at 127.0.0.1
- perf/sys-dns-cloudflare-records/batch-apply: records the zone already
holds as declared skip the apply loop (59889bb5af, 92/3), and record
jobs are polled every second while collecting (f51ab74697, 92/3)
- perf/web-opt-rdr-domains/tls-resolve: redirect vhosts render through
template_shallow without deep-copying the variables (4a6bec9cf0, 92/3)
- perf/sys-front-inj-design/resolve-setfacts: each role design template
renders once per include (0d04336f93, 92/3)
The three tunes come from the galaxyserver and echoserver 2fd6c6d deploy
logs; all four passed independent skeptic review.
🎯 Fix: 90-92 %
💥 Break: 3-4 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 86-89
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collect step polled each async record job every 5 s, so a job that
finished right after a poll idled up to 5 s before its result was read.
The interval is now cloudflare_collect_interval (1 s), and the poll cap
derives from it, so the attempt's collect deadline, the retry split and
ignore_errors behave exactly as before.
🎯 Fix: 92 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ansible-core 2.20's template lookup deep-copies all task variables on
every call. The redirect loop renders two templates per source domain,
so on galaxyserver this copy dominated the resolve and render step.
A role-local lookup, template_shallow, hands the builtin template lookup
a top-level copy of the variables whose values stay shared with the
caller; nothing on the reachable template and lookup paths mutates them
in place. Rendered output is identical across 12 variants (TLS, Tor,
logout, self-signed, wildcard keep_path), per-item failures and
undefined-variable messages included. A real playbook with 65 items
went from 10.5 s to 5.8 s.
🎯 Fix: 92 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two Resolve tasks spent nearly all their time in the per-call
deep copy of ansible-core 2.20's template lookup, because every set_fact
value re-rendered the lazy task variables: the style include ran its
templates 6 times (10 with logo.css), the script include 6 times. On the
2fd6c6d deploys both together took 125 s on galaxyserver and 174 s on
echoserver.
The composed bodies now reach app.css.j2 and design.js.j2 through
template_vars, app_style_present tests the body directly (app.css.j2
always emits ":root {"), and the design check is inlined. Per include
the style lookups drop to 3 (5 with logo.css) and the script lookups to
4. The facts are byte-identical for 77 roles across clearnet, onion and
logo on or off, and for 36 edge cases, error messages included.
🎯 Fix: 92 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a production deploy every vhost of an app re-includes the design and
CDN roles. That repeated two things:
- sys-svc-cdn checked and skipped its two-task run-once block on each of
the ~50 repeat includes. The block is now one guarded include of
tasks/00_core.yml, the convention the other roles use, so a repeat
include skips one task instead of two.
- sys-front-inj-design wrote the same per-app style.css again for every
vhost of a network that shares it (logout, stalwart, matrix, bluesky)
and removed an already removed stylesheet once per vhost (the 11
WordPress vhosts). A per-run record of path and content hash, keyed by
application and the network's stylesheet file, now skips a write of
identical content and a repeat removal. Different content still
writes and the last write wins; each network keeps its own file; the
record is set only after the write or removal succeeded.
Measured from galaxyserver and echoserver 2fd6c6d: about 24 s saved per
galaxyserver deploy and 23-33 s per echoserver deploy (estimate until the
next deploy re-measures it). Two independent reviews found no
correctness or coverage hole.
🎯 Fix: 92 %
💥 Break: 3 %
🌱 Efficiency: –
⚡ Speed: –
🛡️ Security: ~100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, efficiency and speed <= 100, security 100 %
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
svc-runner started its stack with 'compose --chdir /opt/github-runner up'
and no --project, so the wrapper named the project after the directory,
github-runner, while every other role uses its entity name. Pass 2 of the
swarm drill in fork run 41744154601 (svc-runner#0) then stopped in the
pre-flight: network 'runner' belongs to compose project 'github-runner'.
The deploy now passes --project from entity_name, and a unit test renders
the real task, runs the real wrapper and checks the pre-flight accepts the
network it creates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The main merge placed main's rehash handler behind store-i18n's compose pull
and build handlers, so pull ran against stale hashed config names, and the
store-i18n-only lone recreate path (compose-service-up) never rehashed at all.
The handler now sits right after the instance-dir guard, as on main, and
listens on compose-service-up too. The test covers every handler that runs
compose up instead of assuming there is only one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_prefix_grouped_files flagged roles/web-app-funkwhale/files/python/same_origin/, whose three files all started with same_origin_; they were untracked while the lint ran in the objstore worktree. The sources are now settings.py, storage.py and urls.py, volumes.yml mounts them under the unchanged targets /app/same_origin_settings.py, /app/same_origin_storage.py and /app/same_origin_urls.py, so module names and DJANGO_SETTINGS_MODULE stay the same; the mirrored tests and the network_modes.md link follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setup ran add-ldap on every deploy under the name LDAP ({{ DOMAIN_PRIMARY }}); the name column compares case-sensitively, so a casing change created a second source (echoserver had LDAP (Infinito.Nexus) ID 1 with 331 users and LDAP (infinito.nexus) ID 3 with 12), and setup updated only the new one. Cleanup looked the source up with an awk regex in which the parentheses grouped, so it never matched. Setup and cleanup now find LDAP sources by their type (LDAP (via BindDN)), the source is named LDAP, and gitea_ldap_source_plan decides from the per-source user and external-login counts (files/sql/auth-source-users.sql through database_query, only when more than one source exists): keep the source users are linked to, else the lowest id, and delete the others; users on more than one source fail the deploy loudly and delete nothing. The bind password now travels on stdin, is read on the host, forwarded with container exec -e and expanded only by the shell inside the container, with no_log; it left GITEA_LDAP_AUTH_ARGS. On echoserver the 12 users of ID 3 were moved to ID 1 by hand first, so the next deploy keeps ID 1 and deletes ID 3.
Residual: the Gitea CLI takes the password only as --bind-password, so it sits in the argv of the in-container gitea process while add-ldap or update-ldap runs. The IAM line's 5f152d9f28 passes it through task environment: and keeps the name lookup; at the IAM merge take this side for the LDAP tasks and the GITEA_LDAP_* vars.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a long-lived instance the role's DOMAIN, ROOT_URL and SSH_DOMAIN variables never reach app.ini: the image's setup script writes them only when app.ini does not exist yet and applies only GITEA__section__KEY variables on later starts. echoserver's Gitea therefore still runs with ROOT_URL https://code.cymais.cloud/, sends that host as the OAuth2 callback, and Keycloak answers 'Invalid parameter: redirect_uri' (its log shows exactly https://code.cymais.cloud/user/oauth2/SSO%20Login%20%28INFINITO.NEXUS%29/callback). env.j2 now sets GITEA__server__DOMAIN, GITEA__server__ROOT_URL and GITEA__server__SSH_DOMAIN. The OAuth2 source is named 'SSO Login', because external_login_user.provider holds 25 characters and the 26-character domain-suffixed name would break every account link; setup and cleanup find the source by its type instead of by name, so the rename happens in place and no second button appears. OpenID 2.0 sign-in and sign-up are switched off. CI missed it because every CI run installs fresh and the Gitea specs never click the SSO button.
The IAM line's 144871ada1 carries the same rename and lookup plus a patched Gitea build for onion sign-in, but keeps the plain ROOT_URL variable; at the IAM merge keep these GITEA__server__ lines together with that patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decidim#0 and Funkwhale#0 of fork run 41744154601 failed on the tor rows because both apps handed the browser presigned or public URLs on the object-store host, which publishes no onion vhost. Decidim now sets STORAGE_CDN_HOST to its own origin and switches Active Storage to rails_storage_proxy; an initializer routes direct uploads through the disk-service token route (Rails writes them to S3 server-side), points the redirect and legacy blob routes and the rich-text BlobRenderer at the proxy, safelists storage reads in Rack::Attack, and refuses to boot when the patched activestorage 7.2.2.2 / decidim 0.31.6 signatures change. Funkwhale runs a settings module with a storage whose public names (attachments, sized images, dynamic preferences) resolve to MEDIA_URL and whose exports get a signed one-hour token link; a /media view checks every name against clean_name and rejects backslashes and percent signs, signs per request method and answers with X-Accel-Redirect; the front nginx routes those paths to the API, sends relative protected covers back through it and no longer forwards X-Forwarded-Host, which broke the presigned signature. Both apps reach the store over its internal URL; the public-host CSP entry, extra_hosts entry and CA bundle are gone. The Playwright specs assert the direct upload goes to the app and accept no object-store origin.
Verified by compose deploys of both roles, clearnet update and tor-only reinstall, failed=0 with green Playwright and no object-store host in the rendered pages or APIs; swarm and Decidim variant 1 (disk storage) are not deployed yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WORDPRESS_OPTIONS now merges the role's own contributions (admin_email from the platform administrator, blogname from the design title while the design service is on, the freedomtranslate-wp config.options while that addon is on) under services.wordpress.options, so the inventory still wins and wordpress_options_checked guards the merged map. design.php no longer writes blogname, the addon task no longer writes its options, and WORDPRESS_OPTIONS_OWNED_ELSEWHERE drops the three. Every deploy now converges admin_email, and blogname while the design is on; a write WP-CLI reports as unchanged no longer counts as a change, and the FreedomTranslate readback runs after the options write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>