feat: runner install — GitHub Actions runner as an unprivileged systemd service #3

Merged
dan-claude-bot merged 4 commits from feat/runner-install into main 2026-07-11 18:19:00 +00:00
dan-claude-bot commented 2026-07-11 17:51:37 +00:00 (Migrated from github.com)

What

Two additions, plan-first (docs/plans/2026-07-11-runner-install.md is the first commit; an addendum covers the second feature):

rig runner install --repo <owner/repo> --version <pin> [--name|--labels|--user] — turns a bootstrapped box into a self-hosted GitHub Actions runner: the official actions/runner agent installed at an explicit version pin as a systemd service under an unprivileged user (default github-runner, created if absent, never root, no supplementary groups).

rig bootstrap runner — a third bootstrap role alongside control-plane|workload. It defaults --ts-tag to tag:ci and refuses tag:server (exit 2): a runner executes repo-controlled code, and advertising the server tag would extend every grant your servers hold to that code. The pairing rhymes with the existing shape: bootstrap control-planecoolify install, bootstrap runnerrunner install.

Why this shape

  • Agent, not server. The runner long-polls GitHub outbound and receives jobs down that established connection — the box needs zero inbound ports, so it works behind a deny-all provider firewall and can trigger deploys on hosts only it can reach (e.g. a tailnet-only control plane).
  • No Docker, deliberately. docker group membership is root-equivalent (the socket is a root API). On a box whose design goal is a narrow blast radius for repo-controlled code, that is a gratuitous path to root. Docker arrives only when a job genuinely needs it, with the isolation model revisited then.
  • Credential hygiene. The short-lived registration token comes from the RUNNER_TOKEN env var or a silent interactive prompt, is consumed by config.sh in memory, and is never written to disk or logged by rig.
  • Version pin required, then self-update. Like coolify install you state what you install; unlike Coolify the runner then self-updates — GitHub refuses stale runners, so freezing it means it silently stops taking jobs.
  • Convergent. Safe to re-run: an already-registered runner is left alone, and a second run never demands a fresh registration token the operator no longer holds.

Testing

  • bash test/cli.sh: 27 passed, 0 failed (11 new cases: usage/validation/refusal paths, including the non-root refusals, --user root rejection, and the runner-role tag:server refusal).
  • shellcheck install.sh bin/rig commands/*.sh test/cli.sh: clean at default severity.
  • The CLI tests cover argument/refusal paths only; the end-to-end functional test is the box rehearsal per the README's Testing section (pristine Debian → bootstrap runnerrunner install with a real registration token → runner shows Idle → convergent second run).

🤖 Generated with Claude Code

## What Two additions, plan-first (`docs/plans/2026-07-11-runner-install.md` is the first commit; an addendum covers the second feature): **`rig runner install --repo <owner/repo> --version <pin> [--name|--labels|--user]`** — turns a bootstrapped box into a self-hosted GitHub Actions runner: the official `actions/runner` agent installed at an explicit version pin as a **systemd service under an unprivileged user** (default `github-runner`, created if absent, never root, no supplementary groups). **`rig bootstrap runner`** — a third bootstrap role alongside `control-plane|workload`. It defaults `--ts-tag` to `tag:ci` and **refuses `tag:server`** (exit 2): a runner executes repo-controlled code, and advertising the server tag would extend every grant your servers hold to that code. The pairing rhymes with the existing shape: `bootstrap control-plane` → `coolify install`, `bootstrap runner` → `runner install`. ## Why this shape - **Agent, not server.** The runner long-polls GitHub outbound and receives jobs down that established connection — the box needs **zero inbound ports**, so it works behind a deny-all provider firewall and can trigger deploys on hosts only it can reach (e.g. a tailnet-only control plane). - **No Docker, deliberately.** `docker` group membership is root-equivalent (the socket is a root API). On a box whose design goal is a narrow blast radius for repo-controlled code, that is a gratuitous path to root. Docker arrives only when a job genuinely needs it, with the isolation model revisited then. - **Credential hygiene.** The short-lived registration token comes from the `RUNNER_TOKEN` env var or a silent interactive prompt, is consumed by `config.sh` in memory, and is never written to disk or logged by rig. - **Version pin required, then self-update.** Like `coolify install` you state what you install; unlike Coolify the runner then self-updates — GitHub refuses stale runners, so freezing it means it silently stops taking jobs. - **Convergent.** Safe to re-run: an already-registered runner is left alone, and a second run never demands a fresh registration token the operator no longer holds. ## Testing - `bash test/cli.sh`: 27 passed, 0 failed (11 new cases: usage/validation/refusal paths, including the non-root refusals, `--user root` rejection, and the `runner`-role `tag:server` refusal). - `shellcheck install.sh bin/rig commands/*.sh test/cli.sh`: clean at default severity. - The CLI tests cover argument/refusal paths only; the end-to-end functional test is the box rehearsal per the README's Testing section (pristine Debian → `bootstrap runner` → `runner install` with a real registration token → runner shows Idle → convergent second run). 🤖 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#3
No description provided.