Three repository rules, one new target, and CI found all three.
`argparse` is used only in annotations here, so with postponed evaluation it
belongs in a type-checking block; ruff calls that TC003 and the lint job went
red. up.py keeps the plain import because it constructs an argparse.Namespace
at runtime, which is the distinction the rule is about.
Make targets are defined in ascending order, so binfmt moves above bond.
Every make target carries a shell alias, the aliases are themselves sorted, and
an alias name may not hold more than two adjacent consonants. `i8bi` was free
and satisfies all three.
Verified: ruff clean, the two lint files pass, and `make binfmt arch=arm64`
still registers emulation, which is what proves the import really is
annotation-only rather than merely accepted by the linter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The axis was complete in CI and unusable from a workstation. Four separate
gaps, each invisible on a real runner, and each found by running the swarm loop
against it:
`INFINITO_ARCHITECTURES` as an exported variable did nothing, because the
workflow reads its pool from `inputs.architectures`. Passing the input alone did
not help either: ACT_ENV sets INFINITO_APP_DISCOVERY_RUNNER=host, so the
discovery that builds the matrix runs outside the act container, where a
workflow-level `env:` does not reach. This is why the distro axis is handed over
twice, as `distros=` and as `INFINITO_DISTROS`, and SWARM_ARCH now does the same
for the architecture.
The third gap is the sharpest: act does not resolve `${{ matrix.* }}` inside a
job-level `env:` block, so `INFINITO_ARCHITECTURE` reached the assertion empty
every time. Earlier runs only appeared to work because a stale local `.env`
supplied a value, and the assertion was therefore checking a leftover rather
than the row. It surfaced the moment that file was regenerated.
Fourth, act had no mapping for `ubuntu-24.04-arm`, the label RUNNERS
introduced, and skipped the job with a hint instead of failing. It maps to the
same runner image: locally the architecture is delivered by emulation, and the
runner's own hardware is not the promise being made.
`make binfmt` registers that emulation. The handler already existed for
compose-up; it gains a subcommand so the registration is reachable without a
privileged docker invocation, which is what an agent would otherwise have to
ask for every time. The emulation does not survive a swarm run, because act
starts further containers whose systemd clears the shared binfmt registry, so
this is needed often.
Without `arch=`, SWARM_ARCH resolves to the host's own architecture rather than
staying empty, which would now abort every unpinned local swarm run at the
assertion.
Verified: `make binfmt arch=s390x` takes a platform from `exec format error` to
`s390x`; `make -n swarm-zombie arch=arm64` carries arm64 through all three
levers and defaults to amd64 without it; shellcheck passes; a live run reached
`"architecture":"arm64"` with the arm64 glyph in its job title.
Not verified: that the runner-label mapping carries a swarm deploy to the end.
That run was stopped so a parallel agent could use the shared stack.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`os.environ.get(KEY, "")` is what test_no_infinito_get_defaults forbids: an
INFINITO_* default belongs in default.env, and the empty-string fallback hides
an unset key behind a value that looks deliberate.
Reading with no default separates the two states, and `or ""` folds them back
together on purpose rather than by accident: for this handler an unset key and
an empty one both mean the host's own architecture, which is what the env layer
documents. There is no misconfiguration left to mask.
`os.environ[KEY]` would be louder and wrong here, because it would break
compose-up in any shell where the env layer has not been sourced, for a key
whose absence is a legitimate state.
Verified: tests/lint/filesystem/python/test_no_infinito_get_defaults.py passes,
and both branches the change makes distinguishable are exercised — an unset key
and an empty one each reach the no-op path.
The rest of the diff is autoformat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI receives its emulation from docker/setup-qemu-action. A workstation received
nothing, so an INFINITO_ARCHITECTURE the host cannot execute surfaced as
`exec /bin/sh: exec format error` inside an Ansible role rather than as a
missing handler, which is a considerably worse place to learn about it.
compose-up is where the platform becomes fixed: DOCKER_DEFAULT_PLATFORM is set
when the container is created and cannot change afterwards. ensure() therefore
runs there, skips an unset platform and the host's own architecture, and
otherwise registers what is missing.
The probe runs a container rather than reading /proc/sys/fs/binfmt_misc,
because that directory reads as empty from a sandboxed shell whether or not a
handler exists, so a file check reports "missing" on a machine that emulates
perfectly well. It probes again after installing, since `--install` reporting
OK is not the same as the platform running.
This is the other half of the preceding commit. That one stops the container
from destroying the host's handlers; without this one, a host that never had
them still fails, only less legibly. Neither is sufficient alone, which is why
an earlier version of this file was worthless: it registered handlers that the
deploy then wiped.
Verified: the no-op paths and the registering path all run, and a negative
control against linux/ppc64le, for which this host had no handler, goes from
False to True across the call.
Known limitation: tonistiigi/binfmt replaces a distribution's persistent
entries with ones pointing into a container image. Restoring the originals
needs `systemctl restart systemd-binfmt`, which wants root the deploy path does
not have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>