diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index f7356346..a587cc24 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -40,7 +40,9 @@ jobs through the Docker socket. [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. + named missing command if one is absent. The Docker host architecture + must be `amd64` or `arm64`; the runner image build rejects every other + architecture. 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 @@ -107,7 +109,7 @@ jobs through the Docker socket. repository name separately with `--repository` so `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 + from `initialize-fleet` to `main`, 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 @@ -164,10 +166,21 @@ onboarding never touches a fleet host. steps: - run: echo "fleet proof on ${RUNNER_NAME:-unknown}" ``` + This controlled-first-job path is valid only when the staged proof is + the only workflow that can target the shared label. If the repository is + active or has any other matching trigger, stop: use a dedicated empty + proof repository and runner group for Steps 1-3, then onboard the active + repository through [Adding a project](ADDING-A-PROJECT.md). 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`. + `./scripts/validate.sh --strict`. Immediately before authorization, + audit every queued Actions job across every repository already authorized + for the runner group and this repository. Cancel every queued job whose + complete `runs-on` expression could match the pool's shared label; do not + authorize until that matching queue is empty. If you cannot prove that + condition, use a dedicated empty proof repository and runner group rather + than exposing the pool to an unknown first job. 3. Authorize the repository in the GitHub runner group. Full contract: [Adding a project](ADDING-A-PROJECT.md). @@ -177,11 +190,11 @@ Full contract: [Adding a project](ADDING-A-PROJECT.md). 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. +the proof below verifies that it is cleaned up. The supplied proof job does +not invoke project Compose or create project containers, networks, or volumes. -1. Dispatch the staged proof workflow and record its run ID from the - run URL (`GITHUB_RUN_ID` scopes the residue checks below). Its explicit +1. Dispatch the staged proof workflow and record its run ID from the run + metadata. 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 @@ -206,15 +219,6 @@ verifies all of it is cleaned up. sudo docker ps -aq \ --filter label=io.randomdevelopment.ci-fleet.managed=true \ --filter label=io.randomdevelopment.ci-fleet.kind=runner - # No resources owned by the dispatched proof run remain. Compliant - # jobs name Compose projects ci-----, - # and Docker prefixes container names with '/', so anchor accordingly - # and scope to this run ID (host-wide queries would also match a - # concurrent unrelated run): - RUN_ID= - sudo docker ps -a --filter "name=^/ci-.*-${RUN_ID}-" --format '{{.Names}}' - sudo docker network ls --filter "name=^ci-.*-${RUN_ID}-" --format '{{.Name}}' - sudo docker volume ls -q --filter "name=^ci-.*-${RUN_ID}-" # 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 @@ -225,7 +229,7 @@ verifies all of it is cleaned up. 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 + proof job. The runner-container 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. diff --git a/scripts/test_quickstart.py b/scripts/test_quickstart.py new file mode 100644 index 00000000..55a82a24 --- /dev/null +++ b/scripts/test_quickstart.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +from pathlib import Path + +raw_quickstart = (Path(__file__).resolve().parents[1] / "docs" / "QUICKSTART.md").read_text() +quickstart = " ".join(raw_quickstart.split()) + +required = ( + "host architecture must be `amd64` or `arm64`", + "Cancel every queued job", + "across every repository already authorized for the runner group", + "the only workflow that can target the shared label", +) +for text in required: + assert text in quickstart, f"quickstart safety contract missing: {text}" + +assert quickstart.index("Cancel every queued job") < quickstart.index("3. Authorize the repository") +assert "PROJECT_PREFIX=" not in raw_quickstart +assert "managed controller managed controller" not in quickstart +print("quickstart_contract=PASS") diff --git a/scripts/validate.sh b/scripts/validate.sh index 229577d3..8a1f94e9 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -12,10 +12,12 @@ python3 -m py_compile \ scripts/health.py \ scripts/scan_committed_secrets.py \ scripts/test_desired_state.py \ - scripts/test_health.py + scripts/test_health.py \ + scripts/test_quickstart.py python3 .github/actions/plan/test_plan.py python3 scripts/test_desired_state.py python3 scripts/test_health.py +python3 scripts/test_quickstart.py python3 .github/actions/plan/plan.py --plan examples/project/scripts/ci/plan.json --group fast >/dev/null python3 .github/actions/plan/plan.py --plan examples/project/scripts/ci/plan.json --group full >/dev/null scripts/test-capacity-preflight.sh