Commit Graph
38 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 2d7bf740b0 fix(ci): put containerd, Docker and swap on the runner's empty spare disk
The small GitHub runner shape (AWS, 72G root) ran out of disk in swarm and compose rows (#1329: WordPress#2 of run 41261531965, Nextcloud#6 and #7 of run 41262286962) while an empty instance-store disk nvme1n1 sat unused. mount_spare_disk.sh formats and mounts it at /mnt, but only on a GitHub-hosted runner outside act, and only a whole unpartitioned disk without any signature, mount or swap use of at least 75G; every decline leaves a notice. relocate_docker_dataroot.sh then bind-mounts /var/lib/containerd from the spare disk and moves the Docker data-root to /mnt/docker-host (not /mnt/docker, which is the nested daemon's volume that dedicated.sh wipes). It refuses before stopping anything when images, volumes, build cache, foreign containerd namespaces or shims exist or daemon.json is unreadable, and rolls back the bind mount and daemon.json on any later failure or cancel. Swap goes on / once containerd lives on /mnt, dump_state.sh logs disk size, model and free space per storage root, and the swarm watchdog samples / and /var/lib/containerd every 10 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:31:02 +01:00
kevinveenbirkenbachandClaude Opus 5.5 204aa37278 fix(ci): merge the matrix row's disable tokens with the caller's list
Both deploy steps set `disable: ${{ matrix.disable || inputs.disable }}`.
A row carries tokens of its own whenever its network mode needs them (a
clearnet row disables Tor), so the caller's list never reached the
deploy. On feature/store-i18n (CI run 40979252521) the workspace swarm
zombie for svc-db-mariadb/postgres deployed 19 apps instead of 7 on its
clearnet row (fedora: 4h06 for one step, job cancelled at 345 min). main
carries the same expression; it bites as soon as a caller's disable list
meets a clearnet row.

The steps now pass format('{0} {1}', matrix.disable, inputs.disable);
every reader of `disable` splits on spaces and commas. Both input
descriptions follow. test_deploy_disable_merge.py reads the live
expressions; against the old fallback it fails two subtests.

Ported from feature/store-i18n 57e574ac2d (store-i18n's extra `network:`
step env stays there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:13:55 +01:00
kevinveenbirkenbachandClaude Opus 5.5 fd596cf188 fix(ci): tear down host AppArmor profiles on GitHub runners like on dev hosts
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>
2026-10-19 22:36:58 +02:00
kevinveenbirkenbachandClaude Opus 5.5 b055eece05 feat(ci): keep GitHub-hosted runner labels current through the updater
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>
2026-10-19 16:56:52 +02:00
kevinveenbirkenbachandClaude Opus 5.5 52ad8549e0 fix(ci): pin GitHub-hosted runners to ubuntu-24.04
`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>
2026-10-19 15:57:12 +02:00
evangelostsak 4a100cade8 chore(ci): stop handing the swarm round INFINITO_SWARM_TRANSPORT_BENCH 2026-10-18 01:01:10 +02:00
evangelostsak c9f203b9f1 revert(ci): stop handing the compose deploy a vpn state 2026-10-16 21:26:16 +02:00
evangelostsak b8395dbe18 feat(ci): hand the compose deploy its row's vpn state 2026-10-16 03:40:16 +02:00
evangelostsak 1063e12917 docs(ci): the meshes input names a WireGuard layout 2026-10-14 15:03:41 +02:00
evangelostsak 13f8a98faa ci(deploy): accept the mesh declarations as an input
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.
2026-10-14 11:23:22 +02:00
evangelostsak 12f4dfe4a5 feat(swarm-test): price the transport against the escalation
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.
2026-10-09 14:23:36 +02:00
evangelostsak ff1db88753 revert(swarm-test): remove the SSH-on-underlay measurement arm
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.
2026-10-09 12:12:03 +02:00
evangelostsak dd3d10a9a7 feat(ci): read the underlay flag from a repository variable
A repository variable rather than a workflow input: the arm is a
measurement, not an axis, and it should be set for one sweep and cleared.
2026-10-07 09:54:32 +02:00
evangelostsak 0486cc0c36 docs(ci): label the vpn input with the mesh glyph
Same glyph the matrix rows now carry, so the input and its rows read as
one axis.
2026-10-05 12:00:06 +02:00
evangelostsak 468d420781 fix(ci): declare the vpn axis on the dispatch trigger too
The axis was only an input of the reusable trigger, so a dispatch run
dropped it and fell back to the default. On auto, index 0 of sweep 0 is the
half of the rotation that carries the mesh, which is why a throwaway cluster
asked for the wireguard service to be switched off and raised it anyway:

  service 'wireguard' is disabled, but provider role 'svc-net-wireguard'
  is still active

Declared beside the tor axis it mirrors, with the same shape and default, so
a run that passes vpn= now gets what it asked for on either trigger.
2026-10-04 12:25:53 +02:00
evangelostsak e456b1eee6 Merge remote-tracking branch 'origin/main' into feature/svc-net-wireguard
One conflict, in the MCP exclusion list of requirement 035. Both sides
edited the same line: main added svc-net-tor-smtp, this branch replaced the
three svc-net-wireguard-{core,plain,firewalled} entries with the single
svc-net-wireguard it deploys. Resolved to the union of both — 24 entries —
which is what the merged tree actually holds.

Everything else merged clean and was checked for overlap rather than
assumed: no reference anywhere still names the three replaced roles;
matrix.py kept main's PRIMARY_APPS alongside the reordered round, and the
bootstrap pass does not need it, since only one template reads it and
group_vars defaults it empty; and main's ansible.cfg addition sits in
[defaults], so the ssh_args the mesh tool derives its own connection
settings from are untouched.
2026-10-03 02:29:45 +02:00
evangelostsak cd2ff90402 Merge remote-tracking branch 'origin/main' into feature/svc-net-wireguard
# Conflicts:
#	roles/web-app-mailu/README.md
2026-10-01 21:39:17 +02:00
Alejandro Roman 715abcd56e Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-29 13:43:01 +04:00
kevinveenbirkenbachandClaude Opus 5.5 5bd4f778c3 ci(deploy): serialize identical deploy rows again, keyed on every axis
9850d5afa0 disabled the job-level deploy group because it blocked the
queue and kept jobs from being cancelled. Both causes are addressed:

- Cancelling: the chunks that call this workflow guarded with always(),
  so a cancelled run kept its deploy jobs alive and they kept holding
  their groups. Since a5f00ca777 the chunks guard with !cancelled() and
  die with the run, which releases the group.
- Blocking: the key held only mode, tor, apps and variant, so rows that
  differ in distro or filesystem waited on each other across branches.
  The key now carries every axis the matrix generator emits, the same
  set artifact_slug identifies a deployment by: mode, tor, distro,
  filesystem, apps and variant. Two runs now only meet on an exactly
  identical deployment. A sample matrix of eighteen rows had every axis
  set and no duplicate key within the run.

queue: max keeps the second identical row queued instead of replacing
the waiting one; GitHub accepted the key in all 200 runs of August,
and scripts/lint/action.sh already ignores actionlint's unknown-key
report for it.

No resource shared per row was found in the deploy path: jobs push no
images, write nothing through the API and use no actions/cache, and
hosted runners are isolated. The serialization is the cross-branch
ordering ae9d7c2fd3 introduced, not the protection of a resource.

A TODO asks a CI deploy to confirm that a cancel drops queued jobs
from the group and that an identical row of a second run queues
behind it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 12:52:40 +02:00
kevinveenbirkenbachandClaude Opus 5.5 f58713843f ci(deploy): fail fast on pull request runs
On pull_request and pull_request_target the deploy and workspace
matrices now run fail-fast: the first failed row cancels its siblings
in that matrix. A deploy chunk holds around sixty rows, and hosted
runner slots have been the bottleneck, with PR jobs waiting from twenty
minutes to twenty-one hours for a runner. Once one row is red the PR is
red regardless, so the remaining rows only hold slots other runs need.

Push, workflow_dispatch and cron runs keep fail-fast off: main and the
manual sweeps are where the full failure picture is wanted.

The expression reads github.event_name, which inside a reusable
workflow is the caller's event, so both calls see the PR event of the
entry workflow. A red chunk already stops the later ones through
chunk_gate, and done reports the run red. Cancelled rows still run
their always() upload steps, and the failed-run retrigger selects
cancelled roles by default, so a follow-up rerun covers them.

CodeQL keeps fail-fast off: one failing language would cancel the
analysis of the others and lose findings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 12:52:40 +02:00
evangelostsak f224425465 feat(ci): hand the run's mesh mode to the matrix builder 2026-09-26 08:31:23 +02:00
evangelostsak cba50eae68 feat(ci): hand the mesh state to the swarm drill 2026-09-25 22:34:26 +02:00
Alejandro Roman 4b9a834546 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail-swarm 2026-09-21 03:25:20 +04:00
kevinveenbirkenbachandClaude Opus 5 2567c5ece1 feat(ci): let a manual run set the chunk size the formula would derive
chunk_size() multiplied the runner slots a chunk may assume by the waves the
queue window affords, and nothing could say otherwise. The derivation is the
right default -- too few rows leaves runners idle, too many push a chunk's tail
past the queue cancel -- but a run that wants smaller chunks had to edit
default.env to get them.

INFINITO_CI_CHUNK_SIZE now takes precedence when it is greater than zero, and
zero keeps the derivation. The input travels the same path offset already
travels: entry-manual-steer.yml offers the field and maps empty to 0,
call-orchestrator.yml hands it to all four chunk blocks, call-test-deploy.yml
exports it as the env var. It is a string rather than a number for the reason
resume_from_chunk already documents: a workflow_dispatch number input reaches
the caller as a string, and a number-typed workflow_call input then makes
GitHub refuse the job before it exists.

Two places in call-test-deploy.yml set it, not one. The plan summary resolves
the chunk size itself, so exporting it only for the app resolution would print
a plan that disagrees with the matrix the run then deploys.

Measured against the live helper: 0 gives 80 rows over 3 chunks, 25 gives 25
over 4, and returning to 0 restores 80 and 3.

The gate found three faults in the first attempt and all three are in this
commit: the key sorted after INFINITO_CI_CONCURRENCY rather than before it, the
dispatch description ran to 81 characters against a limit of 80, and the
retrigger fixture did not carry the new input. That last one matters beyond the
test: it is the check that would otherwise let a retrigger drop the chunk size
silently. carried_inputs() itself needed nothing, since it derives from the
declared inputs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:25:12 +02:00
kevinveenbirkenbachandClaude Opus 5 786899700f feat(litellm): serve OpenAI, Anthropic and OpenRouter from declared keys
The gateway could reach exactly one remote provider, through a bare
OPENROUTER_API_KEY group_var with an empty-string default and an `{% if %}`
around a single hardcoded model entry. OpenAI and Anthropic had no path at all.

All three are now declared in roles/svc-ai-litellm/meta/secrets.yml, which
mapping.py already calls the application config schema, so a key lands at
applications.svc-ai-litellm.secrets.credentials.<provider>_api_key like every
other credential. Each carries the `type` and `regex` its value must satisfy and
defaults to the central API.<provider>.api_key block, which keeps one
operator-facing place to fill a provider in while the role owns the shape.

type and regex had to become real to be worth declaring. 192 `validation:`
blocks sit in the role schemas today and not one line of Python enforces them,
so a `regex` would have joined a pile of decoration. validate_supplied_value
now runs on every path a value arrives from outside — the --set override, a
value already in the inventory, and the value standing next to a `default:`.
A generated value stays exempt because the algorithm that produced it is named
by the same schema and generate_value takes no schema argument. min_length is
deliberately left unenforced: the generators ignore it, so holding them to it
would fail where nothing is wrong.

The pattern is written for the paste, not for the vendor. OpenAI moved issuance
from sk- to sk-proj- without notice and Anthropic issues sk-ant-api03-; a schema
pinning a prefix would reject a valid key and abort inventory creation. `\S{20,}`
accepts every form those three issue today and one nobody has announced, and
rejects a trailing newline, an embedded space, a truncated copy and a
<placeholder>. A key pasted into the wrong provider's slot is caught at deploy
instead, where the route it publishes answers nothing.

The gateway iterates one LITELLM_REMOTE_BACKENDS list instead of three `{% if %}`
blocks; config.yaml.j2 and env.j2 read the same list and selectattr('key')
decides. LITELLM_CHAT_MODEL falls back to the first configured remote alias
rather than to a hardcoded openrouter/auto.

For CI the keys travel as INFINITO_API_{OPENAI,ANTHROPIC,OPENROUTER}_KEY through
default.env, the passthrough allowlist, the infinito service in compose.yml, the
workflow env block and the development inventory. The compose leg is not
optional: the env lint rejected the change without it, because a key the
inventory reads but compose does not forward resolves to empty inside the
container with no error. The workflow reads `secrets.X || vars.X` so a variable
works as asked while a secret wins, which matters because a variable is not
masked and the development inventory sets MASK_CREDENTIALS_IN_LOGS: false.

An unset key is not an error anywhere: the provider's routes stay unpublished
and the CLI test asserts their absence rather than skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 19:57:21 +02:00
kevinveenbirkenbachandClaude Opus 5 5603b5ca45 fix(ci): read the trace switch from the variable that exists
The workflow read `vars.INFINITO_PLAYWRIGHT_KEEP`. This repository defines two
variables, `GHCR_USERNAME` and `PLAYWRIGHT_KEEP_ALL`, and that is not either of
them. GitHub does not complain about an undefined `vars.*`; it expands to an
empty string, so `keepAll` in playwright.config.js has been false on every run
since the reference was introduced by db66e01020, and `PLAYWRIGHT_KEEP_ALL=true`
has never reached anything.

The consequence is that trace, screenshot and video all stayed at their
on-failure settings, so a passing test recorded nothing. That is why run
34348821577 offers no way to see inside its Playwright timings: the artifact for
the tor Nextcloud job holds fourteen junit files and fourteen HTML reports and
not one trace.zip. The one measurement that would explain where a 161s test
spends its time was switched off by a name.

The env key keeps its name, since the container, the passthrough allow-list and
the config all agree on it; only the source changes.

Worth knowing before the next run: this turns trace, screenshot and video to
"on" for every test. Artifacts grow well beyond the current 2.8MB per job, and
tracing is not free, so the runs that already die at the distro budget may die
sooner. The switch is now a variable, so it can be turned on for a diagnostic
run and off again without touching the workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 01:22:01 +02:00
kevinveenbirkenbachandClaude Opus 5 b3686a9770 ci(deploy): stop installing gettext for a caller that no longer exists
509cd19985 added gettext-base because the Act runner image lacks what
GitHub's ubuntu-latest ships, and coredns.py shelled out to envsubst. That
only ever covered the outer runner: the nested deploy containers it also
failed in are not reachable from an apt call in the job.

00478b8ead removed the dependency instead, substituting the Corefile through
string.Template in-process. git grep envsubst at HEAD now finds four hits and
none of them is a call: this step's name, the docstring that explains the
change, and two stale README lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:37:26 +02:00
kevinveenbirkenbachandClaude Opus 5 7fd495cdfc feat(ci): replay a role's README instructions on its smallest deploy row
The instructions test held a job of its own that picked one random role per
run, so a README's Production block was proven roughly once per 265 roles.
The replay now rides on a deploy row instead: no extra runner, and every role
the sweep touches gets its documented one-command deploy exercised.

The row is the role's smallest variant -- fewest services enabled -- because
that is the shape the README describes, and it is chosen among the variants
the sweep actually deploys, so a row cut as redundant coverage cannot silently
drop the role from the test. A role qualifies only when its README carries a
Production block and the guide's own mode is one the role runs and tests.

The marked entry carries the guide mode in `instructions` and a 📖 in its job
title, between the variant number and a priority ⭐. The guide mode is
independent of the mode the rotation gave the row: a swarm row still replays
in compose. The step runs last in the deploy job, after the swarm teardown,
because the replay deploys the role a second time and needs the runner's
ports.

call-test-instructions.yml, cli.meta.ci.guide_select and the `instructions`
dispatch input go with it. `redundant()` moves next to the marker so the
chunker, the plan table and the marker share one definition.

The plan table trades its 📊 Weight column for 📖 Instructions; nothing read
the weight any more, so it leaves the matrix entries too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 16:57:04 +02:00
Alejandro Roman 333079cc2a feat(UI): fix migration, errors happening in local deploy and swarm integraton + merged main 2026-08-30 21:39:52 +04:00
kevinveenbirkenbachandClaude Opus 5 66bde6af40 feat(ci): spread distro and filesystem per row
A run-wide draw proved one distribution and said nothing about the other four, while reporting green. Both are matrix-row axes now, assigned by the same deterministic rotation mode and tor already use, so one sweep exercises every distribution and every filesystem.

The two read the row's position like an odometer, the distro as the low digit. Turning both on the position directly covers only n of the n x m pairs whenever the pools are the same length, and no sweep unlocks it.

Both are part of a row's identity: glyphs in the job label, columns in the plan table, shards in the artifact name. Both are selectable through %distro and /filesystem, and a retrigger replays the distro off a failed title. It leaves the filesystem out, because a title states the kind the matrix assigned and a deploy may have fallen back from it.

A row's filesystem is a preference unless a human names it, on the run or in a token. Reading the matrix's own narrowing as a demand would fail rows for every transient condition the applying step is built to tolerate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:23:29 +02:00
kevinveenbirkenbachandClaude Opus 5 443db2c35e docs(ci): fit the disable input description on one line
86389f856 added the input with a 113-character description; test_name_emoji_and_length caps a dispatch input at 80. 53e5e9ba2 set the convention this missed: the description says what the input is, the workflow README says what the values do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:41:10 +02:00
kevinveenbirkenbachandClaude Opus 5 661cfe6748 fix(ci): let the caller's disable set reach the deploy job
The workspace swarm track passes disable=<39 provider keys> to make swarm-zombie, which forwards it to act as a runner env var. Both deploy steps then declare 'env: disable: ${{ matrix.disable }}', and a step env beats the runner env - so for variant 0, whose matrix cell is empty, the whole set was dropped.

Effect in job 95881470584: the MariaDB-only swarm deploy also brought up web-app-keycloak (629s), web-app-mailu (295s), svc-db-openldap, web-app-matomo, web-app-dashboard, svc-bkp-volume-2-local and sys-svc-cdn, plus test-e2e-playwright (997s) across all of them - while svc-db-mariadb itself, the role under test, took 169s. sso, email, ldap, matomo, dashboard, container_backup and css all sit in the disable set that never arrived. The job died at the 345-minute workspace limit.

The input defaults to an empty string and matrix.disable still wins where it is set, so the main CI passes nothing and behaves exactly as before. Verified: act accepts --input disable= with an empty value (exit 0); the || expression already runs in call-release-version.yml and call-test-runner-smoke.yml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:41:09 +02:00
kevinveenbirkenbachandClaude Opus 5 b7d4365f38 refactor(ci/plan): reorder the plan table and name why a row was cut
The id leads, the variant follows the name it belongs to, and covered_by,
weight and the new clone column close the row. ✂️ said only that a row was cut
while two different reasons hide behind it: 🔰 points at the earlier row that
already embeds this one, 🐑 says the row shares its service set with an earlier
role that stands in for it. 27 rows of a full plan carry 🐑 and were until now
indistinguishable from the rest.

_COLUMNS is the order and the headers again. tor and triggered used to sit
outside it as special cases, so a reordering had to be made in two places and
the tests indexed by hand; they now ask _COLUMNS for a column's position.

The chunk column loses its ⏭️ marker, which named the block the calling job was
running. Without it cells() has no use for `current` and --chunk no effect, so
both go, along with the workflow's argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:54:47 +02:00
kevinveenbirkenbachandClaude Opus 5 9850d5afa0 ci(deploy): disable per-matrix deploy concurrency group
The per-entry group serialized independent role deploys and kept jobs
queued instead of cancellable. Commented out with a TODO so the group key
can be reworked before it comes back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:03 +02:00
kevinveenbirkenbachandClaude Opus 5 cb1ff29951 fix(ci): decide discovery's row selection on the set that actually deploys
clone and covered_by both name a stand-in row: another row carries this one's coverage, so the sweep may drop it. Both were decided over every row and then applied to a filtered set, so a run that selected a subset deferred to a stand-in the filter had already removed - the row was cut in favour of a deploy that never happens. --whitelist svc-db-mariadb yielded no row at all, and the swarm-zombie job passed green while drilling nothing.

The sort's last-resort tie-breaker was drawn per process, so discovery and the plan renderer could name different assignments for the same run. INFINITO_DISCOVERY_SEED derives it from the row's own identity instead; the workflow feeds github.run_id, so the rotation still happens, once per run rather than once per process.

Measured against the previous order: on the mode-only filter both cut sets are identical (swarm 220 rows, 44 cut; compose 243 rows, 54 cut), so the full sweep is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:02 +02:00
kevinveenbirkenbachandClaude Opus 5 0f450722bc feat(ci): start chunk 0 at the head, or wherever the operator points
The regular rows began wherever (sweep * capacity) % count pointed. On run 210
that was offset 68: chunk 0 opened at candidate 69, the coverage-first head of
the list went undeployed, and no two runs could be compared because the window
moved with the run number. A run that looked wrong turned out to be merely
rotated, and finding that out meant recomputing the arithmetic by hand.

The rotation is gone. Chunk 0 reads the ranking from the top unless an offset
says otherwise, and the offset is an input now, threaded from the manual form
through the orchestrator and the deploy call down to the matrix builder. The
plan renderer takes the same flag, so the table shows the window the run will
take.

The offset is a row count or a selection token. A token is the honest form:
the ranking shifts whenever a variant appears, so 'offset 84' means something
different tomorrow while 'web-app-nextcloud#5' still means the same row. Axes
the token leaves open match anything, so a bare role name starts at its first
row and a pasted job label starts at exactly the job it names. A token that
matches no regular row aborts rather than silently starting at the head an
operator meant to skip.

The run title carries it as ⏩ followed by the value, and only when one was
asked for, so a rotated-looking run says so in its own name and the retrigger
reads it back from there when a job log has expired.

resume_from_chunk leaves the manual form with it. Skipping whole chunks was
the coarse way to say 'carry on further down'; the offset says it exactly and
keeps the rest of the chain intact. The orchestrator input stays for the other
entry points.

What this gives up is automatic tail coverage. Rows beyond the budget are
reached by moving the offset, widening the budget or narrowing the selection,
not by waiting for consecutive sweeps to walk the list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 06:33:50 +02:00
kevinveenbirkenbachandClaude Opus 5 53e5e9ba21 docs(workflows): make a dispatch description one readable line
An input description is what an operator reads in the dispatch form, one line
per input, exactly like a job or step name -- and it was drifting the other
way: eighteen of them opened with a bare word where every name in this
repository opens with an emoji, and sixteen ran past the line, the longest at
382 characters explaining four tor values inside a form field.

The descriptions now say what the input is; the workflow README says what the
values do. It gains the selection-token syntax and a table of the four tor
values, which is where a paragraph belongs when it outgrows its form field.

workflow_call inputs are left alone. They are wiring between workflows and are
rendered for nobody.

Two explanations did not survive the cut and are worth knowing: the mirror
cleanup no longer warns that the default GITHUB_TOKEN may lack the
delete:packages scope, and the workspace track no longer spells out that
'rotate' flips with the run number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 03:38:43 +02:00
kevinveenbirkenbachandClaude Opus 5 db66e01020 feat(ci): collapse the four deploy workflows into one chunk chain
call-test-deploy-{compose,host,swarm,single-node}.yml differed only in
which mode they pinned and which artifact prefix they wrote; compose and
host already ran the identical drill. They become one call-test-deploy.yml
whose matrix rows carry the mode, so the swarm-specific steps are guarded
on it and everything else is shared.

The orchestrator loses its eight deploy jobs and the sequencing job for
three serial chunk blocks. GitHub Actions cannot generate a variable
number of jobs, so the blocks are written out and INFINITO_CI_MAX_CHUNKS
tells the budget how many exist; blocks whose slice is empty skip
themselves. chunk_gate stops the chain at the first red chunk,
resume_from_chunk re-enters after a fix instead of re-running the green
chunks, and modes collapses to a single mode choice.

apps.sh now emits matrix entries for one chunk rather than a token list
for one mode, and no longer sorts them: the discovery order is what the
chunker slices on. split_runner_apps.sh reads its runner quota from
INFINITO_CI_CONCURRENCY instead of carrying a second copy of the 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 06:09:04 +02:00