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>
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>