forked from heavy-duty/rig
Merge pull request #4 from claude-hdb/feat/runner-bootstrap-role
feat: runner follow-ups — bootstrap role + latest-version resolution
This commit is contained in:
commit
bcaade0e93
6 changed files with 97 additions and 27 deletions
43
README.md
43
README.md
|
|
@ -20,7 +20,7 @@ PATH (`/usr/local/bin` when root). Re-run any time to upgrade.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
### `rig bootstrap <control-plane|workload>`
|
### `rig bootstrap <control-plane|workload|runner>`
|
||||||
|
|
||||||
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
|
Run as root on the fresh box (over SSH). Convergent — safe to re-run; a
|
||||||
second run changes nothing.
|
second run changes nothing.
|
||||||
|
|
@ -28,10 +28,13 @@ second run changes nothing.
|
||||||
```sh
|
```sh
|
||||||
rig bootstrap control-plane --hostname my-coolify-box
|
rig bootstrap control-plane --hostname my-coolify-box
|
||||||
rig bootstrap workload --hostname my-prod-box
|
rig bootstrap workload --hostname my-prod-box
|
||||||
|
rig bootstrap runner --hostname my-ci-box
|
||||||
```
|
```
|
||||||
|
|
||||||
- `--hostname <name>` — tailnet hostname (default: the role name)
|
- `--hostname <name>` — tailnet hostname (default: the role name)
|
||||||
- `--ts-tag <tag>` — tailnet tag to advertise (default: `tag:server`)
|
- `--ts-tag <tag>` — tailnet tag to advertise (default: `tag:server`;
|
||||||
|
the `runner` role defaults to `tag:ci` instead, and **refuses**
|
||||||
|
`tag:server` outright — see below)
|
||||||
|
|
||||||
What it does: installs `curl ca-certificates unattended-upgrades` (and
|
What it does: installs `curl ca-certificates unattended-upgrades` (and
|
||||||
enables periodic unattended upgrades); writes an sshd hardening drop-in
|
enables periodic unattended upgrades); writes an sshd hardening drop-in
|
||||||
|
|
@ -42,9 +45,14 @@ tailscale and joins your tailnet.
|
||||||
the interactive prompt. Use a **single-use, tagged, short-expiry** key. It
|
the interactive prompt. Use a **single-use, tagged, short-expiry** key. It
|
||||||
lives in process memory only — rig never writes a credential to disk.
|
lives in process memory only — rig never writes a credential to disk.
|
||||||
|
|
||||||
The two roles are identical today except the default hostname; they exist
|
`control-plane` and `workload` are identical today except the default
|
||||||
because control-plane and workload boxes diverge over time, and because the
|
hostname; they exist because the boxes diverge over time, and because each
|
||||||
next command applies to exactly one of them.
|
follow-up command applies to exactly one role. `runner` is the box a CI
|
||||||
|
agent will live on, and it differs behaviorally: it defaults `--ts-tag` to
|
||||||
|
`tag:ci` and **refuses `tag:server`** — a runner executes repo-controlled
|
||||||
|
code, and advertising your server tag would extend every grant your servers
|
||||||
|
hold (SSH between them, say) to that code. The refusal turns the worst
|
||||||
|
misconfiguration from a documentation warning into a hard error.
|
||||||
|
|
||||||
### `rig coolify install --version <pin>`
|
### `rig coolify install --version <pin>`
|
||||||
|
|
||||||
|
|
@ -53,13 +61,14 @@ Control-plane box only. Installs Coolify at exactly the pinned version with
|
||||||
the platform must never move underneath it on its own. Upgrading is an
|
the platform must never move underneath it on its own. Upgrading is an
|
||||||
explicit re-run with a new pin. The pin is required; there is no default.
|
explicit re-run with a new pin. The pin is required; there is no default.
|
||||||
|
|
||||||
### `rig runner install --repo <owner/repo> --version <pin>`
|
### `rig runner install --repo <owner/repo>`
|
||||||
|
|
||||||
Workload box only, run after `rig bootstrap workload`:
|
Runner box only, run after `rig bootstrap runner` (the same two-step rhythm
|
||||||
|
as `bootstrap control-plane` → `coolify install`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rig bootstrap workload --hostname my-ci-box --ts-tag tag:ci
|
rig bootstrap runner --hostname my-ci-box
|
||||||
rig runner install --repo acme/widgets --version 2.335.1
|
rig runner install --repo acme/widgets
|
||||||
```
|
```
|
||||||
|
|
||||||
Installs GitHub's official `actions/runner` as a systemd service under an
|
Installs GitHub's official `actions/runner` as a systemd service under an
|
||||||
|
|
@ -75,6 +84,10 @@ membership is root-equivalent, which is a gratuitous path to root on a box
|
||||||
whose whole point is a narrow blast radius. Add Docker only once a job
|
whose whole point is a narrow blast radius. Add Docker only once a job
|
||||||
genuinely needs it, and rethink the isolation model then.
|
genuinely needs it, and rethink the isolation model then.
|
||||||
|
|
||||||
|
- `--version <pin>` — actions/runner release to install (default: the
|
||||||
|
latest release, resolved at install time; e.g. `--version 2.335.1` —
|
||||||
|
the latest as of this writing). Pin it when you need a deterministic,
|
||||||
|
auditable install.
|
||||||
- `--name <name>` — runner name (default: this host's hostname)
|
- `--name <name>` — runner name (default: this host's hostname)
|
||||||
- `--labels <csv>` — runner labels, replacing the `ci-runner` default — keep
|
- `--labels <csv>` — runner labels, replacing the `ci-runner` default — keep
|
||||||
any label your workflows' `runs-on` needs (GitHub adds `self-hosted` itself)
|
any label your workflows' `runs-on` needs (GitHub adds `self-hosted` itself)
|
||||||
|
|
@ -84,11 +97,13 @@ genuinely needs it, and rethink the isolation model then.
|
||||||
it at the interactive prompt. It's short-lived, consumed at registration, and
|
it at the interactive prompt. It's short-lived, consumed at registration, and
|
||||||
never written to disk by rig.
|
never written to disk by rig.
|
||||||
|
|
||||||
The version pin is required, same as `coolify install` — but unlike Coolify,
|
Why latest-by-default here when `coolify install` demands a pin: the two
|
||||||
the installed runner **self-updates**: GitHub refuses jobs from stale
|
tools age differently. Coolify never self-updates (`AUTOUPDATE=false`), so
|
||||||
runners, so freezing the version would just make it silently stop taking
|
its version is a contract your deploy tooling is verified against — stating
|
||||||
work. The pin states what you install today; GitHub owns the treadmill after
|
it is the point. The runner **self-updates regardless**: GitHub refuses jobs
|
||||||
that.
|
from stale runners, so freezing it would just make it silently stop taking
|
||||||
|
work. The install-time version is a starting point either way; `--version`
|
||||||
|
exists for when you want that starting point deterministic and auditable.
|
||||||
|
|
||||||
Convergent — safe to re-run; an already-registered runner is left alone.
|
Convergent — safe to re-run; an already-registered runner is left alone.
|
||||||
|
|
||||||
|
|
|
||||||
5
bin/rig
5
bin/rig
|
|
@ -8,10 +8,11 @@ usage() {
|
||||||
usage: rig <command> [args]
|
usage: rig <command> [args]
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
bootstrap <control-plane|workload> [--hostname <name>] [--ts-tag <tag>]
|
bootstrap <control-plane|workload|runner> [--hostname <name>] [--ts-tag <tag>]
|
||||||
OS plumbing on a pristine Debian box: hardening, unattended-upgrades,
|
OS plumbing on a pristine Debian box: hardening, unattended-upgrades,
|
||||||
tailscale join. Prompts for a single-use tailnet pre-auth key
|
tailscale join. Prompts for a single-use tailnet pre-auth key
|
||||||
(TS_AUTHKEY env overrides the prompt). Run as root.
|
(TS_AUTHKEY env overrides the prompt). Run as root. Role runner
|
||||||
|
defaults to tag:ci and refuses tag:server.
|
||||||
coolify install --version <pin>
|
coolify install --version <pin>
|
||||||
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
Pinned Coolify install (AUTOUPDATE=false). Control-plane box only.
|
||||||
runner install --repo <owner/repo> --version <pin> [options]
|
runner install --repo <owner/repo> --version <pin> [options]
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ die() { printf 'rig-bootstrap: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: rig bootstrap <control-plane|workload> [--hostname <name>] [--ts-tag <tag>]
|
usage: rig bootstrap <control-plane|workload|runner> [--hostname <name>] [--ts-tag <tag>]
|
||||||
|
|
||||||
--hostname tailnet hostname (default: the role name)
|
--hostname tailnet hostname (default: the role name)
|
||||||
--ts-tag tailnet tag to advertise (default: tag:server)
|
--ts-tag tailnet tag to advertise (default: tag:server;
|
||||||
|
role runner defaults to tag:ci and refuses tag:server —
|
||||||
|
a CI box executes repo-controlled code, and your server
|
||||||
|
tag's grants must never extend to it)
|
||||||
|
|
||||||
Provide the single-use tailscale pre-auth key via the TS_AUTHKEY env var, or
|
Provide the single-use tailscale pre-auth key via the TS_AUTHKEY env var, or
|
||||||
enter it at the interactive prompt. It is used once and never written to disk.
|
enter it at the interactive prompt. It is used once and never written to disk.
|
||||||
|
|
@ -22,14 +25,18 @@ EOF
|
||||||
# --- args (validated before the root check, so errors are testable) ---------
|
# --- args (validated before the root check, so errors are testable) ---------
|
||||||
ROLE="${1:-}"
|
ROLE="${1:-}"
|
||||||
case "$ROLE" in
|
case "$ROLE" in
|
||||||
control-plane|workload) shift ;;
|
control-plane|workload|runner) shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
"") usage >&2; die "role required (control-plane|workload)" 2 ;;
|
"") usage >&2; die "role required (control-plane|workload|runner)" 2 ;;
|
||||||
*) die "unknown role: $ROLE (want control-plane|workload)" 2 ;;
|
*) die "unknown role: $ROLE (want control-plane|workload|runner)" 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
TS_HOSTNAME="$ROLE"
|
TS_HOSTNAME="$ROLE"
|
||||||
TS_TAG="tag:server"
|
if [ "$ROLE" = "runner" ]; then
|
||||||
|
TS_TAG="tag:ci"
|
||||||
|
else
|
||||||
|
TS_TAG="tag:server"
|
||||||
|
fi
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--hostname)
|
--hostname)
|
||||||
|
|
@ -42,6 +49,12 @@ while [ $# -gt 0 ]; do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# A runner executes repo-controlled code; advertising the server tag would
|
||||||
|
# extend every grant your servers hold to that code. Refused, not warned.
|
||||||
|
if [ "$ROLE" = "runner" ] && [ "$TS_TAG" = "tag:server" ]; then
|
||||||
|
die "role runner must not advertise tag:server" 2
|
||||||
|
fi
|
||||||
|
|
||||||
# --- guards ------------------------------------------------------------------
|
# --- guards ------------------------------------------------------------------
|
||||||
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
if [ -r /etc/os-release ]; then
|
if [ -r /etc/os-release ]; then
|
||||||
|
|
@ -109,4 +122,6 @@ fi
|
||||||
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
|
log "done — role ${ROLE}, hostname ${TS_HOSTNAME}"
|
||||||
if [ "$ROLE" = "control-plane" ]; then
|
if [ "$ROLE" = "control-plane" ]; then
|
||||||
log "next: rig coolify install --version <pin>"
|
log "next: rig coolify install --version <pin>"
|
||||||
|
elif [ "$ROLE" = "runner" ]; then
|
||||||
|
log "next: rig runner install --repo <owner/repo> --version <pin>"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@ die() { printf 'rig-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
usage: rig runner install --repo <owner/repo> --version <pin> [options]
|
usage: rig runner install --repo <owner/repo> [options]
|
||||||
|
|
||||||
--repo <owner/repo> GitHub repository the runner registers to (required)
|
--repo <owner/repo> GitHub repository the runner registers to (required)
|
||||||
--version <pin> actions/runner release to install, e.g. 2.335.1
|
--version <pin> actions/runner release to install, e.g. 2.335.1
|
||||||
(required; no default — you state what you install)
|
(default: the latest release, resolved at install
|
||||||
|
time — safe here because the runner self-updates
|
||||||
|
regardless; pin it when you need a deterministic,
|
||||||
|
auditable install)
|
||||||
--name <name> runner name (default: this host's hostname)
|
--name <name> runner name (default: this host's hostname)
|
||||||
--labels <csv> runner labels; replaces the default (default: ci-runner)
|
--labels <csv> runner labels; replaces the default (default: ci-runner)
|
||||||
--user <name> unprivileged service user (default: github-runner;
|
--user <name> unprivileged service user (default: github-runner;
|
||||||
|
|
@ -66,7 +69,6 @@ done
|
||||||
if ! printf '%s' "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
|
if ! printf '%s' "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
|
||||||
die "--repo must be owner/repo" 2
|
die "--repo must be owner/repo" 2
|
||||||
fi
|
fi
|
||||||
[ -n "$VERSION" ] || die "--version <pin> is required" 2
|
|
||||||
VERSION="${VERSION#v}"
|
VERSION="${VERSION#v}"
|
||||||
[ "$RUNNER_USER" != "root" ] || die "runner user must not be root" 2
|
[ "$RUNNER_USER" != "root" ] || die "runner user must not be root" 2
|
||||||
|
|
||||||
|
|
@ -123,6 +125,20 @@ else
|
||||||
aarch64) ARCH="arm64" ;;
|
aarch64) ARCH="arm64" ;;
|
||||||
*) die "unsupported arch: $(uname -m)" ;;
|
*) die "unsupported arch: $(uname -m)" ;;
|
||||||
esac
|
esac
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
# No pin given: resolve the latest release by following the redirect on
|
||||||
|
# the /releases/latest page — no API call, no rate limit, no JSON to
|
||||||
|
# parse on a dependency-free box.
|
||||||
|
LATEST_URL="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
|
||||||
|
https://github.com/actions/runner/releases/latest)" \
|
||||||
|
|| die "could not resolve the latest actions/runner release"
|
||||||
|
VERSION="${LATEST_URL##*/}"
|
||||||
|
VERSION="${VERSION#v}"
|
||||||
|
case "$VERSION" in
|
||||||
|
""|*[!0-9.]*) die "could not parse a version from ${LATEST_URL}" ;;
|
||||||
|
esac
|
||||||
|
log "resolved latest actions/runner: ${VERSION}"
|
||||||
|
fi
|
||||||
URL="https://github.com/actions/runner/releases/download/v${VERSION}/actions-runner-linux-${ARCH}-${VERSION}.tar.gz"
|
URL="https://github.com/actions/runner/releases/download/v${VERSION}/actions-runner-linux-${ARCH}-${VERSION}.tar.gz"
|
||||||
WORKDIR="$(mktemp -d)"
|
WORKDIR="$(mktemp -d)"
|
||||||
cleanup() { rm -rf "$WORKDIR"; }
|
cleanup() { rm -rf "$WORKDIR"; }
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,27 @@ git commit -m "docs: README section for runner install"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Addendum (2026-07-11, operator-requested, post final review)
|
||||||
|
|
||||||
|
A third bootstrap role, `runner`, joins `control-plane|workload` — requested
|
||||||
|
for CLI consistency (each follow-up command applies to exactly one role) and
|
||||||
|
because it closes a real footgun mechanically: the role defaults `--ts-tag`
|
||||||
|
to `tag:ci` and **refuses `tag:server`** (exit 2, validated before the root
|
||||||
|
check). Forgetting the tag flag previously joined the CI box with the default
|
||||||
|
server tag — the exact misconfiguration the runner posture exists to prevent.
|
||||||
|
Everything else about bootstrap is unchanged; `runner install`'s contract is
|
||||||
|
untouched. Tests: +2 (`runner refuses tag:server`, `runner role parses /
|
||||||
|
refuses non-root`) → 27 non-root.
|
||||||
|
|
||||||
|
Second amendment (same day): `runner install --version` becomes **optional**
|
||||||
|
— omitted, rig resolves the latest release at install time by following the
|
||||||
|
`releases/latest` redirect (no API, no rate limit, no JSON parsing; validated
|
||||||
|
against a digits-and-dots pattern before use). Safe here, and only here,
|
||||||
|
because the runner self-updates regardless of what you install; `coolify
|
||||||
|
install` keeps its mandatory pin — Coolify never self-updates, so its version
|
||||||
|
is a verified contract, not a starting point. The `version required` test is
|
||||||
|
replaced by a `--version needs a value` test → still 27 non-root.
|
||||||
|
|
||||||
## Integration (orchestrator, after final review — not an SDD task)
|
## Integration (orchestrator, after final review — not an SDD task)
|
||||||
|
|
||||||
1. Push the branch to the fork and open the PR **against upstream**:
|
1. Push the branch to the fork and open the PR **against upstream**:
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,12 @@ check "bootstrap: --help exits 0" 0 "usage:" "$ROOT/commands/bo
|
||||||
check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato
|
||||||
check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope
|
check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope
|
||||||
check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname
|
check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname
|
||||||
|
check "bootstrap: runner refuses tag:server" 2 "must not advertise tag:server" "$ROOT/commands/bootstrap.sh" runner --ts-tag tag:server
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload
|
check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload
|
||||||
|
check "bootstrap: runner role parses, refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" runner
|
||||||
else
|
else
|
||||||
echo "skip: bootstrap non-root refusal (running as root)"
|
echo "skip: bootstrap non-root refusals (running as root)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh"
|
check "coolify: version required, exit 2" 2 "--version" "$ROOT/commands/coolify-install.sh"
|
||||||
|
|
@ -55,7 +57,7 @@ fi
|
||||||
check "bare runner shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" runner
|
check "bare runner shows usage, exit 2" 2 "usage:" "$ROOT/bin/rig" runner
|
||||||
check "runner: --help exits 0" 0 "usage:" "$ROOT/commands/runner-install.sh" --help
|
check "runner: --help exits 0" 0 "usage:" "$ROOT/commands/runner-install.sh" --help
|
||||||
check "runner: repo required, exit 2" 2 "--repo" "$ROOT/commands/runner-install.sh" --version 2.335.1
|
check "runner: repo required, exit 2" 2 "--repo" "$ROOT/commands/runner-install.sh" --version 2.335.1
|
||||||
check "runner: version required, exit 2" 2 "--version" "$ROOT/commands/runner-install.sh" --repo acme/widgets
|
check "runner: version needs value" 2 "needs a value" "$ROOT/commands/runner-install.sh" --repo acme/widgets --version
|
||||||
check "runner: repo needs value" 2 "needs a value" "$ROOT/commands/runner-install.sh" --repo
|
check "runner: repo needs value" 2 "needs a value" "$ROOT/commands/runner-install.sh" --repo
|
||||||
check "runner: rejects bad repo slug" 2 "owner/repo" "$ROOT/commands/runner-install.sh" --repo not-a-slug --version 2.335.1
|
check "runner: rejects bad repo slug" 2 "owner/repo" "$ROOT/commands/runner-install.sh" --repo not-a-slug --version 2.335.1
|
||||||
check "runner: refuses --user root" 2 "must not be root" "$ROOT/commands/runner-install.sh" --repo acme/widgets --version 2.335.1 --user root
|
check "runner: refuses --user root" 2 "must not be root" "$ROOT/commands/runner-install.sh" --repo acme/widgets --version 2.335.1 --user root
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue