CI rollout
CI rollout
Section titled “CI rollout”
node ~/.claude/scripts/fleet/ci-rollout.jstakes the farm runner from one repo to every repo that should have it, and says why for every repo it leaves alone. It works through the per-repo tools: the declared CI locus in each provision manifest,runner.sh installfor the runner, andci-locus.js reconcilefor the repo variable. Nothing is written without--apply. (Back to reference index.)
The policy
Section titled “The policy”The policy is operator data at config/ci-rollout.json in the operator repo, validated by
schemas/ci-rollout.schema.json and graded by check-operator.js:
{ "_meta": { "version": 1 }, "farm_owners": ["<account>"], "hosted_owners": ["<other-account>"], "active_since": "2026-01-01", "runner_machine": "farm-01"}| Key | Meaning |
|---|---|
farm_owners |
Accounts whose private, active repos with CI move to the farm. |
hosted_owners |
Accounts that stay on GitHub-hosted Actions. Each of their managed repos is declared hosted, so a later fleet-wide reconcile cannot move it. |
active_since |
The dormancy criterion: a repo last pushed before this date is not converted. Default: 1 January of the current year. |
runner_machine |
The machines.json id that runners are registered on. apply needs it to register a runner. |
runner_label |
The label that converted repos are declared with. Default: self-hosted-bob. |
With no policy file, farm_owners is the operator’s forge.owner and nothing is declared hosted.
Any other key is refused. A name-prefix rule cannot be expressed in the policy at all.
Classification
Section titled “Classification”Every repo of every policy owner is listed from gh repo list at run time. The first rule
that matches decides the class:
| Rule | Class | Why |
|---|---|---|
| archived | skip | Archived repos run nothing. |
owner in hosted_owners |
hosted | Declared hosted: separate account and billing. |
| public (live visibility) | excluded | A self-hosted runner on a public repo lets any fork PR run code on the fleet. |
owner not in farm_owners |
skip | Outside the policy. |
manifest declares hosted |
skip | An explicit declaration wins. This is also how a rollback stays rolled back. |
| no workflows | skip | Nothing to run. |
last push before active_since |
skip | Dormant. The last push and the criterion are printed. |
| otherwise | farm |
The split never reads a repo’s name. Repos named for one account’s product often live under another account, so a name rule would misroute them.
A repo is matched to its provision manifest by the manifest checkout’s origin remote. When
the checkout is on another machine, the match falls back to the exact repo name, marked ~.
The fallback is refused when that name exists under more than one listed owner, because
the manifest cannot say which repo it means. Run the rollout on the machine that has the
checkout instead.
Commands
Section titled “Commands”node ~/.claude/scripts/fleet/ci-rollout.js inventory [--owner <o>]... [--policy <file>] [--json]node ~/.claude/scripts/fleet/ci-rollout.js apply [--owner <o>]... [--repo <owner/name>] [--policy <file>] [--apply] [--json]node ~/.claude/scripts/fleet/ci-rollout.js verify [--owner <o>]... [--repo <owner/name>] [--policy <file>] [--json]node ~/.claude/scripts/fleet/ci-rollout.js rollback --repo <owner/name> [--policy <file>] [--apply] [--json]apply converges each farm or hosted repo in three steps. Each step reads the repo first
and reports ok when it is already right, so a second pass writes nothing:
| Step | Farm | Hosted |
|---|---|---|
| declare | _meta.ci.locus = farm in provisions/<project>.json (only _meta.ci changes) |
_meta.ci.locus = hosted |
| register | runner.sh install --machine <runner_machine> --repo <slug> --label <label> when no runner carries the label |
— |
| reconcile | ci-locus.js reconcile --apply → CI_RUNNER_LABEL = <label> |
CI_RUNNER_LABEL deleted |
Excluded and skipped repos are listed with their reason and never touched. A farm or hosted
repo with no manifest is blocked: run bob ready in its checkout, then re-run. Manifest edits
land in the operator repo’s working tree. Commit them there.
verify names, for each farm repo, a successful run with a job on a runner carrying the
label. A green run on hosted does not count. A converted repo is done only when it is verified.
The check also catches a repo whose workflows still say runs-on: ubuntu-latest: setting the
variable changes nothing there, so no run ever lands on the farm. Patch those workflows to
runs-on: ${{ vars.CI_RUNNER_LABEL || 'ubuntu-latest' }} (Workflow Authoring),
push, and verify again.
rollback moves one repo back to hosted: it declares hosted and reconciles the variable
away. The runner stays registered; runner.sh remove --machine <id> --repo <slug> removes it.
Exit codes
Section titled “Exit codes”| Command | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
inventory |
listed | — | usage or invalid policy | — | an owner could not be listed |
apply |
converged or applied | would change (dry run) | usage or invalid policy | a repo failed, or a public repo still points at the farm | not measured |
verify |
every farm repo verified | some unverified | usage or invalid policy | — | not measured |
rollback |
converged or applied | would change (dry run) | usage | blocked or failed | the repo could not be read |
--json prints one bob-ci-rollout/1 document.
First dry run (2026-09-29, farm-01)
Section titled “First dry run (2026-09-29, farm-01)”This dry run used farm_owners: [paulirv], hosted_owners: [example-co], active_since: 2026-01-01
and runner_machine: farm-01. It covered 84 repos:
- Farm (22): 1 converged (bigbrain), 11 would change (10 need a runner) and 10 blocked with no manifest.
- Hosted (8): 4 would be declared hosted and 4 are blocked.
flowsis blocked because its manifest matchesflowsunder both accounts by name. - Excluded (5): the public repos acfbak, loclook, loopalert, remote-decision-bridge and zipcity.
- Skipped (49): no workflows, dormant, or already declared hosted.
The live apply is attended: it writes runner registrations and repo variables across more than 20 repos.
See also
Section titled “See also”- CI Locus: the per-repo declaration, reconcile and runner-availability preflight.
- Self-hosted runner runbook:
runner.sh, capacity and hygiene. - CI health: queue time and outcomes after the rollout.