From 7d88b60d6b1db1d133dd3dee1b38af6d940f9958 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 22:32:21 -0500 Subject: [PATCH 01/13] docs: add beginner quickstart as the onboarding entry path Issue #24: a new operator no longer has to assemble the flow from architecture documents. docs/QUICKSTART.md walks what ci-fleet does, installing one controller, connecting one repository, and running and verifying one job, with advanced material (schema v3, capacity promotion, monitoring) explicitly out of the initial path. The docs index routes new operators to it first. --- docs/QUICKSTART.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++ docs/README.md | 3 ++ 2 files changed, 96 insertions(+) create mode 100644 docs/QUICKSTART.md diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 00000000..74381b1f --- /dev/null +++ b/docs/QUICKSTART.md @@ -0,0 +1,93 @@ +# Quickstart: your first fleet job + +This is the beginner path. It takes you from zero to one verified job on +your own ephemeral CI fleet, then points at advanced material. Every step +states where it runs and whether it changes anything. + +Advanced topics — schema-v3 internals, capacity promotion, health +monitoring, multi-site fleets — are linked at the end. You do not need +them for this path. + +## Step 0: What ci-fleet does + +ci-fleet runs your GitHub Actions jobs on your own Docker hosts using +**ephemeral** runners: a small controller service on each host watches +GitHub for queued jobs, starts a throwaway runner container for exactly +one job, and destroys it afterward. Runners are secret-free and +unprivileged; projects bring their own toolchains in their own +containers. Capacity (how many jobs run at once) lives in reviewed +private Git configuration, never in application workflows. + +One controller on one Docker host is a complete fleet. + +## Step 1: Install one controller + +Runs on: a fresh Linux Docker host. Changes state: yes. + +1. Check the host qualifies: Docker, Compose v2, Git, Bash, outbound + HTTPS to GitHub, no project workloads, and a declared failure + boundary (disposable or recoverable). See + [Adding a host](ADDING-A-HOST.md) sections 1-3. +2. Create the GitHub App and runner group the controller will use, and + place the host-local identity files (root-owned, mode `0600`) as in + [Adding a host](ADDING-A-HOST.md) section 5. +3. Declare one `controllers` entry in your private configuration + repository (created from the public + [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template)) + with `min_runners: 0`, `max_runners: 1`, and a pinned engine commit. +4. Apply it: + + ```bash + sudo ./scripts/install-worker-controller.sh \ + --adopt \ + --config-repo OWNER/PRIVATE-CONFIG-REPO \ + --ref RESOLVED_CONFIG_COMMIT \ + --controller YOUR-CONTROLLER-ID + ``` + +5. Verify: the same command with `--check` reports `CHECK_OK`, and the + GitHub runner group shows the scale set idle at zero runners. + +## Step 2: Connect one repository + +Runs on: GitHub + the project repository + private configuration. +Changes state: yes, but no host changes — onboarding never touches a +fleet host. + +1. Add the repository to the pool's `allowed_repositories` in private + configuration and validate with `./scripts/validate.sh --strict`. +2. Authorize the repository in the GitHub runner group. +3. Give the project a workflow whose jobs use the pool's shared routing + label (for example `runs-on: docker-ci`). + +Full contract: [Adding a project](ADDING-A-PROJECT.md). + +## Step 3: Run and verify one job + +Runs on: GitHub. Changes state: no (ordinary CI only). + +1. Trigger one trivial job — the fleet canary's + `Fleet concurrency canary` workflow with count `1`, or a one-step + project job — with `runs-on` set to the shared label. +2. Verify from the job's metadata: it ran on your scale set + (runner name prefix matches the scale-set name), succeeded, and the + ephemeral runner disappeared afterward. +3. Verify the controller returned to zero idle runners. + +If the job stays queued: compare the job's complete `runs-on` expression +against the scale set's configured routing label, then check runner-group +repository authorization. A label or group mismatch is a configuration +bug, not a capacity problem. + +## Step 4: Next steps (advanced) + +- [Git-authored controller desired state](DESIRED-STATE.md) — the + schema-v3 model, lifecycle states, checkpoints, and rollback. +- [Capacity promotion](CAPACITY-PROMOTION.md) — raising + `max_runners`/budgets safely. +- [Fleet health monitoring](HEALTH-MONITORING.md) — heartbeats and + missed-heartbeat detection. +- [Migrating existing CI](MIGRATING-EXISTING-CI.md) — moving a real + project's test suite onto the fleet. +- [Architecture](ARCHITECTURE.md) and + [Project CI standard](PROJECT-STANDARD.md) — the full contract. diff --git a/docs/README.md b/docs/README.md index 3bddefe2..9d7ec9a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,8 +4,11 @@ Use this index to find ci-fleet concepts, requirements, examples, and step-by-st ## Start here +New operator? Follow the [Quickstart](QUICKSTART.md): what ci-fleet does, install one controller, connect one repository, run and verify one job. Everything below is reference. + | I want to… | Read | | --- | --- | +| Complete the beginner path end to end | [Quickstart](QUICKSTART.md) | | Understand the whole system | [Architecture](ARCHITECTURE.md) | | Understand how ephemeral runners are created | [Runner controller design](CONTROLLER-DESIGN.md) | | Decide whether it fits my infrastructure | [Architecture](ARCHITECTURE.md) and the root [README](../README.md) | From 2f0c5e343e725cef58e0e7b43196e44059e8e257 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 22:51:52 -0500 Subject: [PATCH 02/13] docs: address Codex review on quickstart - --install for fresh hosts (--adopt is migration-only) - runners described as host-privileged (Docker socket), secret-free only for long-lived controller credentials - scale-set verification via controller evidence, not runner-name prefix - read-only Git credential / pinned-checkout prerequisite before install - isolated first-job proof retained via LIVE-PILOT.md - GitHub App Self-hosted runners permission requirement - installer management prerequisites (python3, systemd, tar, flock) Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 53 +++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 74381b1f..b7f67bb2 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -13,10 +13,13 @@ them for this path. ci-fleet runs your GitHub Actions jobs on your own Docker hosts using **ephemeral** runners: a small controller service on each host watches GitHub for queued jobs, starts a throwaway runner container for exactly -one job, and destroys it afterward. Runners are secret-free and -unprivileged; projects bring their own toolchains in their own -containers. Capacity (how many jobs run at once) lives in reviewed -private Git configuration, never in application workflows. +one job, and destroys it afterward. Runners are secret-free — long-lived +controller credentials are never exposed to jobs — but they are +**host-privileged**: each job gets the host Docker socket, which is +host-root-equivalent, so only trusted private repositories may ever be +authorized. Projects bring their own toolchains in their own containers. +Capacity (how many jobs run at once) lives in reviewed private Git +configuration, never in application workflows. One controller on one Docker host is a complete fleet. @@ -27,25 +30,38 @@ Runs on: a fresh Linux Docker host. Changes state: yes. 1. Check the host qualifies: Docker, Compose v2, Git, Bash, outbound HTTPS to GitHub, no project workloads, and a declared failure boundary (disposable or recoverable). See - [Adding a host](ADDING-A-HOST.md) sections 1-3. -2. Create the GitHub App and runner group the controller will use, and - place the host-local identity files (root-owned, mode `0600`) as in - [Adding a host](ADDING-A-HOST.md) section 5. + [Adding a host](ADDING-A-HOST.md) sections 1-3. The installer itself + additionally requires a systemd-based host with `python3`, `tar`, + `install`, `flock`, and standard coreutils — it fails closed with a + named missing command if one is absent. +2. Create the GitHub App and runner group the controller will use. The + App must be installed on the organization with organization-level + **Self-hosted runners: Read and write** permission; the concrete + bootstrap settings are in [Live pilot runbook](LIVE-PILOT.md) + sections 2-3. Then place the host-local identity files (root-owned, + mode `0600`) as in [Adding a host](ADDING-A-HOST.md) section 5. 3. Declare one `controllers` entry in your private configuration repository (created from the public [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template)) with `min_runners: 0`, `max_runners: 1`, and a pinned engine commit. -4. Apply it: +4. Give the host a way to read that private repository: either a + narrowly scoped host-local read-only Git credential, or a temporary + pinned Git bundle / local checkout transferred from your management + machine (the installer accepts a local checkout path as + `--config-repo`). Without one of these, the installer's + noninteractive fetch fails closed. +5. Apply it (`--install` for a fresh host; `--adopt` is only for + converting an existing manually installed controller): ```bash sudo ./scripts/install-worker-controller.sh \ - --adopt \ + --install \ --config-repo OWNER/PRIVATE-CONFIG-REPO \ --ref RESOLVED_CONFIG_COMMIT \ --controller YOUR-CONTROLLER-ID ``` -5. Verify: the same command with `--check` reports `CHECK_OK`, and the +6. Verify: the same command with `--check` reports `CHECK_OK`, and the GitHub runner group shows the scale set idle at zero runners. ## Step 2: Connect one repository @@ -69,10 +85,17 @@ Runs on: GitHub. Changes state: no (ordinary CI only). 1. Trigger one trivial job — the fleet canary's `Fleet concurrency canary` workflow with count `1`, or a one-step project job — with `runs-on` set to the shared label. -2. Verify from the job's metadata: it ran on your scale set - (runner name prefix matches the scale-set name), succeeded, and the - ephemeral runner disappeared afterward. -3. Verify the controller returned to zero idle runners. +2. Verify the job ran on your fleet: the Actions job metadata names the + runner, and the controller host's logs show that runner was created + by your scale set. (Runner names derive from the controller ID, not + necessarily the scale-set name, so use controller or scale-set + evidence rather than a name-prefix guess.) +3. Complete the isolated first-job proof from + [Live pilot runbook](LIVE-PILOT.md): read-only job permissions, + controller health, scoped cleanup, and zero remaining job-owned + containers, networks, or volumes. Job success plus a returned-to-zero + runner count alone does not prove the host is clean. +4. Verify the controller returned to zero idle runners. If the job stays queued: compare the job's complete `runs-on` expression against the scale set's configured routing label, then check runner-group From cfbbf10b439a5cb8d428f6dbdb7a7592ac1307e5 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 23:00:28 -0500 Subject: [PATCH 03/13] docs: address second Codex review on quickstart - runners no longer called secret-free; short-lived registration config and job secrets acknowledged - explicit minimal GITHUB_TOKEN permissions in starter workflow guidance - engine checkout must be the pinned reviewed commit before running the installer as root - private config must be fully initialized via init.sh, not only the controllers entry - first-job pointer now targets the in-repo live-pilot example - step 3 correctly attributes execution to the fleet host and transient Docker state Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index b7f67bb2..96c45fa6 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -13,8 +13,10 @@ them for this path. ci-fleet runs your GitHub Actions jobs on your own Docker hosts using **ephemeral** runners: a small controller service on each host watches GitHub for queued jobs, starts a throwaway runner container for exactly -one job, and destroys it afterward. Runners are secret-free — long-lived -controller credentials are never exposed to jobs — but they are +one job, and destroys it afterward. Long-lived controller credentials +are never exposed to jobs — but runners do receive short-lived +registration configuration, and a job can receive `GITHUB_TOKEN` or +explicitly configured project secrets. Runners are also **host-privileged**: each job gets the host Docker socket, which is host-root-equivalent, so only trusted private repositories may ever be authorized. Projects bring their own toolchains in their own containers. @@ -40,21 +42,30 @@ Runs on: a fresh Linux Docker host. Changes state: yes. bootstrap settings are in [Live pilot runbook](LIVE-PILOT.md) sections 2-3. Then place the host-local identity files (root-owned, mode `0600`) as in [Adding a host](ADDING-A-HOST.md) section 5. -3. Declare one `controllers` entry in your private configuration - repository (created from the public - [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template)) - with `min_runners: 0`, `max_runners: 1`, and a pinned engine commit. +3. Create your private configuration repository from the public + [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template) + and initialize it completely: run its `./scripts/init.sh` so the + fictional `example-org`/`example-app` placeholders are replaced, then + declare one `controllers` entry with `min_runners: 0`, + `max_runners: 1`, and a pinned engine commit. Validate with + `./scripts/validate.sh --strict` before continuing. 4. Give the host a way to read that private repository: either a narrowly scoped host-local read-only Git credential, or a temporary pinned Git bundle / local checkout transferred from your management machine (the installer accepts a local checkout path as `--config-repo`). Without one of these, the installer's noninteractive fetch fails closed. -5. Apply it (`--install` for a fresh host; `--adopt` is only for +5. Clone the public engine repository on the host and check out the + exact reviewed engine commit your private configuration pins — the + installer uses code and validators from that checkout and runs as + root, so it must be the reviewed commit, not a moving branch. Then + apply it (`--install` for a fresh host; `--adopt` is only for converting an existing manually installed controller): ```bash - sudo ./scripts/install-worker-controller.sh \ + git clone https://github.com/RandomDevelopment/ci-fleet.git + git -C ci-fleet checkout PINNED_ENGINE_COMMIT + sudo ci-fleet/scripts/install-worker-controller.sh \ --install \ --config-repo OWNER/PRIVATE-CONFIG-REPO \ --ref RESOLVED_CONFIG_COMMIT \ @@ -80,11 +91,18 @@ Full contract: [Adding a project](ADDING-A-PROJECT.md). ## Step 3: Run and verify one job -Runs on: GitHub. Changes state: no (ordinary CI only). - -1. Trigger one trivial job — the fleet canary's - `Fleet concurrency canary` workflow with count `1`, or a one-step - project job — with `runs-on` set to the shared label. +Runs on: dispatched from GitHub, but steps execute inside an ephemeral +runner on your fleet host with host-root-equivalent Docker access. +Changes state: yes, transiently — the job creates a runner container and +may create Docker containers, networks, and volumes; the proof below +verifies all of it is cleaned up. + +1. Trigger one trivial job with `runs-on` set to the shared label — + either the starter example in this repository at + `examples/workflows/live-pilot.yml.example`, or a one-step project + job. Declare `permissions: contents: read` (or another explicit + minimal set) on the workflow so the job never inherits a read-write + `GITHUB_TOKEN` default. 2. Verify the job ran on your fleet: the Actions job metadata names the runner, and the controller host's logs show that runner was created by your scale set. (Runner names derive from the controller ID, not From 7c4cef5e3760547761ae370c0e4815b83a6f27db Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 23:08:03 -0500 Subject: [PATCH 04/13] docs: address third Codex review on quickstart - trusted jobs can reach host-local secrets via the Docker socket; no absolute credential-isolation claim - engine clone/checkout moved before the identity-file step - complete init.sh command including the created runner group - merged reviewed configuration required before install; resolve merge SHA - starter job guidance uses the managed label/image, not the experimental live-pilot example Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 82 ++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 96c45fa6..d594d532 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -13,13 +13,14 @@ them for this path. ci-fleet runs your GitHub Actions jobs on your own Docker hosts using **ephemeral** runners: a small controller service on each host watches GitHub for queued jobs, starts a throwaway runner container for exactly -one job, and destroys it afterward. Long-lived controller credentials -are never exposed to jobs — but runners do receive short-lived -registration configuration, and a job can receive `GITHUB_TOKEN` or -explicitly configured project secrets. Runners are also -**host-privileged**: each job gets the host Docker socket, which is -host-root-equivalent, so only trusted private repositories may ever be -authorized. Projects bring their own toolchains in their own containers. +one job, and destroys it afterward. Runners are **host-privileged**: +each job gets the host Docker socket, which is host-root-equivalent. A +compromised job can therefore reach everything on the host, including +host-local controller credentials — this is why only trusted private +repositories may ever be authorized, and why host-local identity files +stay root-owned. Jobs also receive short-lived registration +configuration and may receive `GITHUB_TOKEN` or explicitly configured +project secrets. Projects bring their own toolchains in their own containers. Capacity (how many jobs run at once) lives in reviewed private Git configuration, never in application workflows. @@ -36,35 +37,57 @@ Runs on: a fresh Linux Docker host. Changes state: yes. additionally requires a systemd-based host with `python3`, `tar`, `install`, `flock`, and standard coreutils — it fails closed with a named missing command if one is absent. -2. Create the GitHub App and runner group the controller will use. The +2. Clone the public engine repository and check out the exact reviewed + engine commit you intend to pin — later steps copy templates from it + and run its installer as root, so it must be the reviewed commit, not + a moving branch: + + ```bash + git clone https://github.com/RandomDevelopment/ci-fleet.git + git -C ci-fleet checkout PINNED_ENGINE_COMMIT + ``` + +3. Create the GitHub App and runner group the controller will use. The App must be installed on the organization with organization-level **Self-hosted runners: Read and write** permission; the concrete bootstrap settings are in [Live pilot runbook](LIVE-PILOT.md) sections 2-3. Then place the host-local identity files (root-owned, - mode `0600`) as in [Adding a host](ADDING-A-HOST.md) section 5. -3. Create your private configuration repository from the public + mode `0600`) from the checked-out engine's templates as in + [Adding a host](ADDING-A-HOST.md) section 5. +4. Create your private configuration repository from the public [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template) - and initialize it completely: run its `./scripts/init.sh` so the - fictional `example-org`/`example-app` placeholders are replaced, then - declare one `controllers` entry with `min_runners: 0`, - `max_runners: 1`, and a pinned engine commit. Validate with - `./scripts/validate.sh --strict` before continuing. + and initialize it completely — pass the runner group you just created, + your controller ID, and the pinned engine commit, so no fictional + `example-org`/`example-app` placeholders survive: + + ```bash + ./scripts/init.sh \ + --organization YOUR-ORG \ + --project YOUR-APP \ + --controller YOUR-CONTROLLER-ID \ + --location primary-site \ + --runner-group YOUR-CREATED-RUNNER-GROUP \ + --capacity-budget 1 \ + --max-runners 1 \ + --engine-ref PINNED_ENGINE_COMMIT + ``` + + Then validate with `./scripts/validate.sh --strict`, open a pull + request, and **merge it**: managed controller lifecycle is permitted + only from a reviewed, merged private configuration commit. Resolve + and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); + do not install from an unmerged branch. 4. Give the host a way to read that private repository: either a narrowly scoped host-local read-only Git credential, or a temporary pinned Git bundle / local checkout transferred from your management machine (the installer accepts a local checkout path as `--config-repo`). Without one of these, the installer's noninteractive fetch fails closed. -5. Clone the public engine repository on the host and check out the - exact reviewed engine commit your private configuration pins — the - installer uses code and validators from that checkout and runs as - root, so it must be the reviewed commit, not a moving branch. Then - apply it (`--install` for a fresh host; `--adopt` is only for - converting an existing manually installed controller): +5. Apply the merged configuration from the reviewed engine checkout + (`--install` for a fresh host; `--adopt` is only for converting an + existing manually installed controller): ```bash - git clone https://github.com/RandomDevelopment/ci-fleet.git - git -C ci-fleet checkout PINNED_ENGINE_COMMIT sudo ci-fleet/scripts/install-worker-controller.sh \ --install \ --config-repo OWNER/PRIVATE-CONFIG-REPO \ @@ -97,12 +120,13 @@ Changes state: yes, transiently — the job creates a runner container and may create Docker containers, networks, and volumes; the proof below verifies all of it is cleaned up. -1. Trigger one trivial job with `runs-on` set to the shared label — - either the starter example in this repository at - `examples/workflows/live-pilot.yml.example`, or a one-step project - job. Declare `permissions: contents: read` (or another explicit - minimal set) on the workflow so the job never inherits a read-write - `GITHUB_TOKEN` default. +1. Trigger one trivial one-step project job with `runs-on` set to the + shared label and `permissions: contents: read` declared explicitly, + so the job never inherits a read-write `GITHUB_TOKEN` default. (The + older `examples/workflows/live-pilot.yml.example` targets the + experimental label and the `:dev` runner image; on a managed install + use your pool's shared label and the controller-rendered image tag + instead of copying it unchanged.) 2. Verify the job ran on your fleet: the Actions job metadata names the runner, and the controller host's logs show that runner was created by your scale set. (Runner names derive from the controller ID, not From 1d204bb0acc5452db519c0e9e2d951ff6f8855c2 Mon Sep 17 00:00:00 2001 From: Nickfosts Hermes Date: Sun, 26 Jul 2026 23:08:33 -0500 Subject: [PATCH 05/13] docs: close quickstart trust-boundary gaps --- docs/QUICKSTART.md | 46 ++++++++++++++--------- examples/workflows/live-pilot.yml.example | 10 ++--- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index b7f67bb2..e151d2e8 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -13,11 +13,12 @@ them for this path. ci-fleet runs your GitHub Actions jobs on your own Docker hosts using **ephemeral** runners: a small controller service on each host watches GitHub for queued jobs, starts a throwaway runner container for exactly -one job, and destroys it afterward. Runners are secret-free — long-lived -controller credentials are never exposed to jobs — but they are -**host-privileged**: each job gets the host Docker socket, which is -host-root-equivalent, so only trusted private repositories may ever be -authorized. Projects bring their own toolchains in their own containers. +one job, and destroys it afterward. Jobs never receive the controller's +long-lived credential, but they can receive short-lived registration +configuration, `GITHUB_TOKEN`, and explicitly configured project secrets. +They are also **host-privileged**: each job gets the host Docker socket, +which is host-root-equivalent, so only trusted private repositories may +ever be authorized. Projects bring their own toolchains in their own containers. Capacity (how many jobs run at once) lives in reviewed private Git configuration, never in application workflows. @@ -40,17 +41,23 @@ Runs on: a fresh Linux Docker host. Changes state: yes. bootstrap settings are in [Live pilot runbook](LIVE-PILOT.md) sections 2-3. Then place the host-local identity files (root-owned, mode `0600`) as in [Adding a host](ADDING-A-HOST.md) section 5. -3. Declare one `controllers` entry in your private configuration - repository (created from the public - [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template)) - with `min_runners: 0`, `max_runners: 1`, and a pinned engine commit. +3. Create the private configuration repository from the public + [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template), + then run its `scripts/init.sh` as described in that repository's README. + Review the complete generated `fleet.json` and replace every fictional + organization, project, runner-group, and controller value; editing only + `controllers` leaves unsafe example mappings behind. Start with + `min_runners: 0`, `max_runners: 1`, and a pinned engine commit. 4. Give the host a way to read that private repository: either a narrowly scoped host-local read-only Git credential, or a temporary pinned Git bundle / local checkout transferred from your management machine (the installer accepts a local checkout path as `--config-repo`). Without one of these, the installer's noninteractive fetch fails closed. -5. Apply it (`--install` for a fresh host; `--adopt` is only for +5. Check out the engine at the exact reviewed commit declared by the + private configuration and verify `git rev-parse HEAD` matches it. Do not + run the root installer from an unreviewed checkout or moving branch. +6. Apply it (`--install` for a fresh host; `--adopt` is only for converting an existing manually installed controller): ```bash @@ -61,7 +68,7 @@ Runs on: a fresh Linux Docker host. Changes state: yes. --controller YOUR-CONTROLLER-ID ``` -6. Verify: the same command with `--check` reports `CHECK_OK`, and the +7. Verify: the same command with `--check` reports `CHECK_OK`, and the GitHub runner group shows the scale set idle at zero runners. ## Step 2: Connect one repository @@ -73,18 +80,23 @@ fleet host. 1. Add the repository to the pool's `allowed_repositories` in private configuration and validate with `./scripts/validate.sh --strict`. 2. Authorize the repository in the GitHub runner group. -3. Give the project a workflow whose jobs use the pool's shared routing - label (for example `runs-on: docker-ci`). +3. Copy `examples/workflows/live-pilot.yml.example` from the reviewed + engine checkout into the project, keep its explicit read-only + `permissions`, and set `runs-on` to the pool's shared routing label (for + example `docker-ci`). Do not rely on the repository's default token + permissions. Full contract: [Adding a project](ADDING-A-PROJECT.md). ## Step 3: Run and verify one job -Runs on: GitHub. Changes state: no (ordinary CI only). +Dispatched through: GitHub. Job steps run inside the ephemeral runner on +the fleet Docker host with host-root-equivalent socket access. Changes +state: yes — the controller creates a runner container and the proof job +creates scoped Docker resources, all of which must be removed afterward. -1. Trigger one trivial job — the fleet canary's - `Fleet concurrency canary` workflow with count `1`, or a one-step - project job — with `runs-on` set to the shared label. +1. Dispatch the copied first-job lifecycle proof once with `runs-on` set + to the shared label. 2. Verify the job ran on your fleet: the Actions job metadata names the runner, and the controller host's logs show that runner was created by your scale set. (Runner names derive from the controller ID, not diff --git a/examples/workflows/live-pilot.yml.example b/examples/workflows/live-pilot.yml.example index 805de994..db9c27ba 100644 --- a/examples/workflows/live-pilot.yml.example +++ b/examples/workflows/live-pilot.yml.example @@ -1,6 +1,6 @@ # Copy this file into one explicitly selected trusted private repository. -# Do not enable the ci-fleet runner group for this public template repository. -name: Experimental live runner pilot +# Do not enable the ci-fleet runner group for this public repository. +name: Fleet first-job lifecycle proof on: workflow_dispatch: @@ -9,13 +9,13 @@ permissions: contents: read concurrency: - group: experimental-live-runner-pilot + group: ci-fleet-first-job-proof cancel-in-progress: false jobs: pilot: name: Read-only one-job lifecycle proof - runs-on: docker-ci-experimental + runs-on: docker-ci timeout-minutes: 10 steps: - name: Check out without retaining credentials @@ -33,7 +33,7 @@ jobs: - name: Prove run-scoped Docker cleanup run: | set -Eeuo pipefail - name="ci-fleet-pilot-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + name="ci-fleet-proof-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" cleanup() { docker rm -f "$name" >/dev/null 2>&1 || true docker volume rm "$name" >/dev/null 2>&1 || true From 57709b0fd748f066a32b6983f8f331929136c209 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 23:22:17 -0500 Subject: [PATCH 06/13] docs: address fourth Codex review on quickstart - engine pin must be merged and publicly reachable - split step 1 between management workstation and fleet host; keep write credentials off the host - local config checkout must persist for scheduled drift checks - starter job: timeout-minutes 5, no nested runner-image pinning - initialized repository is confirmed, not added twice Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index d594d532..dc69e232 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -28,7 +28,11 @@ One controller on one Docker host is a complete fleet. ## Step 1: Install one controller -Runs on: a fresh Linux Docker host. Changes state: yes. +Runs on: split between a management workstation (Git and GitHub work) +and the fresh Linux Docker host (installation only). Changes state: yes. +Keep repository-writing credentials off the fleet host: anything with +write access retained there is reachable by later host-root-equivalent +jobs through the Docker socket. 1. Check the host qualifies: Docker, Compose v2, Git, Bash, outbound HTTPS to GitHub, no project workloads, and a declared failure @@ -40,11 +44,14 @@ Runs on: a fresh Linux Docker host. Changes state: yes. 2. Clone the public engine repository and check out the exact reviewed engine commit you intend to pin — later steps copy templates from it and run its installer as root, so it must be the reviewed commit, not - a moving branch: + a moving branch. The pin must be a commit merged into and reachable + from the engine's public default branch; never pin an unmerged local + branch tip. ```bash git clone https://github.com/RandomDevelopment/ci-fleet.git git -C ci-fleet checkout PINNED_ENGINE_COMMIT + git -C ci-fleet merge-base --is-ancestor HEAD origin/main && echo merged ``` 3. Create the GitHub App and runner group the controller will use. The @@ -78,11 +85,13 @@ Runs on: a fresh Linux Docker host. Changes state: yes. and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); do not install from an unmerged branch. 4. Give the host a way to read that private repository: either a - narrowly scoped host-local read-only Git credential, or a temporary - pinned Git bundle / local checkout transferred from your management - machine (the installer accepts a local checkout path as + narrowly scoped host-local read-only Git credential, or a pinned + local checkout transferred from your management machine via a + temporary Git bundle (the installer accepts a local checkout path as `--config-repo`). Without one of these, the installer's - noninteractive fetch fails closed. + noninteractive fetch fails closed. If you use the local-checkout + path, keep that checkout in place afterward — the recorded path is + reused by scheduled drift checks — and remove only the bundle. 5. Apply the merged configuration from the reviewed engine checkout (`--install` for a fresh host; `--adopt` is only for converting an existing manually installed controller): @@ -104,8 +113,10 @@ Runs on: GitHub + the project repository + private configuration. Changes state: yes, but no host changes — onboarding never touches a fleet host. -1. Add the repository to the pool's `allowed_repositories` in private - configuration and validate with `./scripts/validate.sh --strict`. +1. Confirm the repository you passed as `--project` to `init.sh` is in + the pool's `allowed_repositories` (the initializer put it there; add + it only if you skipped initialization), and validate with + `./scripts/validate.sh --strict`. 2. Authorize the repository in the GitHub runner group. 3. Give the project a workflow whose jobs use the pool's shared routing label (for example `runs-on: docker-ci`). @@ -121,12 +132,16 @@ may create Docker containers, networks, and volumes; the proof below verifies all of it is cleaned up. 1. Trigger one trivial one-step project job with `runs-on` set to the - shared label and `permissions: contents: read` declared explicitly, - so the job never inherits a read-write `GITHUB_TOKEN` default. (The - older `examples/workflows/live-pilot.yml.example` targets the - experimental label and the `:dev` runner image; on a managed install - use your pool's shared label and the controller-rendered image tag - instead of copying it unchanged.) + shared label, `permissions: contents: read` declared explicitly, and + `timeout-minutes: 5`, so the job never inherits a read-write + `GITHUB_TOKEN` default and cannot occupy the single runner past the + ordinary-CI ceiling. Do not copy a nested runner image reference into + the job: image tags derive from each controller's engine pin, and a + workflow hard-coding one host's tag can be routed to a host where + that image does not exist. (The older + `examples/workflows/live-pilot.yml.example` targets the experimental + label and the `:dev` runner image and is not a managed-install + starter.) 2. Verify the job ran on your fleet: the Actions job metadata names the runner, and the controller host's logs show that runner was created by your scale set. (Runner names derive from the controller ID, not From 45bc6b8abccc1ae36cb0f2b0136662ff30f2de12 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 26 Jul 2026 23:48:03 -0500 Subject: [PATCH 07/13] docs: address fifth Codex review on quickstart - stage the dispatch-only proof workflow before runner-group authorization so no push-triggered job becomes the first job - executable managed-install cleanup proof (managed-label containers, run-scoped networks/volumes, health, CHECK_OK) - engine clone happens on the fleet host (public repo, no write cred) - root-scoped read-only credential requirement (sudo + drift service) - private config initialized from the pinned engine's vendored template - --repository passed separately from the project slug Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 146 +++++++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 57 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 48f63d6d..c13f327b 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -41,12 +41,13 @@ jobs through the Docker socket. additionally requires a systemd-based host with `python3`, `tar`, `install`, `flock`, and standard coreutils — it fails closed with a named missing command if one is absent. -2. Clone the public engine repository and check out the exact reviewed - engine commit you intend to pin — later steps copy templates from it - and run its installer as root, so it must be the reviewed commit, not - a moving branch. The pin must be a commit merged into and reachable - from the engine's public default branch; never pin an unmerged local - branch tip. +2. On the **fleet host**, clone the public engine repository and check + out the exact reviewed engine commit you intend to pin — later steps + copy templates from it and run its installer as root, so it must be + the reviewed commit, not a moving branch. The pin must be a commit + merged into and reachable from the engine's public default branch; + never pin an unmerged local branch tip. Cloning the public engine + needs no repository-writing credential. ```bash git clone https://github.com/RandomDevelopment/ci-fleet.git @@ -54,23 +55,28 @@ jobs through the Docker socket. git -C ci-fleet merge-base --is-ancestor HEAD origin/main && echo merged ``` -3. Create the GitHub App and runner group the controller will use. The - App must be installed on the organization with organization-level +3. On the **management workstation** (GitHub web UI), create the GitHub + App and runner group the controller will use. The App must be + installed on the organization with organization-level **Self-hosted runners: Read and write** permission; the concrete bootstrap settings are in [Live pilot runbook](LIVE-PILOT.md) - sections 2-3. Then place the host-local identity files (root-owned, - mode `0600`) from the checked-out engine's templates as in - [Adding a host](ADDING-A-HOST.md) section 5. -4. Create your private configuration repository from the public - [configuration template](https://github.com/RandomDevelopment/ci-fleet-config-template) - and initialize it completely — pass the runner group you just created, - your controller ID, and the pinned engine commit, so no fictional - `example-org`/`example-app` placeholders survive: + sections 2-3. Then, on the **fleet host**, place the host-local + identity files (root-owned, mode `0600`) from the checked-out + engine's templates as in [Adding a host](ADDING-A-HOST.md) section 5. +4. On the **management workstation**, create your private configuration + repository from the configuration template **versioned inside the + pinned engine checkout** — the schema, initializer, and validator the + pinned engine actually enforces live under its + `templates/config-repository`, while the standalone public template + may have advanced past your engine pin: ```bash + cp -r ci-fleet/templates/config-repository my-fleet-config + cd my-fleet-config && git init -q && git add -A && git commit -qm init ./scripts/init.sh \ --organization YOUR-ORG \ - --project YOUR-APP \ + --project YOUR-APP-SLUG \ + --repository YOUR-ORG/ACTUAL-REPOSITORY-NAME \ --controller YOUR-CONTROLLER-ID \ --location primary-site \ --runner-group YOUR-CREATED-RUNNER-GROUP \ @@ -79,22 +85,29 @@ jobs through the Docker socket. --engine-ref PINNED_ENGINE_COMMIT ``` - Then validate with `./scripts/validate.sh --strict`, open a pull - request, and **merge it**: managed controller lifecycle is permitted - only from a reviewed, merged private configuration commit. Resolve - and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); - do not install from an unmerged branch. -5. Give the host a way to read that private repository: either a - narrowly scoped host-local read-only Git credential, or a pinned - local checkout transferred from your management machine via a - temporary Git bundle (the installer accepts a local checkout path as - `--config-repo`). Without one of these, the installer's - noninteractive fetch fails closed. If you use the local-checkout - path, keep that checkout in place afterward — the recorded path is - reused by scheduled drift checks — and remove only the bundle. -6. Apply the merged configuration from the reviewed engine checkout - (`--install` for a fresh host; `--adopt` is only for converting an - existing manually installed controller): + `--project` is a logical lowercase slug; pass the real GitHub + repository name separately with `--repository` so + `allowed_repositories` names a repository that exists. Validate with + `./scripts/validate.sh --strict`, push to a new **private** GitHub + repository, open a pull request, and **merge it**: managed controller + lifecycle is permitted only from a reviewed, merged private + configuration commit. Resolve and record that merge commit SHA + (`RESOLVED_CONFIG_COMMIT` below); do not install from an unmerged + branch. +5. Give the fleet host a way to read that private repository **as + root**: the installer fetches under `sudo` and the recurring drift + check runs as `User=root`, so a credential in an operator's + user-scoped Git configuration is invisible to both. Either configure + a narrowly scoped read-only credential in root's Git configuration, + or transfer a pinned local checkout from your management workstation + via a temporary Git bundle (the installer accepts a local checkout + path as `--config-repo`). Without one of these, the noninteractive + fetch fails closed. If you use the local-checkout path, keep that + checkout in place afterward — the recorded path is reused by + scheduled drift checks — and remove only the bundle. +6. On the **fleet host**, apply the merged configuration from the + reviewed engine checkout (`--install` for a fresh host; `--adopt` is + only for converting an existing manually installed controller): ```bash sudo ci-fleet/scripts/install-worker-controller.sh \ @@ -109,17 +122,19 @@ jobs through the Docker socket. ## Step 2: Connect one repository -Runs on: GitHub + the project repository + private configuration. -Changes state: yes, but no host changes — onboarding never touches a -fleet host. - -1. Confirm the repository you passed as `--project` to `init.sh` is in - the pool's `allowed_repositories` (the initializer put it there; add - it only if you skipped initialization), and validate with +Runs on: management workstation (GitHub + the project repository + +private configuration). Changes state: yes, but no host changes — +onboarding never touches a fleet host. + +1. Stage the proof workflow first: commit the trivial + `workflow_dispatch`-only job from Step 3 to the project repository. + Do this **before** authorization so the first eligible job is the + controlled proof, never a push-triggered or previously queued job. +2. Confirm the repository you passed as `--repository` to `init.sh` is + in the pool's `allowed_repositories` (the initializer put it there; + add it only if you skipped initialization), and validate with `./scripts/validate.sh --strict`. -2. Authorize the repository in the GitHub runner group. -3. Give the project a workflow whose jobs use the pool's shared routing - label (for example `runs-on: docker-ci`). +3. Authorize the repository in the GitHub runner group. Full contract: [Adding a project](ADDING-A-PROJECT.md). @@ -131,14 +146,14 @@ Changes state: yes, transiently — the job creates a runner container and may create Docker containers, networks, and volumes; the proof below verifies all of it is cleaned up. -1. Trigger one trivial one-step project job with `runs-on` set to the - shared label, `permissions: contents: read` declared explicitly, and - `timeout-minutes: 5`, so the job never inherits a read-write - `GITHUB_TOKEN` default and cannot occupy the single runner past the - ordinary-CI ceiling. Do not copy a nested runner image reference into - the job: image tags derive from each controller's engine pin, and a - workflow hard-coding one host's tag can be routed to a host where - that image does not exist. (The older +1. Dispatch the staged proof workflow. It should be one trivial job + with `runs-on` set to the shared label, `permissions: contents: read` + declared explicitly, and `timeout-minutes: 5`, so the job never + inherits a read-write `GITHUB_TOKEN` default and cannot occupy the + single runner past the ordinary-CI ceiling. Do not copy a nested + runner image reference into the job: image tags derive from each + controller's engine pin, and a workflow hard-coding one host's tag + can be routed to a host where that image does not exist. (The older `examples/workflows/live-pilot.yml.example` targets the experimental label and the `:dev` runner image and is not a managed-install starter.) @@ -147,12 +162,29 @@ verifies all of it is cleaned up. by your scale set. (Runner names derive from the controller ID, not necessarily the scale-set name, so use controller or scale-set evidence rather than a name-prefix guess.) -3. Complete the isolated first-job proof from - [Live pilot runbook](LIVE-PILOT.md): read-only job permissions, - controller health, scoped cleanup, and zero remaining job-owned - containers, networks, or volumes. Job success plus a returned-to-zero - runner count alone does not prove the host is clean. -4. Verify the controller returned to zero idle runners. +3. On the fleet host, prove cleanup and health explicitly — job success + plus a returned-to-zero runner count alone does not prove the host + is clean: + + ```bash + # No fleet-managed containers remain: + sudo docker ps -aq \ + --filter label=io.randomdevelopment.ci-fleet.managed=true + # No run-scoped networks or volumes remain (name prefix ci-fleet--): + sudo docker network ls -q --filter "name=^ci-fleet-" + sudo docker volume ls -q --filter "name=^ci-fleet-" + # Controller is healthy and idle: + sudo systemctl status ci-fleet-health.service --no-pager + sudo ci-fleet/scripts/install-worker-controller.sh \ + --check --config-repo OWNER/PRIVATE-CONFIG-REPO \ + --ref RESOLVED_CONFIG_COMMIT --controller YOUR-CONTROLLER-ID + ``` + + Every listing above must be empty and the check must report + `CHECK_OK`. The [Live pilot runbook](LIVE-PILOT.md) documents the + full isolated first-job proof including read-only job permissions. +4. Verify the controller returned to zero idle runners in the GitHub + runner group. If the job stays queued: compare the job's complete `runs-on` expression against the scale set's configured routing label, then check runner-group From 5dc0db87cb05e953626695b13e222027b73c7f8d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 27 Jul 2026 00:02:25 -0500 Subject: [PATCH 08/13] docs: address sixth Codex review on quickstart - commit the initialized fleet.json before pushing (was committed pre-init) - include the dispatch-only proof workflow YAML referenced by step 2 - filter cleanup check by kind=runner so the persistent controller is not flagged; search ci-* project-owned resource names per the PROJECT-STANDARD naming contract - start a fresh health oneshot after the proof job instead of reading the previous timer result - clone the pinned engine on the management workstation before copying the vendored template Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 78 +++++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index c13f327b..c02d7bb8 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -70,9 +70,15 @@ jobs through the Docker socket. `templates/config-repository`, while the standalone public template may have advanced past your engine pin: + On the management workstation, clone the same pinned engine commit + (the previous clone lives on the fleet host), then copy its vendored + template: + ```bash + git clone -q https://github.com/RandomDevelopment/ci-fleet.git + git -C ci-fleet checkout PINNED_ENGINE_COMMIT cp -r ci-fleet/templates/config-repository my-fleet-config - cd my-fleet-config && git init -q && git add -A && git commit -qm init + cd my-fleet-config ./scripts/init.sh \ --organization YOUR-ORG \ --project YOUR-APP-SLUG \ @@ -87,9 +93,18 @@ jobs through the Docker socket. `--project` is a logical lowercase slug; pass the real GitHub repository name separately with `--repository` so - `allowed_repositories` names a repository that exists. Validate with - `./scripts/validate.sh --strict`, push to a new **private** GitHub - repository, open a pull request, and **merge it**: managed controller + `allowed_repositories` names a repository that exists. Commit the + initialized result, validate, push to a new **private** GitHub + repository, open a pull request, and **merge it**: + + ```bash + git init -q && git add -A && git commit -qm "Initialize fleet configuration" + ./scripts/validate.sh --strict + ``` + + The commit must contain the initialized `fleet.json`; validating the + working tree without committing it would push the untouched example + configuration. Managed controller managed controller lifecycle is permitted only from a reviewed, merged private configuration commit. Resolve and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); do not install from an unmerged @@ -126,10 +141,24 @@ Runs on: management workstation (GitHub + the project repository + private configuration). Changes state: yes, but no host changes — onboarding never touches a fleet host. -1. Stage the proof workflow first: commit the trivial - `workflow_dispatch`-only job from Step 3 to the project repository. +1. Stage the proof workflow first: commit the dispatch-only job below + to the project repository as `.github/workflows/fleet-proof.yml`. Do this **before** authorization so the first eligible job is the controlled proof, never a push-triggered or previously queued job. + + ```yaml + name: Fleet first-job proof + on: + workflow_dispatch: + permissions: + contents: read + jobs: + proof: + runs-on: docker-ci # your pool's shared routing label + timeout-minutes: 5 + steps: + - run: echo "fleet proof on ${RUNNER_NAME:-unknown}" + ``` 2. Confirm the repository you passed as `--repository` to `init.sh` is in the pool's `allowed_repositories` (the initializer put it there; add it only if you skipped initialization), and validate with @@ -146,12 +175,11 @@ Changes state: yes, transiently — the job creates a runner container and may create Docker containers, networks, and volumes; the proof below verifies all of it is cleaned up. -1. Dispatch the staged proof workflow. It should be one trivial job - with `runs-on` set to the shared label, `permissions: contents: read` - declared explicitly, and `timeout-minutes: 5`, so the job never - inherits a read-write `GITHUB_TOKEN` default and cannot occupy the +1. Dispatch the staged proof workflow. Its explicit + `permissions: contents: read` and `timeout-minutes: 5` keep the job + from inheriting a read-write `GITHUB_TOKEN` default or occupying the single runner past the ordinary-CI ceiling. Do not copy a nested - runner image reference into the job: image tags derive from each + runner image reference into a job: image tags derive from each controller's engine pin, and a workflow hard-coding one host's tag can be routed to a host where that image does not exist. (The older `examples/workflows/live-pilot.yml.example` targets the experimental @@ -167,22 +195,30 @@ verifies all of it is cleaned up. is clean: ```bash - # No fleet-managed containers remain: + # No ephemeral runner containers remain (the long-lived controller + # container is also fleet-managed, so filter by kind=runner): sudo docker ps -aq \ - --filter label=io.randomdevelopment.ci-fleet.managed=true - # No run-scoped networks or volumes remain (name prefix ci-fleet--): - sudo docker network ls -q --filter "name=^ci-fleet-" - sudo docker volume ls -q --filter "name=^ci-fleet-" - # Controller is healthy and idle: - sudo systemctl status ci-fleet-health.service --no-pager + --filter label=io.randomdevelopment.ci-fleet.managed=true \ + --filter label=io.randomdevelopment.ci-fleet.kind=runner + # No run-owned project resources remain. Compliant jobs name Compose + # projects ci-----: + sudo docker ps -aq --filter "name=^ci-" + sudo docker network ls -q --filter "name=^ci-" + sudo docker volume ls -q --filter "name=^ci-" + # Run a fresh health evaluation, then check installed state: + sudo systemctl start ci-fleet-health.service + sudo systemctl is-failed ci-fleet-health.service # expect: inactive/failed must NOT be failed sudo ci-fleet/scripts/install-worker-controller.sh \ --check --config-repo OWNER/PRIVATE-CONFIG-REPO \ --ref RESOLVED_CONFIG_COMMIT --controller YOUR-CONTROLLER-ID ``` - Every listing above must be empty and the check must report - `CHECK_OK`. The [Live pilot runbook](LIVE-PILOT.md) documents the - full isolated first-job proof including read-only job permissions. + The health service is a timer-driven oneshot — `systemctl status` + alone only shows its previous run, so start it explicitly after the + proof job. Every resource listing above must be empty and the check + must report `CHECK_OK`. The [Live pilot runbook](LIVE-PILOT.md) + documents the full isolated first-job proof including read-only job + permissions. 4. Verify the controller returned to zero idle runners in the GitHub runner group. From 214e2f3ee8d017f876775c6ca27af0165340487f Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 27 Jul 2026 00:10:20 -0500 Subject: [PATCH 09/13] docs: address seventh Codex review on quickstart - validate the configuration tree before the first commit - proof workflow must land on the default branch for workflow_dispatch - exclude the persistent ci-fleet_default controller network from the residue check Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index c02d7bb8..25d9a6b2 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -98,13 +98,14 @@ jobs through the Docker socket. repository, open a pull request, and **merge it**: ```bash - git init -q && git add -A && git commit -qm "Initialize fleet configuration" ./scripts/validate.sh --strict + git init -q && git add -A && git commit -qm "Initialize fleet configuration" ``` - The commit must contain the initialized `fleet.json`; validating the - working tree without committing it would push the untouched example - configuration. Managed controller managed controller + Validate **before** committing so a rejected file or accidental value + never enters branch history. The commit must contain the initialized + `fleet.json`; pushing the untouched example configuration would give + `RESOLVED_CONFIG_COMMIT` no controller to apply. Managed controller managed controller lifecycle is permitted only from a reviewed, merged private configuration commit. Resolve and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); do not install from an unmerged @@ -142,7 +143,9 @@ private configuration). Changes state: yes, but no host changes — onboarding never touches a fleet host. 1. Stage the proof workflow first: commit the dispatch-only job below - to the project repository as `.github/workflows/fleet-proof.yml`. + to the project repository's **default branch** as + `.github/workflows/fleet-proof.yml` (GitHub only offers + `workflow_dispatch` for workflows present on the default branch). Do this **before** authorization so the first eligible job is the controlled proof, never a push-triggered or previously queued job. @@ -201,9 +204,11 @@ verifies all of it is cleaned up. --filter label=io.randomdevelopment.ci-fleet.managed=true \ --filter label=io.randomdevelopment.ci-fleet.kind=runner # No run-owned project resources remain. Compliant jobs name Compose - # projects ci-----: - sudo docker ps -aq --filter "name=^ci-" - sudo docker network ls -q --filter "name=^ci-" + # projects ci-----; the + # controller's own persistent ci-fleet_default network is expected + # and excluded: + sudo docker ps -aq --filter "name=^ci-" | grep -vx "" || true + sudo docker network ls --filter "name=^ci-" --format '{{.Name}}' | grep -v '^ci-fleet_default$' sudo docker volume ls -q --filter "name=^ci-" # Run a fresh health evaluation, then check installed state: sudo systemctl start ci-fleet-health.service From 42fe3d16b20aa6ba83974352f092e391c226aaf2 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 27 Jul 2026 00:10:51 -0500 Subject: [PATCH 10/13] docs: exclude controller-owned ci-fleet_* names from all residue filters Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 25d9a6b2..64fa7727 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -205,11 +205,12 @@ verifies all of it is cleaned up. --filter label=io.randomdevelopment.ci-fleet.kind=runner # No run-owned project resources remain. Compliant jobs name Compose # projects ci-----; the - # controller's own persistent ci-fleet_default network is expected - # and excluded: - sudo docker ps -aq --filter "name=^ci-" | grep -vx "" || true - sudo docker network ls --filter "name=^ci-" --format '{{.Name}}' | grep -v '^ci-fleet_default$' - sudo docker volume ls -q --filter "name=^ci-" + # controller's own persistent ci-fleet_* resources are expected and + # excluded (runner containers are already covered by the label filter + # above): + sudo docker ps -a --filter "name=^ci-" --format '{{.Names}}' | grep -v '^ci-fleet-' + sudo docker network ls --filter "name=^ci-" --format '{{.Name}}' | grep -v '^ci-fleet_' + sudo docker volume ls -q --filter "name=^ci-" | grep -v '^ci-fleet_' # Run a fresh health evaluation, then check installed state: sudo systemctl start ci-fleet-health.service sudo systemctl is-failed ci-fleet-health.service # expect: inactive/failed must NOT be failed From fccfeb1a20aec3d00a4aefcee925495dbf79540b Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 27 Jul 2026 00:23:46 -0500 Subject: [PATCH 11/13] docs: address eighth Codex review on quickstart - runner group created without repository selection; authorization deferred to step 2 after the proof workflow is staged - controller excluded by exact names so fleet-named project containers cannot be hidden from the residue check Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 64fa7727..ff355f15 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -60,7 +60,11 @@ jobs through the Docker socket. installed on the organization with organization-level **Self-hosted runners: Read and write** permission; the concrete bootstrap settings are in [Live pilot runbook](LIVE-PILOT.md) - sections 2-3. Then, on the **fleet host**, place the host-local + sections 2-3, with one exception: create the runner group **without + selecting repositories** — repository authorization happens in Step 2 + only after the proof workflow is staged, so no previously queued or + push-triggered job can become the first job. Then, on the + **fleet host**, place the host-local identity files (root-owned, mode `0600`) from the checked-out engine's templates as in [Adding a host](ADDING-A-HOST.md) section 5. 4. On the **management workstation**, create your private configuration @@ -208,9 +212,12 @@ verifies all of it is cleaned up. # controller's own persistent ci-fleet_* resources are expected and # excluded (runner containers are already covered by the label filter # above): - sudo docker ps -a --filter "name=^ci-" --format '{{.Names}}' | grep -v '^ci-fleet-' - sudo docker network ls --filter "name=^ci-" --format '{{.Name}}' | grep -v '^ci-fleet_' - sudo docker volume ls -q --filter "name=^ci-" | grep -v '^ci-fleet_' + sudo docker ps -a --filter "name=^ci-" --format '{{.Names}}' | grep -vx 'ci-fleet-controller-1' + sudo docker network ls --filter "name=^ci-" --format '{{.Name}}' | grep -vx 'ci-fleet_default' + sudo docker volume ls -q --filter "name=^ci-" + # (No compliant job creates controller-prefixed volumes; the exact-name + # exclusions above avoid hiding project containers from a repository + # named 'fleet' or 'fleet-*', whose Compose names also start ci-fleet-.) # Run a fresh health evaluation, then check installed state: sudo systemctl start ci-fleet-health.service sudo systemctl is-failed ci-fleet-health.service # expect: inactive/failed must NOT be failed From ce8f33e1d56341895b56a6c89d7b9626661c4ec7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 27 Jul 2026 00:32:10 -0500 Subject: [PATCH 12/13] docs: seed template on main so the initialization PR has a base Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index ff355f15..e1c497fb 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -103,13 +103,18 @@ jobs through the Docker socket. ```bash ./scripts/validate.sh --strict - git init -q && git add -A && git commit -qm "Initialize fleet configuration" + git init -q -b main + git add -A && git commit -qm "Import ci-fleet configuration template" + git checkout -qb initialize-fleet + git add -A && git commit -qm "Initialize fleet configuration" --allow-empty ``` Validate **before** committing so a rejected file or accidental value - never enters branch history. The commit must contain the initialized - `fleet.json`; pushing the untouched example configuration would give - `RESOLVED_CONFIG_COMMIT` no controller to apply. Managed controller managed controller + never enters branch history. Commit the vendored template to `main` + first, then commit the initialized `fleet.json` on a branch, so the + required pull request has a base to diff against. Pushing the only + commit directly would establish the default branch with no review. + Managed controller managed controller lifecycle is permitted only from a reviewed, merged private configuration commit. Resolve and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); do not install from an unmerged From 5fe251f1a1651c3fe644f8aa2fdc7752131f4917 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 27 Jul 2026 00:33:02 -0500 Subject: [PATCH 13/13] docs: correct seed order: template commit, branch, init, validate, commit Signed-off-by: Hermes Agent --- docs/QUICKSTART.md | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index e1c497fb..27f571a9 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -83,6 +83,10 @@ jobs through the Docker socket. git -C ci-fleet checkout PINNED_ENGINE_COMMIT cp -r ci-fleet/templates/config-repository my-fleet-config cd my-fleet-config + # Seed the vendored template on main so the initialization PR has a base: + git init -q -b main + git add -A && git commit -qm "Import ci-fleet configuration template" + git checkout -qb initialize-fleet ./scripts/init.sh \ --organization YOUR-ORG \ --project YOUR-APP-SLUG \ @@ -93,28 +97,17 @@ jobs through the Docker socket. --capacity-budget 1 \ --max-runners 1 \ --engine-ref PINNED_ENGINE_COMMIT + # Validate before committing so a rejected file or accidental value + # never enters branch history, then commit the initialized result: + ./scripts/validate.sh --strict + git add -A && git commit -qm "Initialize fleet configuration" ``` `--project` is a logical lowercase slug; pass the real GitHub repository name separately with `--repository` so - `allowed_repositories` names a repository that exists. Commit the - initialized result, validate, push to a new **private** GitHub - repository, open a pull request, and **merge it**: - - ```bash - ./scripts/validate.sh --strict - git init -q -b main - git add -A && git commit -qm "Import ci-fleet configuration template" - git checkout -qb initialize-fleet - git add -A && git commit -qm "Initialize fleet configuration" --allow-empty - ``` - - Validate **before** committing so a rejected file or accidental value - never enters branch history. Commit the vendored template to `main` - first, then commit the initialized `fleet.json` on a branch, so the - required pull request has a base to diff against. Pushing the only - commit directly would establish the default branch with no review. - Managed controller managed controller + `allowed_repositories` names a repository that exists. Push both + branches to a new **private** GitHub repository, open a pull request + from `initialize-fleet` to `main`, and **merge it**. Managed controller managed controller lifecycle is permitted only from a reviewed, merged private configuration commit. Resolve and record that merge commit SHA (`RESOLVED_CONFIG_COMMIT` below); do not install from an unmerged