A selection token may pin the mesh state as &vpn or &direct, but check_pins
had no arm for it. pinned_vpn_states intersects the pin with the states the
mode allows and only swarm meshes, so a token like @compose+clearnet&vpn left
an empty state list: the row produced no job at all and the run reported green
for a combination that never deployed. That is the outcome _reject exists to
prevent, and every sibling axis already refuses an impossible pin.
The new arm asks whether any candidate mode keeps a state rather than whether
the pin is reachable, so a held vpn axis still overrides the pin instead of
being refused. That behaviour has its own test and is unchanged.
An earlier check was removed with the compose-mesh revert of 2026-10-16
because it rested on meshable() and smallest_variant(), which that revert
deleted. This one reuses pinned_vpn_states, applies to the swarm-only axis the
revert restored, and covers both mesh states instead of only &vpn.
pin_vpn and vpn_mode now reach check_pins from all three callers. The run's
vpn axis reaches the validation job through call-orchestrator and
call-validation, which passed every other axis but this one. The priority
procedure and its skill state all eight axes a token carries, not six:
architecture and mesh were pinnable and, for architecture, already validated,
yet neither was documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main rejects a moving runner label since 52ad8549e0, because ubuntu-latest moved to 26.04 in the middle of run 39166850273 and every job that landed on the new image met a different OS. These two workflows were written on this branch and still carried ubuntu-latest, so the branch broke its own target's rule.
The updater of b055eece05 did not catch them: it raises a bare runs-on: ubuntu-NN.NN and leaves everything else alone, and ubuntu-latest is neither that nor an arch-suffixed label. Pinning them hands them to the updater from here on.
One place stays outside its reach: the architecture ternary of call-images-build-ci.yml holds the label inside an expression, where the updater's runs-on: prefix does not match, so that pin has to be raised by hand until the pattern covers it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A deploy job name repeats the category path on every row of the matrix. One Egyptian hieroglyph costs four UTF-8 bytes where the two Chinese characters it replaces cost six. Unicode names each sign by its Gardiner code, so the opaque value stays identifiable without a comment per entry.
A label has to stay one codepoint: a sign written with a variation selector costs seven bytes and is worse than what it replaced. tests/integration/meta/categories/test_glyph.py enforces that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ubuntu-26.04 runner images load per-binary AppArmor profiles (wg-quick,
dig, who, fusermount3, ~150 upstream). Path-based attachment applies to
every unconfined task, so the profiles also attach inside our privileged
`apparmor=unconfined` containers. The upstream wg-quick profile only allows
`/usr/bin/ip`; on CentOS and Fedora nodes `ip` lives at `/usr/sbin/ip`, so
`svc-net-wireguard : Bring 'data' up` failed with rc=126:
/usr/bin/wg-quick: line 32: /usr/sbin/ip: Permission denied
apparmor="DENIED" operation="exec" profile="wg-quick" name="/usr/sbin/ip"
(run 39268090162, Fediwall#0, CentOS swarm VPN).
Dev hosts never see this: `environment-bootstrap` runs
`security-apparmor-teardown`. The runners skipped that preparation. The
deploy, workspace and DNS workflows now run the same target on the runner
host before any container starts (skipped under act, whose job container
cannot reach the host's policy), so CI matches the dev-host state for all
profiles, not only the ones seen so far.
A dedicated permissive profile for the containers was rejected: every
AppArmor host without that profile loaded would refuse to start them,
including dev hosts and act.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The workflows pin `runs-on: ubuntu-NN.NN`, so no job moves to a new Ubuntu
release behind our back, but nothing raised the pin either. A sixth job of
`🔄 Update versions` closes that: `utils/update/runner.py` reads the image
table of actions/runner-images, takes the newest x64 Ubuntu label whose
image carries no preview or beta badge, and raises every older bare
`runs-on: ubuntu-NN.NN` to it; arch-suffixed labels stay untouched. The job
opens the change as one PR over `.github/workflows`, so its CI run tests
the new image before it reaches main.
This commit carries the updater's first result: every job now runs on
ubuntu-26.04 (49 `runs-on`).
actionlint 1.7.12, the newest release, does not know `ubuntu-26.04` yet
(rhysd/actionlint#682, fix proposed in rhysd/actionlint#743), so
`.github/actionlint.yaml` declares the label until a release knows it.
act maps each `ubuntu-NN.NN` label the workflows use to the local platform
image, derived from `.github/workflows`, instead of a fixed list that did
not know ubuntu-26.04.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`ubuntu-latest` moved from ubuntu-24.04 to ubuntu-26.04 on its own
schedule, in the middle of run 39166850273, and every job that landed on
the new image met a different OS: sudo-rs instead of sudo, no ip_tables
loaded, and now the NFS probe-write of svc-storage-nfs-client failing on
26.04 swarm nodes. The same branch went red without a code change.
Every `runs-on` now names `ubuntu-24.04`. GitHub still rebuilds that image
weekly, so patches and tool updates keep arriving; only the release jump
becomes a deliberate change of the label. The CodeQL job drops its dead
swift/macos-latest branch, since no swift language is in its matrix. act
already maps ubuntu-24.04 to the local platform image.
A lint rejects any job whose `runs-on` contains a `*-latest` label.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 16.0.0 entry pkgmgr wrote on top of CHANGELOG.md uses "-" bullets
while every older entry uses "*". MD004's default "consistent" style takes
the marker of the first list in a file, so lint-markdown reported 400
MD004 findings in CHANGELOG.md and the pre-push gate refused the release
push.
MD004 is now pinned to "dash", the marker pkgmgr and the changelog skill
write, and make autoformat (markdownlint-cli2 --fix) converted the lists
of the 72 files that still used "*". Only list markers changed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sys-front-inj-design now renders templates/css/app.css.j2 ahead of the
role's stylesheet and templates/js/design.js.j2 ahead of its script. The
branding URLs, the slot dimensions and the logo guard live there instead of
being repeated per role: the guard that stood in 41 sheets is one condition
in the composing template, and a sheet reads branding through
--design-url-* / --design-slot-* custom properties.
29 stylesheets and 43 design scripts lose their Jinja and move to static
files. Roles whose sheet carries app-specific URLs declare them as
design_css_urls and read them as --design-url-app-*.
The script prelude is prepended only when the role script references it,
and it carries urls only while a logo is configured, so every role that
needs neither is emitted byte for byte as before.
Five checks keep the mechanism honest: a composed sheet may only read
design custom properties that app.css.j2 declares for its role, a template
may not carry Jinja expression text outside its delimiters, a design script
may not quote its config object, and the injected-script parse check now
covers the design channel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workspace jobs pulled every image anonymously from Docker Hub and
two of them failed on its rate limit in run 39059128678 (#990): the
fedora swarm drill on busybox:1.38 on all three nodes, the centos
compose drill while building tor-smtp_custom. The regular deploy jobs
pull through the GHCR mirror, but the mirror was tied to being a real
GitHub runner: Profile.image_mirror_enabled() returned
runs_on_github(), and the swarm routine generated mirrors.yml only when
cli.meta.runtime reported github, which is act inside make
swarm-zombie.
The mirror is now its own capability, INFINITO_GHCR_MIRROR (next to
INFINITO_GHCR_MIRROR_PREFIX), written as a run fact like
INFINITO_CACHE_STACK: true on a real GitHub runner,
false everywhere else, and a non-empty exported value wins for the run.
Profile.image_mirror_enabled() reads the declaration and keeps
runs_on_github() as the fallback, and 02_provision_inventory.sh gates
on the same value instead of the runtime name. make swarm-zombie
forwards it into act through ACT_ENV, since act ignores the host env.
call-test-workspace.yml declares INFINITO_GHCR_MIRROR=true and passes
INFINITO_GHCR_MIRROR_PREFIX into the workspace container. Nothing else
of the workspace changes: is_ci, runs_on_github, the cache stack and
the docker root stay local-like. The step that loads the rescue paths
now removes the .env it generated on the runner, because on debian
rows load.sh sourced it inside the container and the run inherited
INFINITO_RUNNING_ON_GITHUB=true.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two jobs of run 39040406916 died with the runner's own annotation, 'The
hosted runner lost communication with the server'. Both left every
diagnostic step pending, so no artifact exists for either. For the swarm
one the in-band sampler ruled starvation out — 9324M of 15947M available,
stall60 0.0%, 25 seconds before the cut — and nothing in the repository
can tell the remaining causes apart.
A stuck NFS mount wedges tasks in uninterruptible D state, which shows as
I/O pressure while memory pressure stays flat, and this drill runs its own
NFS node. /proc/pressure/io and /proc/pressure/cpu were read nowhere in
the repository. They are now sampled beside memory, with the D-state count,
every ten seconds instead of thirty, and an I/O stall at or above half the
minute aborts the step while the runner is still alive to upload the
banner.
cpu reads 'some', not 'full': the full line is pinned at 0.00 system-wide,
measured 13.50 against 0.00 on this host, so reading it like memory would
have shipped a metric that can never move.
Dump runner state was switched off for swarm, so the one post-mortem that
survives a step failure never ran there. It runs now, and the swarm drill
gets the step timeout its compose sibling has had all along — 350, the
number test_distro_budget_fits_timeout already derives the distro budget
from.
The sampler moved to its own module: the additions took matrix.py past the
file-size cap, and the lint asks for a split rather than a marker.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OpenSSF Scorecard reports no fuzzer integration (code scanning alert
507, issue #853). tests/fuzz/_yaml_bootstrap.py is an atheris harness
that feeds arbitrary input to utils.yaml_bootstrap.load_block and fails
whenever the reader accepts a document that load_yaml_str parses
differently or rejects. That is the contract the module's docstring
states, and the existing property test only samples it. The leading
underscore keeps unittest discovery from importing it, because atheris
is only installed with the fuzz extra.
cron-security-fuzz.yml runs the harness for 15 minutes every Sunday and
on demand, on ubuntu-latest, and uploads crashing inputs as the
fuzz-crashes artifact. atheris 3.1.0 ships x86_64 manylinux wheels for
CPython 3.12 to 3.14 only, so it lives in its own `fuzz` extra and
stays out of every other install path. tests/fuzz/ is not part of
`make test`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four of the five workspace matrix legs boot from an upstream Docker Hub base
(`manjarolinux/base`, `debian:13`, `ubuntu:latest`, `fedora:latest` in
`meta/distros.yml`; only centos comes from quay.io). The runner pulls those
itself, so the GHCR mirror does not cover them: its allowlist is derived from
role images. That leaves the job on Docker Hub's anonymous rate limit.
The reusable workflow now declares both credentials as optional secrets,
exposes them as job env and logs in before the image is used.
Without the secrets the expressions render empty, the step's `if` is false and
it is skipped, so the job keeps pulling anonymously rather than failing. The
second half of the condition skips the step on pull requests from a fork,
where GitHub passes no secrets at all; that is redundant against the emptiness
check today and becomes load-bearing if the trigger ever moves to
`pull_request_target`, since `docker/login-action` fails hard on empty
credentials.
The action pin matches the twelve other uses in this repository, and the
`env.X != ''` guard is the shape `cron-images-mirror-all.yml` and
`call-images-mirror-missing.yml` already use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A value written to `$GITHUB_ENV` reaches the environment of the following
step, not the one writing it. The distro step ran after the step that calls
`make dotenv`, so the generated `.env` never saw `INFINITO_DISTRO` and fell
back to `default.env`'s `debian`. The generator derives the compose service
container name from it (`default.env:142`), so the arch and centos matrix legs
addressed a container name their image does not use.
Nothing failed: a default existed, so the job carried a wrong value instead of
an error, and a written `.env` keeps it.
The step now runs directly after the checkout, before the rescue-path step
that calls `make dotenv`. It depends only on `DEV_RUNTIME_IMAGE`, which the
matrix fixes before the job starts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With mode swarm and vpn enforced it deploys an example's declarations in CI.
The manual entry already exists on the default branch, so the input can be
dispatched before this lands; a new entry workflow could not.
Forwarded to the swarm drill as INFINITO_SWARM_MESHES; empty keeps the
development lab's.
The deploy job also installs iproute2, which the harness uses to find the
controller's stale mesh links. GitHub runners already ship it; the act image
a local run uses does not.
Twelve files conflicted; most resolutions keep both sides rather than pick
one.
cli env.py and compose.py: both branches extend the same spots. The tools
override is appended before write_override, because that call filters the
mirror override to the compose files it is handed. compose.py keeps this
branch's CACHE_ROLE_SHELL_DIR, since the cache scripts moved into a role and
main's literal scripts/docker/cache path is their old home, and takes main's
mirrored init image alongside it.
utils/docker/image/discovery.py: this branch's version_variants expansion
plus main's whole runner-image block, which runner_mirror imports.
sys-svc-compose network create: main's side wins on substance and this
branch's on structure. main carries eda6249d7b, the fix for a probe that
matched an id prefix and let a sibling shadow the exact name; this branch had
refactored the task into create/overlay.yml and create/bridge.yml but brought
back the docker_network_info probe that fix replaced. The subfiles stay and
now probe through the exact-name lookup. main's half also read
SWARM_CLUSTER_AVAILABLE, which this branch removed in favour of
IS_SWARM_MODE, as the merge base confirms, so the gates follow this branch.
The lookup's import follows this branch's get_entity_name to entity_name
rename.
web-app-pixelfed: the custom entrypoints stay removed, since the new image
brings its own and the variables behind them no longer exist, and main's
host_alias=true comes along.
web-app-suitecrm: main's session.yaml and its config/packages path, because
only that version wires framework.session and only that directory is read by
Symfony.
web-app-odoo: main's onionOptions import is required by the test.use below
it; this branch's test-seaweedfs registration stays.
Four defects surfaced that no conflict marker shows.
The auto-merge of web-app-suitecrm/vars/main.yml duplicated
SUITECRM_SESSION_CONFIG_HOST, which ansible-lint rejects. Removing it exposed
the second: SUITECRM_SESSION_CONFIG_DOCKER still named config/session.yaml
while the mount had moved to config/packages, so the freshness comparison in
tasks/01_manager_ops.yml would have tested a path nothing writes.
svc-db-elasticsearch carried environment: on two include_tasks, which Ansible
accepts on include_role only. ELASTIC_PW now sits on the five tasks that read
it, which is also the tighter scope for a credential.
The openclaw templates render NETWORK_TRUSTED_PROXY_CIDRS while their test
renderer passes only its explicit kwargs, so seven tests raised Undefined.
That predates the merge: the template line is on this branch and not on main,
and both sides share the same test file.
test_runner_mirror neutralises four environment variables under clear=False
and missed three, so the gpu and tools overrides leaked in from the container
and the assertion on the layered list broke. It now neutralises all seven.
make quality-high passes both phases.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The GHCR mirror covered role images only. The images the runner pulls on
its own went straight to Docker Hub: coredns from compose.yml, the cache
stack in compose/cache.override.yml and alpine:latest, the init image of
scripts/docker/cache/package-frontend/certs.sh. With the anonymous
githubactions quota spent, the guide step of Postmarks#0 (main run
37954203718) and Shopware#2 (#872) died on "toomanyrequests" while pulling
alpine:latest, after coredns had hit the same limit.
iter_runner_images (utils/docker/image/discovery.py) finds those images,
and the mirror sync and the fork-PR wait now cover them next to the role
images. On a real GitHub runner compose_file_args layers a generated
build/compose/runner-mirror.override.yml that points only the services of
the layered compose files at their mirror, and the certificate generator
gets the mirrored init image. Local runs and act are unchanged, and the
coredns literal stays in compose.yml for Dependabot. test-dns now waits for
images-mirror-missing like the deploy and workspace jobs, since it brings up
the stack too.
Verified with docker compose config on a simulated GitHub runner: the
service set is unchanged and only the runner images point at
ghcr.io/infinito-nexus/core/mirror/docker.io/..., and an existing role mirror
under that scheme pulls anonymously.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The three-arm comparison on web-app-gitea#0 put the meshed arm at 414ms
per task over the direct arm, and the tunnel at 4% of that. What it could not
split is the rest: the arms differ both in connection plugin (docker against
ssh) and in escalation (root with no prompt against administrator with a
password sudo on every task).
A fixed set of module calls, timed either side of the transport switch with
become off and on, gives four numbers that separate the two. It costs about a
minute where a fourth deploy arm would have cost three hours, and it needs no
root login and no sudoers change.
INFINITO_SWARM_TRANSPORT_BENCH guards it, so a normal round times nothing.
The arm existed to answer one question: how much of the meshed arm's
cost is the connection plugin and how much is the tunnel. Runs 37592061960
(docker), 37753506661 (SSH on the underlay) and 37621556682 (SSH over
WireGuard) answered it on web-app-gitea#0, debian/ext4:
pass 1 5964s / 8511s / 8832s
pass 2 5319s / 7472s / 7705s
Leaving ansible_connection: docker costs +4700s, the tunnel +554s - 89.5%
against 10.5%. The answer is recorded; the scaffolding does not need to ship.
Reverts d792519725, 1b28f87b37, 89b8a8c8d1, 0470491e1a, 529c5ace43,
dd3d10a9a2, c20dd0e5f5, 458d8956e1, 63ade1e03b, d281555945 and 4adfb6e3d2.
95cfc8b7b7 is already superseded by bbfd82f89, which sets the profiling limit
upstream.
One conflict, in the generated role overview table of README.md.
Both sides had edited the same block. Upstream dropped Redis from the pihole
row; this branch still listed it. Upstream also listed svc-net-wireguard-core,
-plain and -firewalled, which this branch removed in e706170a0b as superseded
by the unified svc-net-wireguard role.
Resolved by taking upstream's block and then running `make readme-index`, so
the table is generated from the roles that actually exist rather than
hand-merged: Redis is gone from pihole, the three legacy WireGuard roles stay
gone, and svc-net-wireguard is listed once. `make readme-index check=true`
reports the table up to date.
Git kept the role deletions silently and correctly - upstream has not touched
those three directories since the merge base, so there was no modify/delete
conflict to resolve.
The version-source updater only read meta/services.yml. An addon pinned in
meta/addons/<id>.yml stayed where it was: the ONLYOFFICE connector for
Nextcloud sat on v10.1.2, which does not install on Nextcloud 35, until
someone bumped it by hand.
The updater now reads addon files too. An addon declares its source in the
`update:` block it already has, beside monitored, catalog and upstream_id.
A monitored addon of the github-releases catalog needs nothing more: its
upstream_id names the repository whose tags are its versions. The
update-version-sources job opens the PRs, one per role.
Every pinned addon is wired:
- nextcloud onlyoffice carries its own version pin now. Its archive URL
stays a literal that carries the pin, because
test_pinned_addon_archives_available reads it as written
- wordpress mcp-adapter, moodle auth_oidc and jellyfin sso-authentication
follow the tags of their repository, moodle within its v4.5 series
- the three xwiki extensions read their Maven metadata
- pretix-oidc reads the PyPI simple index. The JSON API answers 404 behind
the package cache the test stack routes pypi.org through
- joomla's plugin is built here and jellyfin's ldap-authentication is bound
to the md5 of its archive; both are opted out with that reason
A bump rewrites the pin and the config.archive line together, and an
archive that does not carry the pin fails test_version_keys, so the two
cannot drift apart again. test_version_pins_are_watched covers addon pins
as well: a new one without a source or a reasoned opt-out fails the lint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Docker updater watches image plus version, the repository updater
repository plus ref. A dozen pins fell outside both - an app_version, a
release, a tag in a registry neither speaks - so nothing ever told anyone
they had gone stale.
Such a pin now names its upstream next to itself in an `update:` block:
git_tags, registry_tags, npm, http_regex or a role-local script, with
`match` selecting a tag flavour and `strip` removing a prefix the pin does
not carry. utils/update/source.py resolves them beside the two existing
updaters, `python -m cli.contributing.update.source` bumps them, the
update-version-sources job opens one PR per role, and
tests/external/update/source warns while a bump is pending.
Two lints hold the rule: every semver pin is watched by one of the three
updaters or opted out with a reason, and a semver only ever sits behind
`version`, `<what>_version` or `ref` - which renames moodle's release to
app_version and simpleicons' local_version to local_tag, the latter being
the tag of a locally built image rather than an upstream version. A
declaration naming a key the entity does not carry, or a type the resolver
does not know, fails the lint instead of resolving nothing in silence.
Picked from feature/store-i18n (94d607fd0a) onto current main. Two things
differ from the original: svc-ai-lmstudio no longer carries an app_version
pin, so its declaration is dropped, and web-app-stalwart's webui_version,
added since, declares the git tags of the webui repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No conflicts, and no file touched by both sides. Incoming is a run of
Docker image version bumps plus one CI change, which moves the diff
baseline from a force-pushed mirror of main to a fetch in each consumer --
entry-push-latest.yml and the resolve scripts, none of which this branch
touches.
Gates on the merged tree: unit 6199, lint 525, integration 330, external
19, ansible-lint clean, markdownlint clean, ruff clean.