Swarm names containers <stack>_<svc>.<slot>.<taskid> and does no
prefix matching, so the bare services.<key>.name never resolves. The
tor-disabled variant of the E2E CLI test execs into the DB container
by that bare name and burned all 20 attempts on mariadb, postgres and
openldap in the swarm lane.
Route DB_CONTAINER through the existing *_CONTAINER_ADDRESS SPOT. The
container_address lookup returns the bare name in compose (behaviour
unchanged) and a $(resolve-container-id ...) fragment in swarm, which
expands because run_one.yml sources test.env rather than parsing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both ran as soon as the mirror was ready, in parallel with lint and test, so a lint error still cost a full dev-runtime matrix at a 345 minute timeout. With lint and test in their needs, GitHub skips them when either fails - no if: is required because neither job carries always().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were created without the executable bit, which every sibling under scripts/ carries and which tests/lint/filesystem/shell/test_executable.py enforces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The link targets were updated with the rename, the link texts were not, so four places advertised a workflow file that no longer exists. No lint catches this: the path resolves, only the word beside it is wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ruff PERF401 rejected the append-in-loop and failed make quality at autoformat, before any test ran. The generator keeps order, filter and output identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without the bindings the ansible apt module installs them itself mid-play, and the dist-upgrade that follows deadlocks: the module spins in user code while apt-get blocks writing to a full stderr pipe. It ran that way on all five nodes for three hours.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow directory mixed entry points, reusable stages and scheduled jobs into one flat list sorted by topic, so the first question about any file - what starts it - was the one the name did not answer. Every workflow now carries one of three prefixes, derived from its on: block: cron- for a schedule trigger, call- for one only reachable through workflow_call, entry- for everything a repository event or a human starts. Scheduled workflows additionally carry an alarm clock as the first character of their name.
Two lints keep it that way. One derives the expected prefix from the triggers and compares it against the file name; it flagged all 17 offenders before the rename and none after. The other resolves every .github/workflows/<file> string in the repository against the directory, which is what a rename silently breaks in Markdown links, Python constants and shell API URLs alike.
Renaming pulled 69 pre-existing comments into the comment lint's changed-file scope. The vendored CodeQL template got the nocheck marker its scorecard sibling already carried, two workflow headers moved above name: where they count as file headers, and the remaining banners were deleted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A run cancelled by a concurrency group loses the work it had already finished. An hourly sweep now walks every branch, and when its latest run ended cancelled it restarts only the failed jobs through gh run rerun --failed, keeping the successful ones.
CI_RERUN_INTERVAL_MINUTES, CI_RERUN_MAX_ATTEMPTS and CI_RERUN_MAX_AGE_HOURS tune the sweep. A cron expression cannot read a repository variable, so the workflow is triggered every full hour and a gate lets only the hours through that open the configured window; the gate reads the hour, not the minute, so a late start still counts for its hour. An unset attempt ceiling means no ceiling, bounded only by the 50 reruns GitHub allows per run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Workspace, instructions and the compose, host and swarm deploy matrices now carry a job-level concurrency group, so one entry at a time runs repository-wide instead of per branch. Job level is required: only there does the matrix context exist. The deploy key joins apps and variant_slug with '#' because both fields contain hyphens themselves and a hyphen would make the pair ambiguous; apps alone is not unique, swarm expands one entry per variant and compose splits large roles into bundles. queue: max lifts the pending slot from one to a hundred, so a third concurrent run queues instead of cancelling the waiting one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitHub shipped the concurrency queue property on 2026-05-07. actionlint 1.7.12 and act v0.2.89 both predate it: actionlint reports an unexpected key, act refuses to parse the whole workflow. Ignore the finding in the lint call, and hand act a copy with those lines stripped so local iteration survives once the workflows use the key.
Refs: rhysd/actionlint#654, nektos/act#6095, nektos/act#6152
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The handler carried failed_when: false while the rescue include below waited on 'when: result is failed'. failed_when: false sets failed to False unconditionally, so the condition could never hold and the four diagnostic tasks behind it had never run once. A genuinely failed SSH restart was logged as ok: and the deploy continued. Measured with the project's own ansible: failed_when_false.is_failed=False, ignore_errors.is_failed=True.
The terminal fail is now tied to the cause instead of firing on every failure. It aborts only when sshd -t rejects the config, i.e. when the fault is ours; a transient restart failure with a valid config yields full diagnostics and the play continues. The gate uses 'is defined' rather than a default so an unregistered result cannot raise.
The config path becomes a role constant. The path the template writes and the path sshd -t validates are the same file, and whether a run aborts follows from that identity - if the two spellings drift, the run aborts for the wrong reason. Verified that the constant resolves two include levels deep inside the handler.
The runtimes report is unaffected: its parser downgrades a fatal line back to executed when '...ignoring' follows, which is exactly this case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ansible renders a task name before the loop runs, so item is not in scope. Both NFS-prep names referenced it anyway, which produced an 'Encountered 1 template error' warning and a task name reading '<< error 1 - object of type str has no attribute key >>' twice per app in every swarm job.
The per-item value belongs in loop_control.label, where Ansible can resolve it. The Materialize task already had one and only needed a name without item. The Pre-create task had no label at all, so its log line carried the whole volume mapping; it now labels with the volume key. Both names carry application_id instead, which the name context does provide.
Net effect: the warning disappears and the log gets shorter. A permanent, harmless warning is worse than noise - it trains readers to skip the next one, which may not be harmless.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_ram_gb reimplemented the /proc/meminfo parse that utils/env/runtime.py owns: same pseudo-file, same MemTotal scan, same line.split()[1], same swallow-and-fall-back guard. It now calls mem_total_mb().
The error semantics map exactly. The helper returns 0 for an unreadable file, a malformed line and a missing field; all three previously produced 4.0, and 0 is falsy, so the conservative fallback still covers every one of them.
One real delta: the helper floors kB to MB before the GB conversion, so _ram_gb loses up to 0.001 GB. That can only move the worker count where MemTotal/2 sits within half a megabyte of an exact 1.5 GiB multiple, and it moves it downwards, which is the direction this sizing already prefers.
Touching the file pulled its ten pre-existing narration comments into the diff-scoped comment lint. They were deleted, not relabelled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EnvBuilder.setdefault already reads os.environ.get(key).strip() and prefers it over the computed value. Three handlers repeated that check in front of eb.set: four dead lines each plus an import, restating a contract that lives one module over. If setdefault ever changes, the three copies do not follow.
Behaviour is identical and was measured on both paths: each handler yields the same derived value as before, and with os.environ[KEY] set the override still wins. The only change in flow is that an overridden value is now computed and discarded, costing one df or /proc read per make dotenv.
The host-path guard in registry_cache_max_size stays: it prevents a df on an empty path, which is a different concern from the override.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The five DinD lab nodes run privileged with cgroupns=host, a writable host cgroupfs and seccomp/apparmor off, and carried no resource limit of any kind. A runaway node therefore competes with the Actions runner agent itself, and the platform reports that only as 'lost communication', never as an OOM.
Only pids_limit for now. A memory cap needs a measured per-node peak, and every measurement available is a failure-time snapshot, i.e. a lower bound: 4.4 GiB on the busiest node of a variant-0 drill. Sizing the cap off that risks OOM-killing a legitimate container instead of the runaway node. The pid ceiling has no such problem: 8192 against a measured 981 is 8.3x.
The value is a static default.env key on purpose. A handler-derived key is absent from .env on a fresh runner, and the topology export step runs before the install step that generates it, so compose interpolation would have failed every swarm job before anything deployed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When the host runs out of memory the Actions runner loses its heartbeat. GitHub then reports only 'lost communication': no step completes, so every if-always upload step is skipped and the rescue diagnostics, the inventory artifact and the Playwright results are all lost. Job 92710765494 died exactly that way.
The 30s poll in _run already did this for disk. It now also samples MemAvailable, MemTotal and the memory PSI stall, prints one line per poll, and aborts below 6 percent of RAM. The abort returns 75, which propagates to the EXIT trap in 00_one.sh, so the existing post-mortem apparatus runs normally instead of dying with the runner.
The sample line is the main point: artifacts provably do not survive a lost runner, streamed log lines largely do, and the numbers discriminate memory thrash from a kernel wedge on the next occurrence. The floor is relative to MemTotal so a dev box does not inherit a runner-sized threshold; measured margin across four real drills is 9-15x. PSI is sampled but not used as a trigger, since no drill has ever measured it.
mem_total_mb and mem_stall_pct join the existing host probes in utils/env/runtime.py and share the /proc/meminfo parser. The probes read only /proc, never the host-side NFS mount, which would block in D-state under the very condition they exist to catch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
compose_replicas defaults to len(groups[application_id]), so the swarm deploy ran 3 semaphore replicas. The stock entrypoint runs 'semaphore setup' plus db.Migrate on every container start with no advisory lock, so all three raced the migration chain on a fresh DB. The loser hit migrations_pkey on version 2.3.1, and its rollback path deleted the winner's ledger row without undoing the schema. Every later start then replayed v2.3.1 against the newer session table and died on 42601, so the stack never converged.
Upstream OSS is single-instance regardless: the HA node registry, schedule deduplicator and orphan cleaner are pro_impl stubs returning nil.
Seen in CI run 31128575683, job 92710765438.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without the bindings the ansible apt module installs them itself mid-play, and the dist-upgrade that follows deadlocks: the module spins in user code while apt-get blocks writing to a full stderr pipe. It ran that way on all five nodes for three hours.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The role was the only web app that pinned to a manager and still disabled swarm; 28 sibling roles run that exact combination. The comment that gated it named a CI-chaining risk and a condition that compose has since met, so it went with the flag.
The node-local SQLite volume stays behind placement: manager. A reschedule across managers would meet an empty volume, which is a platform-level property the siblings share, not a reason to single this role out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make docs picks up the alpha lifecycle for magento and socialhome, drops the swarm marker from the socialhome graph since the role disables that mode, and adds its optional coturn edge; autoformat modernises the listmonk autologin loop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The walk excluded only .git and node_modules and ignores .gitignore, so it read agent session artifacts such as .claude/loop.md, which the sandbox denies and which exist only while a run is in flight.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
IncompleteRead subclasses HTTPException, not OSError, so it slipped past the transport-error handlers the fetchers already had and propagated out of the thread pool, turning a warn-only image-version test into a hard failure.
The affected image is now skipped rather than checked, which the run does not surface: the test's unchecked list keys on registry support, not on fetch success.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The env-file lint requires ascending keys, and PERSONA_ADMINISTRATOR_BLOCKED was appended after PERSONA_BIBER_BLOCKED when the persona contract landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every conflicted hunk resolves onto the swarm-nfs side, because on all six
files it is the strict superset: it carries the display-name codec
(utils/roles/display.py) the CI role selections now encode through, and the
sequencing glyph move from 910748ec4. svc-net-tor holds no unique content on
any of them -- runs.py even had non-conflicted hunks already calling `codec`,
so keeping its side would have left a NameError.
The lint fallout is settled in the same commit, as 4afc9b770 did for the
previous merge: the shell-or-true lint lives on svc-net-tor (c505ce746) and
swarm-nfs brings two unmarked `|| true` sites with it. Both get a nocheck
reason rather than a rewrite, because neither swallows anything -- the
slaptest call is the measured exception of c4bbbba93 and has_cn_config decides
right below it, and the dockerd restart is followed by an is-active poll that
fatals. In both the exit code was never the authority.
make quality green end to end.
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>
The 5GB budget covers the 4 GiB media cap that vars/main.yml pins through SH_MAX_STORAGE_BYTES, plus the SQLite database, which the headroom lint cannot infer from the image size alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both values are computed by env handlers and land in .env, but the service's environment block is an allowlist, so they never reached the container. rerun-spec.sh aborts without them and ci/dedicated.sh needs the reports path to resolve inside the container it copies from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Magento's admin form names its inputs as a form array (login[username], login[password]), which none of the exact name matches covered, so the persona typed into nothing and the native login failed. A suffix match covers array-style names generally, as used by Zend derivatives, Rails and Symfony.
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>
compose_replicas defaults to len(groups[application_id]), so the swarm deploy ran 3 semaphore replicas. The stock entrypoint runs 'semaphore setup' plus db.Migrate on every container start with no advisory lock, so all three raced the migration chain on a fresh DB. The loser hit migrations_pkey on version 2.3.1, and its rollback path deleted the winner's ledger row without undoing the schema. Every later start then replayed v2.3.1 against the newer session table and died on 42601, so the stack never converged.
Upstream OSS is single-instance regardless: the HA node registry, schedule deduplicator and orphan cleaner are pro_impl stubs returning nil.
Seen in CI run 31128575683, job 92710765438.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>