feat(runner): status, remove, and repoint — the runner lifecycle verbs #11

Merged
dan-claude-bot merged 2 commits from feat/runner-lifecycle into main 2026-07-13 13:33:11 +00:00
dan-claude-bot commented 2026-07-13 13:25:54 +00:00 (Migrated from github.com)

Why

rig runner install is convergent by skipping — it sees a registered runner and leaves it alone. rig could therefore create a runner and never move or destroy one. Re-pointing a box at a different repository meant hand-rolled config.sh / svc.sh incantations against an install layout only rig knew about (~github-runner/actions-runner), which is exactly the class of box-shaped, on-box bash that rig exists to absorb.

This came out of a real act: moving a CI box's runner from a fork to the upstream repo, where the runner is the deploy path to a tailnet-only control plane. That move should be one command, not a runbook.

What

  • rig runner status — repo, runner name, labels, install dir, systemd unit + state. Read-only: no token, no network call, exits 1 when nothing is installed.
  • rig runner remove [--local] — service down, then deregister. --local wipes the box without contacting GitHub (no token), leaving a stale offline entry to delete by hand.
  • rig runner repoint --repo <owner/repo> — deregister + re-register in one act, keeping the runner's name and reusing the binary already on the box. Convergent: repointing to the repo it's already on is a no-op that never asks for a token.

Three things the source forced

  1. The service must come down before deregistration, in both paths. UnconfigureAsync throws "Uninstall service first" on Linux while the service is configured — and config.sh remove --local skips that check entirely (Runner.cs:171 deletes local config and exits), which would strand a running service pointed at config that no longer exists. So svc.sh stop && svc.sh uninstall runs first regardless of path.
  2. repoint collects both tokens up front. They come from different repos and different endpoints (remove-token for the old, registration-token for the new). A token you turn out not to have must fail while the runner is still registered and working — not halfway through the move. If re-registration fails anyway, rig says so plainly and prints the exact runner install line that finishes the job.
  3. Labels are the sharp edge. GitHub holds them, the runner does not persist them locally, and they are what runs-on matches — so a silent change there is a workflow that quietly stops finding its runner. install now records what it registered with (.rig-labels, box-local metadata, never a credential) so repoint and status can read it back. A runner installed before that has nothing to read: repoint falls back to the ci-runner default and warns loudly before it touches anything, while ctrl-c is still useful.

Verification

  • shellcheck install.sh bin/rig commands/*.sh test/cli.sh — clean.
  • bash test/cli.sh59 passed, 0 failed (+22 new cases: help/usage, arg validation, bad repo slug, --user root refusal, non-root refusals incl. the token-free --local path).
  • The CLI tests cover argument and refusal paths only, as before. So the flow was also rehearsed end-to-end against a synthetic runner dir with stubbed config.sh/svc.sh, asserting: convergent no-op on same-repo; correct ordering (svc stop → uninstall → config remove → configure → svc install → start); the runner's name preserved across the move; no re-download; .rig-labels written; and status reading back the new repo. The JSON parse correctly picks gitHubUrl over the adjacent serverUrl.
  • Not yet exercised against a live GitHub registration — that's the box rehearsal, and it's the next thing this gets used for.

🤖 Generated with Claude Code

## Why `rig runner install` is convergent **by skipping** — it sees a registered runner and leaves it alone. rig could therefore *create* a runner and never *move* or *destroy* one. Re-pointing a box at a different repository meant hand-rolled `config.sh` / `svc.sh` incantations against an install layout only rig knew about (`~github-runner/actions-runner`), which is exactly the class of box-shaped, on-box bash that rig exists to absorb. This came out of a real act: moving a CI box's runner from a fork to the upstream repo, where the runner is the deploy path to a tailnet-only control plane. That move should be one command, not a runbook. ## What - **`rig runner status`** — repo, runner name, labels, install dir, systemd unit + state. Read-only: no token, no network call, exits 1 when nothing is installed. - **`rig runner remove [--local]`** — service down, then deregister. `--local` wipes the box without contacting GitHub (no token), leaving a stale offline entry to delete by hand. - **`rig runner repoint --repo <owner/repo>`** — deregister + re-register in one act, keeping the runner's name and reusing the binary already on the box. Convergent: repointing to the repo it's already on is a no-op that never asks for a token. ## Three things the source forced 1. **The service must come down before deregistration, in both paths.** `UnconfigureAsync` throws `"Uninstall service first"` on Linux while the service is configured — and `config.sh remove --local` skips that check entirely (`Runner.cs:171` deletes local config and exits), which would strand a *running* service pointed at config that no longer exists. So `svc.sh stop && svc.sh uninstall` runs first regardless of path. 2. **`repoint` collects both tokens up front.** They come from different repos and different endpoints (`remove-token` for the old, `registration-token` for the new). A token you turn out not to have must fail while the runner is still registered and working — not halfway through the move. If re-registration fails anyway, rig says so plainly and prints the exact `runner install` line that finishes the job. 3. **Labels are the sharp edge.** GitHub holds them, the runner does not persist them locally, and they are what `runs-on` matches — so a silent change there is a workflow that quietly stops finding its runner. `install` now records what it registered with (`.rig-labels`, box-local metadata, never a credential) so `repoint` and `status` can read it back. A runner installed *before* that has nothing to read: `repoint` falls back to the `ci-runner` default and warns loudly **before** it touches anything, while ctrl-c is still useful. ## Verification - `shellcheck install.sh bin/rig commands/*.sh test/cli.sh` — clean. - `bash test/cli.sh` — **59 passed, 0 failed** (+22 new cases: help/usage, arg validation, bad repo slug, `--user root` refusal, non-root refusals incl. the token-free `--local` path). - The CLI tests cover argument and refusal paths only, as before. So the flow was also rehearsed end-to-end against a synthetic runner dir with stubbed `config.sh`/`svc.sh`, asserting: convergent no-op on same-repo; correct ordering (`svc stop → uninstall → config remove → configure → svc install → start`); the runner's name preserved across the move; no re-download; `.rig-labels` written; and `status` reading back the new repo. The JSON parse correctly picks `gitHubUrl` over the adjacent `serverUrl`. - Not yet exercised against a live GitHub registration — that's the box rehearsal, and it's the next thing this gets used for. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/rig#11
No description provided.