The contributor docs described repository settings that are not in
place. security.md marked "Require status checks", "Require branch to
be up to date" and "Require signed commits" as enforced, but the active
ruleset `Main` has no required_status_checks and no required_signatures
rule, and the separate `Signed` ruleset is disabled. repository.md
listed Dependabot alerts, security and version updates, secret scanning
and push protection as disabled although all of them are on, and its
summary repeated the two rules that are not enforced. branch.md claimed
branch protection enforces the branch naming rules, while only the PR
validation run checks the prefix. Three pages still described a hadolint
SARIF job in call-lint.yml that no longer exists, which hid that nothing
uploads the Hadolint results the ruleset asks for.
The pages now state what the ruleset and the repository settings
enforce today, name the policy pages for rules that are policy only,
and record that no workflow uploads Hadolint results.
Scorecard alerts 472, 504 and 505 depend on repository settings and
process and stay open; 507 and 539 are no longer reported on main.
Refs #853
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A push to main whose head commit starts with "Release version " (the subject pkgmgr writes for every release) now cancels the main run in progress, so the release starts at once instead of queueing behind it. Every other push to main still leaves the running pipeline alone.
When the pushed main commit carries a vX.Y.Z tag, the orchestrator runs with chunk_gate false: every chunk deploys and reports, and a failed chunk still turns the run red, so release-guard and release-version stay skipped.
entry-cancel-superseded.yml mirrors the new rule behind its push guard, parenthesised so && does not bind the release clause; the mirror test composes the expected rule the same way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
Closing a PR left "Cancel: PR Runs on Close" (run 36385268627) on
pending for hours. The canceller joins the PR orchestrator's
concurrency group, so GitHub cancelled the orchestrator run within a
second. But a cancel re-evaluates the job-level if: of every live job
and spares each one whose condition still holds. The deploy chunks
started with always(), which always holds: test-workspace died at
06:11:59, while Chunk 0 kept deploying and its ~60 queued matrix jobs
kept waiting for runners. The group stays occupied until the occupant
is terminal, so the canceller parked behind the very run it had
already cancelled.
The chunks, test-runner-smoke, the fork-prereqs gate and the release
job now guard with !cancelled(). Outside a cancel it matches always():
they still run after a failed or skipped need. On a cancel it turns
false and the job is cancelled with the run.
done and report-main-failures keep always(). They exist to report a
cancelled run, and a done skipped on cancel would count as a passing
required check.
The new lint walks every workflow whose concurrency cancels in
progress, plus everything it calls transitively, and rejects a
job-level always() outside that allowlist. It found the release job,
which a line-based grep had missed. It also fails when an allowlisted
job stops needing the exemption, and when its scope no longer reaches
the deploy workflows, so it cannot go silently empty. Against the
pre-fix tree it reports all six offenders.
Behaviour change: manually cancelling a release run now stops the
retag too. cron-release-highest re-dispatches the release.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cron-release-highest.yml ran `gh workflow run release-version.yml`, a file
renamed to call-release-version.yml by 71888d86af on 2026-08-07. GitHub does not
error on a stale name: it resolves it against the deleted workflow's tombstone
(upstream id 226809803, fork 248877824, both state=deleted) and answers HTTP
422. The step is the job's last, so the whole run went red. The last 25
scheduled upstream runs failed there, the streak reaching back to 2026-08-09.
This removes the 422. It does not make releases green: workflow 226809803 last
succeeded on 2026-07-08, and from 2026-07-09 through 2026-08-08 the dispatch
worked while every run still failed at "Build missing CI images (ubuntu)".
Expect that failure to surface next.
test_workflow_references.py was meant to survive exactly this rename and could
not: its pattern required a directory prefix, so a bare filename was invisible,
and it reported zero offenders against twenty real ones. It now also matches a
bare *.yml inside .github/workflows, where every such token names a sibling, and
it checks both manual-dispatch forms, `gh workflow run` and a REST POST to
actions/workflows/<file>/dispatches, for existence and for a workflow_dispatch
trigger. Outside that directory the prefix stays mandatory, because a bare name
collides with services.yml and compose.yml.
The nineteen README labels are the same sweep's leftovers in the mermaid
diagrams; the widened lint fails without them.
Negative controls executed per branch: reverting the dispatch name, and pointing
the REST call at a missing workflow and at a reusable-only one, each fails the
matching check, and restoring makes them pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A workflow the entries call declares its own concurrency group, and its jobs then leave the entry's group. The entry's cancel-in-progress no longer reaches them, so a superseded run keeps the runners its remaining jobs hold. Run 34614940508 held its runners after its successor arrived and only a manual force cancel ended it; its check runs carry no 'Canceling since a higher priority waiting request' annotation, only the forced cancel.
call-orchestrator.yml and call-test-runner-smoke.yml drop their groups. The smoke group was keyed on the caller's ref as well, so a manually dispatched smoke run cancelled the smoke job of an orchestrator run on the same ref. call-images-build-ci.yml and call-images-mirror-missing.yml keep theirs: their group deduplicates image builds across runs rather than scoping a single run, and it does not cancel.
The test asserts both halves, that the orchestrator declares no group and that every entry calling it carries group plus cancel-in-progress, and adds the chain below it. It finds the callers by their uses: line rather than by an entry-* filename, so a caller named otherwise cannot skip the requirement. It fails against HEAD, where call-test-runner-smoke.yml declares a group.
The cause is established negatively: no concurrency annotation exists on the superseded run. What would settle it is a run that supersedes another and cancels it, and that needs a push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A sweep may deploy 210 rows (available) in chunks of 80. Three declared
blocks hold exactly that when there is no priority line, but priority
rows get a chunk of their own that is never topped up with regular rows.
Nine priority rows therefore occupied a whole block, left the regular
rows 2 x 80 = 160, and 42 rows of budget went unused.
Adding a block to the YAML alone would not have changed that:
matrix.chunks_of handed the split slots.chunk_count(), which is
min(ceil(available / chunk size), INFINITO_CI_MAX_CHUNKS) and ignores
the priority seam, so the plan still stopped at three blocks.
call-orchestrator.yml now declares test-deploy-chunk-3, gated on chunks
0 to 2 like every chunk gates on all before it, and report-main-failures
and done need it. INFINITO_CI_MAX_CHUNKS is 4, one above what the budget
fills, and slots.chunk_blocks() returns it. chunks_of hands the split
every declared block; available still caps the rows, and a chunk whose
slice comes back empty skips its deploy. chunk_count() stays the "chunks
filled" figure of the budget report.
Tests: the split spends 9 + 80 + 80 + 41 = 210 rows with a spare block;
chunks_of plans three chunks from chunk_blocks=3 while chunk_count=2
(the previous wiring yields [1, 10]); an integration guard requires the
declared chunk jobs to equal INFINITO_CI_MAX_CHUNKS and to exceed
ceil(available / chunk size) by one, and fails on the stale value 3.
A shell that inherited INFINITO_CI_MAX_CHUNKS=3 keeps failing that guard
until it is restarted; make dotenv-force rewrites .env.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A retrigger copies every dispatch input from its source run, chunk_gate
included, so a sweep that stopped at its first red chunk came back stopping
there again, and nothing short of a manual dispatch could ask the chain to
deploy every chunk and report all of it at once.
`infinito administration deploy ci trigger --chunk-gate {true,false}` now
overrides the carried value after the source run's configuration is read; it
is the one deliberate departure from "reproduce the source run exactly", and
the module docstring says so. Omitted, the carried value or the workflow
default applies as before.
`i8ciallon` is `i8ciall` with `--chunk-gate false`: force-cancel the run, put
everything without a verdict back on the priority line, and keep deploying
past a failed chunk. The alias lint caps a name at two adjacent consonants,
which is why it is not `i8ciallnonstop`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
A long orchestrator sweep sometimes survives its concurrency group: the occupant sits on queued while its matrix waits for runners, cancel-in-progress does not reap it, and the newer run parks on pending until someone cancels the sweep by hand. PR #380 sat that way for nine hours behind a run from the previous commit.
entry-cancel-superseded.yml cancels those leftovers through the API. The group stays primary and untouched: it reaps within seconds and needs no runner, so the fallback normally arrives to find nothing left to do. It carries no concurrency group of its own, because the run holding a group cannot be the run that frees it.
Selection mirrors what the group would have done: cancel every matched run except the newest in the group instance, keyed the way the group is keyed. Not by head SHA, which goes stale while the job waits for a runner and would cancel the newest commit's CI while sparing the stale one. INCLUDE_PATHS keeps the sweep inside the group's own members, so a push to main cannot take the cron workflows with it.
deleted_branch_runs.sh becomes branch_runs.sh, since it now also serves live branches, and the shared parts move to lib.sh. A rejected cancel is logged and the remaining runs are still attempted; only a denied token fails the job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Both were left by earlier commits and are collapsed by markdownlint-cli2 and shfmt respectively, so every make quality run left the tree dirty and the change would eventually have ridden along in an unrelated commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
GitHub cancels a job queued for 24h. A line starting swarm, compose and
host at once queues its whole matrix in one go, so with more roles than
runner slots the tail waits past that cut and dies unrun -- 54 of 241
jobs in run 30937913632.
Deploy the modes one after the other instead, heaviest first, which
restarts the queue clock per mode. cli.meta.ci.sequencing counts a
line's matrix jobs on the row basis the discover steps use and
serialises above INFINITO_CI_SEQUENTIAL_THRESHOLD; below it the modes
still start together, so a small diff pays no wall-clock for this.
A needs: edge is static, so both layouts exist as jobs and the decision
skips one. Their conditions are structurally asserted, not evaluated:
ten near-identical if expressions invite a copied sentinel or a missing
chain edge, and both failures are silent at runtime.
mode_fail_fast stops a serial line at its first failed mode, on by
default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test-environment.yml and test-guide.yml become test-workspace.yml and
test-instructions.yml. Orchestrator job ids, the distros-SPOT matrix
lint, the act example, the diagnostics slug and the rescue artefact
names follow.
Replace the roles_only input with per-job workspace/instructions
tri-state inputs (auto|true|false). 'auto' derives the former
roles_only skip straight from whitelist: both jobs run on a global
scope (whitelist empty or __ALL__) and are skipped on a role-scoped
run, which keeps the PR behaviour identical because the diff resolver
never emits an empty whitelist. entry-manual exposes both as choice
dropdowns defaulting to false, so a manual run opts in explicitly.
The now redundant roles_only output is dropped from
pr_affected_roles.sh and subset_roles.py.
Branch-protection required-status-check names change along with the
job renames.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the ten lint-*.yml workflows and the four test-code-*.yml
workflows with lint.yml (make lint fanning out every check, plus the
hadolint SARIF job) and test.yml (make test running unit, integration,
lint and external suites in parallel on the runner host). Drop the
lint-gate and test-code-gate jobs; code-quality-gate now needs lint,
test and security-codeql directly. Dependency diagram, workflows table
and pipeline docs follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan job renders under a map emoji with plan wording across the CLI,
docs and aliases; the guide workflow, its orchestrator wiring, the
dependency diagrams and the workflows doc follow the test-guide name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The orchestrator renders the per-mode deploy plan into the run summary.
test-run becomes test-readme (book emoji), depends on build-ci-images,
grants NET_ADMIN (the in-container MSS clamping needs it) and seeds the
run's freshly built CI image instead of the published default. The
manual run-name stars the priority roles.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Priority deploy callers and their discover jobs now carry a star
(marker input threaded through single-node into the leaf workflows);
regular callers are labelled distinctly. New test-run.yml extracts the
Production command from a role README and executes it against the
runner (localhost => local connection, Docker socket instead of TTY),
runs parallel to test-environment and gates the final done job.
Lint Mermaid gets the broom emoji like every other lint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test-deploy-single-node.yml bundles the compose and host deploy tests
behind one reusable workflow; the orchestrator gains a priority input
that deploys focus roles first per mode and runs the remaining roles
only after every priority job succeeded, excluded via the new
INFINITO_BLACKLIST discovery filter in apps.sh. entry-manual exposes
the priority field and treats priority-only dispatches as roles-only.
Workflow dependency diagram added under .github/workflows/README.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>