rig/drill
cluade-reviewer-andresmgsl 9afcdf858f
Some checks failed
ci / check (pull_request) Failing after 7s
ci / install (pull_request) Successful in 4s
ci / db-integration (pull_request) Successful in 4s
labels / labels (pull_request) Failing after 7s
fix(drill): the leg row is the whole lifecycle, not just the job
codex, grok and kimi all landed on the same hole at e42ba31, and they are
right. The record row keyed only on fj_took, so install/status/remove failures
called `no` without gating it. A dispatched job only needs SOMETHING answering
runs-on: drill — and this leg removes locally, telling the operator to delete
the stale runner by hand, so a leftover drill-labeled runner from the previous
drill is the designed-for aftermath, not a contrived case. It answers the
fixture exactly, and the row then read "PASS — registered, took a job,
removed" for a drill whose install never happened.

The exit code was never wrong: every one of those failures also called `no`.
What was wrong is the row, and the row is what outlives the run in
drills/<v>.md. A row claiming a lifecycle that did not happen is exactly what
the gate exists to refuse — kimi's framing, and it is the right one.

forgejo_leg_row folds all five assertions, and the leg no longer dispatches
after install or status has failed: a foreign runner's success could only ever
be evidence about someone else's runner. The regression composes the real
functions over a payload carrying a newer successful run, the way the leg
composes them.

The GitHub leg has the identical shape and is byte-frozen by #129's contract;
left alone deliberately, and worth a sibling issue for triage.

Refs #129

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 20:04:16 +00:00
..
drill.sh fix(drill): the leg row is the whole lifecycle, not just the job 2026-07-31 20:04:16 +00:00
README.md feat(drill): a forgejo-runner lifecycle leg beside the GitHub one 2026-07-30 23:27:36 +00:00

The drill — running it

drill/drill.sh is the instrument; drills/ is the record it feeds (see drills/README.md for what a record means and how the three repos' drills relate). rig's drill asserts convergence: a machine reaches its role, idempotently. This file is the procedure — written down so a run is repeatable, not reconstructed from memory each release (#105, and #107's debt).

What you need

  • A throwaway Debian 13 machine you can format, reached as root. The drill hardens its sshd, renames it, joins it to a tailnet, and installs box/Incus, Coolify and Actions runners on it. It is not coming back. The machine is its own reset — there is no teardown script and no need for one.
  • The pinned candidate refs, both of them. --rig-ref and --box-ref are required; the harness refuses to run without them and refuses to continue if what installed disagrees with what was asked (INSTALLED_FROM, both trees). Since heavy-duty/rig#103 landed, both installers have sane defaults when unpinned — box installs the BOX_RELEASE pin (currently 0.9.0), rig's install.sh resolves the latest release — and a sane default is exactly why the drill will not let a ref go unstated: an unpinned run silently drills a shipping pair that is not the candidate, and the record it leaves looks clean.
  • A single-use, tagged tailscale pre-auth key in TS_AUTHKEY (tag:local for the default staging-server role — bootstrap refuses tag:server outside the control-plane shapes).
  • A users file (--users) naming at least one operator — leg 1 asserts the accounts and keys actually converged.
  • For leg 3 (runner lifecycle): a fork to register against (--runner-repo you/rig) carrying a workflow_dispatch workflow — default name drill.yml — whose job has runs-on: [self-hosted, drill] and does something trivial (echo drilled). Tokens come from an authenticated gh, or from RUNNER_TOKEN / RUNNER_REMOVE_TOKEN. Without a fork the leg skips, loudly, into the record.
  • For leg 3's Forgejo half (#129): --forgejo-instance <url> and --forgejo-runner-repo <owner>/<repo>, where that repo carries the same workflow_dispatch workflow — but with runs-on: drill, because a Forgejo runner matches the bare label it registered with. Tokens come from FORGEJO_RUNNER_TOKEN (a registration token) or FORGEJO_API_TOKEN, which mints one and is also what dispatches the job. --forgejo-ref names the branch to dispatch (default main): Forgejo's dispatch endpoint requires a ref in the body, where GitHub's defaults to the repo's default branch. Without an instance and a repo this half skips, loudly and separately. Note there is no removal token — Forgejo has no deregistration endpoint, so the leg removes locally and the record tells you to delete the stale runner row by hand.
  • For leg 4 (coolify): a version pin, --coolify-version 4.1.2. No pin, no leg — rig's own coolify install refuses to default a version and so does its drill. The skip is recorded.
  • A run ID (--run-id) when this drill shares a substrate with box's or cast's — the shared ID is what lets the per-repo records be joined afterwards. Defaults to drill-<date>.

Running it

From a checkout of this repo on the throwaway machine (the record lands in the checkout's drills/):

TS_AUTHKEY=tskey-... bash drill/drill.sh \
  --rig-ref release/0.4.0 --box-ref 0.9.0 \
  --users ./drill-users --run-id drill-2026-07-24-a \
  --coolify-version 4.1.2 --runner-repo you/rig --yes

--box-ref is a tag on purpose: since #103 the box that ships is the BOX_RELEASE tag, so a release/… branch is the wrong thing to pin for box — while a release branch stays exactly right for rig's own candidate.

It runs unattended from there. Legs execute as 1, 4, 2, 3 — Coolify's installer is what puts Docker on the box and the db leg needs a daemon — and the record lists them as they ran. A failing check never aborts the run (set -u, no -e: a failing check is data), and the summary counts passes, failures and skips separately.

What it asserts

  1. Convergence, and idempotence. rig bootstrap <role> --users … reaches the declared role, asserted on effective state — the marker, sshd -T, the granted tailnet tag, the operators' accounts and keys. Then bootstrap runs again, and the state captured before and after the re-run must diff empty. The diff is mechanical; "watched it not obviously break" is exactly what this leg exists to replace. Riding along, the --host yes assertions: the pinned box installed (INSTALLED_FROM matches --box-ref, fatal if not), box doctor passes. It stops there and says so in the output — the isolation boundary is box's drill's assertion, never rig's.
  2. dbtest/db-integration.sh from the installed tree: a real dump/restore round-trip. Its clean-skip contract (no Docker → loud skip, exit 0) survives into the record as a SKIP, never a pass.
  3. Runner lifecycle — register against the fork, dispatch the drill workflow and watch the runner take it, deregister, and assert the box's registration is actually gone. Runs once per forge: rig runner against GitHub, then rig forgejo-runner against a Forgejo instance (#129). Both families ship, so a release that evidences only one evidences half of what it ships; each half skips separately, so a record can honestly show one forge drilled and the other not.
  4. Coolify — installed at the pin, AUTOUPDATE=false landed in the effective .env, container running.

The record

The run always ends by writing drills/<version>.md (the version is the installed tree's own VERSION) — on failures too: a failed drill is a valid record; the gate wants evidence, not success. Skipped legs are named as not-run so the record can never read as a clean sweep. Commit the file on the release branch; the drill-recorded guard reads that file and nothing else.

The instrument's own honesty — the refusals, the skip accounting, the capture-and-diff, the emitter — is test/drill.sh's job, and CI runs it on every PR. The live four-leg run is a release's job, once per cycle.