A looped include that leaves its variable as item inherits whatever a
loop inside the included file binds, so the redirect loop now names
it and the include reads that name. Nothing in the role referred to
item, so the rename reaches every read.
The variants test asked the environment for TMPDIR and substituted
/tmp when it was unset, which is the silent fallback the project
forbids; tempfile.gettempdir() already knows the order to try.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_variants handed out a deep copy on every call so one caller's
mutation could not reach the next. services_overrides_for_round asks
for the map once per round, and a sweep resolves hundreds of rounds, so
the matrix planner paid that copy 460 times: 78 million deepcopy calls,
and get_variants alone accounting for 82 of the 163 seconds a profiled
build took.
The copy is now opt-out for that one caller. It reads the map and never
writes to it, resolve_pins takes it as a Mapping and has no mutation
site, and _deep_merge deep-copies both of its arguments, so nothing the
round produces keeps a reference into the cache. Fifteen other callers
keep the copy untouched.
Measured end to end on cli.meta.ci.playwright_coverage: 33.7s to 24.6s.
Equivalence is measured against the copying path for two fixed rounds,
291 roles each, rather than against a sweep -- two identical planner
runs assign different modes to some rows, so a plan diff cannot carry
that proof.
The test asserts identity rather than runtime, as the comparable
optimization in 0cb8bae88d did: a wall-clock bound on this machine is
the same flakiness the sweep already shows.
Co-Authored-By: Claude Opus 5 (1M context) <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>