The CI rotation gate moves every application credential between the two deploy passes. MariaDB takes the new value, but both apps baked the old one into a persisted config at install time and their entrypoints refuse to rewrite it once the install flag exists, so every request dies with 'Access denied for user' - a 500 under compose, and under swarm a healthcheck reap that swarm restarts forever.
suitecrm gets the tasks/utils/patch/db_credentials.yml hook that sys-svc-rdbms already reaches for on every deploy, patching db_password in public/legacy/config.php. magento cannot use that hook - app/etc/env.php may carry more than one 'password' line and the shared script is a line-wise sed - so it calls its own supported writer, bin/magento setup:config:set, unguarded after the creates:-guarded install.
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>
The container docroot was spelled out in four shell invocations and twice in the nginx vhost, where both sides must agree or nginx serves an empty server block — the 403 this role hit. A single MAGENTO_ROOT now feeds both, and the install guard's mountpoint expression moves next to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Variant 0 had never run before: it is the only variant with sso and remote storage enabled, and every layer on that path was broken.
setup:install passed --remote-storage-path-style, but ConfigOptionsList writes remote_storage/config/path-style while AwsS3Factory reads path_style. The hyphenated key is dead, so the SDK addressed the bucket virtual-hosted-style and resolved magento.seaweedfs, which has no DNS record. files/remote-storage.php writes the key the factory actually reads, after the install; a PUT of storage.flag through Magento's own AWS SDK confirms path-style reaches the gateway.
The install guard named app/etc/env.php, which setup:install writes long before it finishes, so a failed install skipped the install forever after. It now guards on a marker the task writes only on success, and clears a stale env.php on the path where no install ever completed.
nginx includes the vhost as a glob, so on a fresh volume it starts before composer creates the file and serves an empty server block. The reload was gated on the vhost file changing, which is false when a previous run already wrote it; it now also fires when the install ran.
Magento ships no OIDC, so the oauth2-proxy round trip authenticates at the edge without creating a Magento session and no in-app logout exists. Both personas are declared blocked accordingly, biber unconditionally and the administrator under sso.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role could never serve. 01_setup.yml copied nginx.conf.sample inside the
bootstrap shell, but magento/project-community-edition 2.4.8's dist archive is
2583 bytes and holds exactly one file, composer.json - nginx.conf.sample ships
in magento2-base and only lands once composer install runs. Executed, the
bootstrap's own shell says: cp: cannot stat 'nginx.conf.sample'.
set -e made that fatal, and the block's creates: guard made it permanent: it
points at html/composer.json, which create-project had already written, so
every rerun skipped the block, nginx.conf never appeared, the glob include
matched nothing and / returned 403 forever.
This is a regression from the create-project --no-install split: the cp stayed
behind in a step that no longer downloads the package that carries the file.
It now runs after the install with its own creates: on html/nginx.conf, which
also heals volumes the previous code already wedged.
The rest of the topology was proven sound in the same investigation - real
images, real nginx.conf.sample, HTTP/1.1 200 OK through php:9000, and a clean
nginx -t on an empty volume - so nothing else about the vhost changes.
Also here, all found while proving the above:
- placement: manager. 01_setup.yml bootstraps through container exec gated on
IS_STACK_HOST, but the role was not manager-pinned, so in swarm the manager
bootstraps while php and nginx run on a worker roughly two times in three:
volume inspect finds no volume and resolve-container-id returns an ID that
lives on another node. 30 roles already pin this; magento was the outlier.
- --base-url without the trailing slash. lookup('tls', …, 'url.base') already
ends in one (tls.py:89), so every stored link carried // and the session
cookie path became //.
- composer install --no-dev. The run was locking 622 packages including
codeception, allure and phpmd.
- Both nginx restarts now gate on the vhost task actually having changed;
changed_when: rc == 0 made them fire on every single deploy.
- fastcgi buffers on the server block. Magento's sample sets them only on the
main entry point, so /setup/ and /update/ fell back to nginx's 4k default.
- Nine of the ten variants-static-override nochecks were inert - the base
values they claim to override are Jinja, not literals - and their pasted
justification named backup shared on services that have nothing to do with
backup. Only container_backup.shared is load-bearing.
make test green across all four targets. test-lint caught one defect while
this was being written: the new task first used ansible.builtin.command, which
passes the container_address $(...) through literally instead of evaluating
it - the same swarm failure mode the placement fix addresses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five independent blockers, none of which the role could get past.
1. Credentials. create-project pulled the metapackage from repo.magento.com,
which needs Adobe Marketplace keys; without them the bootstrap got HTTP 401
after 40 minutes of stack build. meta/schema.yml declared both keys with
validation ^.*$, so an empty value passed inventory validation silently.
mirror.mage-os.org serves the same packages unauthenticated and hosts the
dists itself - verified on the deploy's own network path, HTTP 200 for
project-community-edition-2.4.8.zip. Repository, package and version now
live in meta/services.yml, which also makes a later switch to the Mage-OS
distribution proper a data change rather than a code change.
2. Premature handler flush. The config renders notified 'compose up', so the
handler chain ran before sys-stk-full had rendered compose.yml and the CA
handler died on the missing file. Worse, the render had already created the
instance directory, so the repo's own guard for exactly this
(Fail: instance dir missing at handler flush) no longer tripped.
3. Anonymous volumes. Both markoshust images declare VOLUME /var/www while
the named volume mounted one level deeper at /var/www/html, so docker created
an anonymous volume per container and sys-ctl-hlth-volumes refused to start.
The mount moves to /var/www, where the image declares it; the image ships an
empty html/ that a fresh named volume initialises from.
4. Unescaped expansions. setup:install passed --db-host=\"$MYSQL_HOST\" and
friends, but only the quotes were escaped for the outer shell, not the
dollars. The DiD shell expanded them to empty, Magento fell back to localhost,
took that as a unix socket and reported SQLSTATE[HY000] [2002]. 18 expansions.
5. A non-resumable bootstrap. create-project needs an empty target and cannot
be retried, yet it carried the dependency download - and mirror.mage-os.org
runs nginx with the default http2_max_requests=1000, so composer's ~490
requests on one connection hit GOAWAY mid-transfer at the same package twice
in a row (curl error 18, not the flake it looked like). It is now
create-project --no-install plus a retryable composer install, which resumes
from the cache; measured succeeding in the live container after the abort.
The creates: guards follow: composer.json marks the skeleton, bin/magento the
finished install - it only exists once dependencies are in.
MAGENTO_COMPOSER_SPEC folds package and version because autoformat broke the
create-project line inside a Jinja expression; it rendered correctly by luck.
Not fixed here: the base URL renders with a double slash, and the lifecycle
stays pre-alpha - the role reaches setup:install now but has not yet served
a page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role could not deliver anything. The markoshust nginx image ships a
default vhost built for a different topology: fastcgi points at
unix:/sock/phpfpm.sock, port 8000 only returns 301 to https, the real
server sits on 8443 with a self-signed cert, and it references an upstream
host 'phpfpm' that does not exist here - so nginx aborted at config load
with 'host not found in upstream' and the container never started.
php-fpm listened on that same unix socket, because its
/usr/local/etc/php-fpm.conf carries no include line and php-fpm.d/*.conf
is therefore dead config.
Both are now replaced through type: config mounts. The role never had any
- that absence is why every template it previously rendered was inert.
The vhost stays minimal and includes Magento's own nginx.conf, copied from
nginx.conf.sample by the bootstrap. Its location blocks carry the .php
whitelist, the /static/version rewrites and the /media to get.php fallback
that a hand-written vhost has to reimplement - the previous attempt did
not, and would have executed every .php under pub/.
PHP moves from 8.5 to 8.4: Magento 2.4.8 requires ~8.2.0||~8.3.0||~8.4.0
and the bootstrap passes no platform-requirement override, so
composer create-project failed before nginx ever mattered.
nginx waits for the php socket before starting, for the reason given in
the erpnext frontend commit: the wildcard search domain turns a DNS miss
into a routable wrong answer, and nginx pins upstreams at config load.
Also: the oauth2 origin port named 8080, where nothing listens, and the
healthcheck proved only that some listener existed - it stayed green on a
redirect loop and on 502.
Measured in a rebuild of the role topology against Magento's real
nginx.conf.sample: HTTP 200 on / and /index.php, healthy healthcheck, and
the counter-test - without the gate and with php down, nginx exits 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
01_runtime_conf.yml rendered a nginx.conf and a php-fpm pool override that
never reached a container: no task included the file, and meta/volumes.yml
declares no mount for either output. Verified against HEAD in a second
checkout - both templates were already dead there, so removing them changes
runtime behaviour by exactly nothing.
Their four path variables go with them, as do MAGENTO_PHP_HOST,
MAGENTO_PHP_PORT and MAGENTO_NGINX_PORT, whose only consumers were the
assert and the two templates.
Closing the numbering gap renames 02_setup.yml to 01_setup.yml, the name it
could not take while 01_runtime_conf.yml existed.
This does not make the role work - it does not work today either. The
markoshust nginx image cannot even start under this topology
(host not found in upstream "phpfpm"), and its php-fpm listens on a unix
socket rather than TCP 9000. Both are addressed separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>