feat: runner install resolves the latest release when --version is omitted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-hdb 2026-07-11 18:44:43 +00:00
parent e395d6754a
commit 63b2effe03
4 changed files with 42 additions and 11 deletions

View file

@ -61,14 +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>`
Runner box only, run after `rig bootstrap runner` (the same two-step rhythm Runner box only, run after `rig bootstrap runner` (the same two-step rhythm
as `bootstrap control-plane``coolify install`): as `bootstrap control-plane``coolify install`):
```sh ```sh
rig bootstrap runner --hostname my-ci-box 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
@ -84,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)
@ -93,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.

View file

@ -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"; }

View file

@ -286,6 +286,15 @@ Everything else about bootstrap is unchanged; `runner install`'s contract is
untouched. Tests: +2 (`runner refuses tag:server`, `runner role parses / untouched. Tests: +2 (`runner refuses tag:server`, `runner role parses /
refuses non-root`) → 27 non-root. 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**:

View file

@ -57,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