Continuous integration¶
Every repository in the fleet runs the same CI model, and it is version-agnostic by design: the set of language versions a repo tests against is configuration, not workflow code, and the merge gates never name a version. Changing what a repo tests against is a one-line edit; nothing about the workflows or the branch protection has to move with it.
This page is the fleet-level summary. The authoritative mechanics — job names,
the reusable-workflow inputs, the evidence bundle format — live in
vergil-tooling and are linked below rather than restated here:
The version set lives only in vergil.toml¶
A repo's CI version matrix is stored in exactly one place: [ci].versions in
its vergil.toml. It is not embedded in ci.yml and not passed as a
workflow input. The vergil-actions reusable workflows read [ci].versions
from the consuming repo at run time and fan their matrix out over that list —
a dynamic matrix.
The consequence is that changing what a repo tests against — adding a new
language version, or dropping an end-of-life one — is a one-line
vergil.toml edit. No ci.yml is hand-edited, so the matrix cannot drift
from the stored version set.
Thin-caller ci.yml¶
A consuming repo's ci.yml is a thin caller. Each job simply uses: a
vergil-actions reusable workflow at the pinned @v2.1 tag and passes only
language: and container-suffix:. It passes no version matrix and no
container tag — both resolve dynamically: the matrix from [ci].versions, and
the single-container jobs' image tag from the primary version (below).
Because the matrix and the container tag are derived rather than written down,
the same short workflow serves every repo, and a version change takes effect
fleet-wide without touching a single ci.yml.
Version-agnostic evidence gates¶
Branch protection requires stable, version-agnostic aggregate checks, not
per-version legs. For each matrixed kind — audit, quality (lint + typecheck),
and tests — the required check is the <kind> / evidence aggregate the
reusable workflow emits (audit / evidence, quality / evidence,
test / evidence), never the individual … / 3.12, … / 3.13 legs. Each
evidence job depends on the whole matrix, so one required context covers
every version.
Because the required-check names carry no version, a matrix change merges
through the normal gate — including a matrix reduction. This closes the old
deadlock, where branch protection required a per-version leg that a reduced
matrix could never produce, leaving the PR "expected, never reported" and
permanently blocked with no --admin escape. A version change is now an
ordinary PR.
The non-matrixed checks (the security scanners, the version-bump gate, the docs build) keep their fixed, version-free names, and they are required too — there are no report-only PR gates.
[ci].primary-version for single-container jobs¶
Some jobs are not matrixed — they run once in a single container (for example
the security scan, the version-bump gate, and the docs build). These run on
the primary version: [ci].primary-version if it is set, otherwise the
highest entry of [ci].versions (so 3.14 for
["3.12", "3.13", "3.14"]). primary-version is the escape hatch for the rare
case where the highest version is not the right single-container default.
Nightly governance (ops.yml)¶
Keeping this model canonical across the fleet is itself automated. Every
managed repo carries an ops.yml workflow with a nightly config-audit
caller (a scheduled vergil-actions reusable workflow). It keeps each
repo's GitHub configuration canonical — reconciling branch protection against
the derived required-check set, and flagging drift such as a required context
that no workflow can produce. A repo whose ops.yml is missing or has no
schedule is treated as non-compliant.