feat(runner): status, remove, and repoint — the runner lifecycle verbs #11
7 changed files with 501 additions and 7 deletions
78
README.md
78
README.md
|
|
@ -170,6 +170,84 @@ 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.
|
||||||
|
|
||||||
|
### `rig runner status`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rig runner status
|
||||||
|
```
|
||||||
|
|
||||||
|
What this box's runner is registered to — repo, runner name, labels,
|
||||||
|
install dir, systemd unit and its state. Reads the runner's own on-disk
|
||||||
|
config; no token, no network call. Exits 1 when no runner is installed.
|
||||||
|
|
||||||
|
The answer to "wait, which repo is this box wired to?" should not require
|
||||||
|
knowing that the config lives in a dotfile under an unprivileged user's home.
|
||||||
|
|
||||||
|
### `rig runner remove`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rig runner remove
|
||||||
|
rig runner remove --local # no token; leaves a stale entry to delete by hand
|
||||||
|
```
|
||||||
|
|
||||||
|
Stops and uninstalls the systemd service, then deregisters the runner from
|
||||||
|
GitHub. The binary and its user stay put, so a later `rig runner install`
|
||||||
|
re-registers without downloading anything.
|
||||||
|
|
||||||
|
**The token here is a *removal* token, not a registration token** — a
|
||||||
|
different endpoint, and mixing them up is the easy mistake:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gh api -X POST repos/<owner/repo>/actions/runners/remove-token
|
||||||
|
```
|
||||||
|
|
||||||
|
Supply it via `RUNNER_REMOVE_TOKEN` or the prompt; it never touches disk.
|
||||||
|
|
||||||
|
`--local` is the escape hatch for when the registration is already gone
|
||||||
|
server-side (or you can't mint a token): the box is cleaned, but a stale
|
||||||
|
offline runner stays listed in the repo, for you to delete from
|
||||||
|
Settings → Actions → Runners.
|
||||||
|
|
||||||
|
The service always comes down *first*, in both paths. GitHub's own removal
|
||||||
|
refuses to run while the service is installed ("Uninstall service first"),
|
||||||
|
and `--local` skips that check entirely — which would otherwise leave a
|
||||||
|
running service pointed at config that no longer exists.
|
||||||
|
|
||||||
|
Convergent — a box with no runner installed exits 0.
|
||||||
|
|
||||||
|
### `rig runner repoint --repo <owner/repo>`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rig runner repoint --repo acme/widgets
|
||||||
|
```
|
||||||
|
|
||||||
|
Moves an installed runner from one repository to another: deregister,
|
||||||
|
re-register, reusing the binary already on the box. It keeps the runner's
|
||||||
|
existing name unless you pass `--name`.
|
||||||
|
|
||||||
|
This is the verb that was missing. `runner install` is convergent *by
|
||||||
|
skipping* — it sees a registered runner and leaves it alone — so it can
|
||||||
|
create a runner but never move one, and re-pointing a box meant hand-rolled
|
||||||
|
`config.sh`/`svc.sh` incantations against an install path only rig knew.
|
||||||
|
|
||||||
|
Two short-lived tokens, each minted from **its own** repo — `RUNNER_REMOVE_TOKEN`
|
||||||
|
for the one it's leaving, `RUNNER_TOKEN` for the one it's joining. Both are
|
||||||
|
collected **before** anything is torn down: a token you turn out not to have
|
||||||
|
should 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.
|
||||||
|
|
||||||
|
> **Labels do not survive a move on their own.** GitHub holds them; the runner
|
||||||
|
> does not persist them locally. rig now records what it registered with, so
|
||||||
|
> `repoint` and `status` can read it back — but a runner installed before rig
|
||||||
|
> did that has nothing to read, and `repoint` falls back to the `ci-runner`
|
||||||
|
> default and warns loudly before it touches anything. Labels are what
|
||||||
|
> `runs-on` matches, so a silent change there is a workflow that simply stops
|
||||||
|
> finding its runner. Pass `--labels` if yours differ.
|
||||||
|
|
||||||
|
Convergent — repointing to the repo it is already on changes nothing, exits 0,
|
||||||
|
and never asks for a token.
|
||||||
|
|
||||||
## What rig deliberately does NOT do
|
## What rig deliberately does NOT do
|
||||||
|
|
||||||
- **Provider firewalls** — Docker publishes ports past host firewalls, so
|
- **Provider firewalls** — Docker publishes ports past host firewalls, so
|
||||||
|
|
|
||||||
42
bin/rig
42
bin/rig
|
|
@ -20,10 +20,22 @@ commands:
|
||||||
systemd timer. rig installs the machinery and templates an empty
|
systemd timer. rig installs the machinery and templates an empty
|
||||||
0600 bindings file; you fill in the age recipient and S3 details.
|
0600 bindings file; you fill in the age recipient and S3 details.
|
||||||
Control-plane box only. Run as root.
|
Control-plane box only. Run as root.
|
||||||
runner install --repo <owner/repo> --version <pin> [options]
|
runner install --repo <owner/repo> [options]
|
||||||
GitHub Actions runner as a systemd service under an unprivileged
|
GitHub Actions runner as a systemd service under an unprivileged
|
||||||
user — outbound-only, no Docker. Prompts for the short-lived
|
user — outbound-only, no Docker. Prompts for the short-lived
|
||||||
registration token (RUNNER_TOKEN env overrides). Run as root.
|
registration token (RUNNER_TOKEN env overrides). Run as root.
|
||||||
|
runner status [--user <name>]
|
||||||
|
What this box's runner is registered to: repo, name, labels, unit.
|
||||||
|
Reads the box only — no token, no network call. Run as root.
|
||||||
|
runner remove [--local] [--user <name>]
|
||||||
|
Take the service down and deregister the runner. Prompts for the
|
||||||
|
short-lived removal token (RUNNER_REMOVE_TOKEN env overrides).
|
||||||
|
Run as root.
|
||||||
|
runner repoint --repo <owner/repo> [options]
|
||||||
|
Move an installed runner to another repository — deregister, then
|
||||||
|
re-register, reusing the binary already on the box. Needs a removal
|
||||||
|
token for the old repo and a registration token for the new one.
|
||||||
|
Run as root.
|
||||||
|
|
||||||
install/upgrade:
|
install/upgrade:
|
||||||
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
curl -fsSL https://raw.githubusercontent.com/heavy-duty/rig/main/install.sh | bash
|
||||||
|
|
@ -62,12 +74,28 @@ case "$cmd" in
|
||||||
runner)
|
runner)
|
||||||
shift
|
shift
|
||||||
sub="${1:-}"
|
sub="${1:-}"
|
||||||
if [ "$sub" != "install" ]; then
|
case "$sub" in
|
||||||
usage >&2
|
install)
|
||||||
exit 2
|
shift
|
||||||
fi
|
exec "$ROOT/commands/runner-install.sh" "$@"
|
||||||
shift
|
;;
|
||||||
exec "$ROOT/commands/runner-install.sh" "$@"
|
status)
|
||||||
|
shift
|
||||||
|
exec "$ROOT/commands/runner-status.sh" "$@"
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
shift
|
||||||
|
exec "$ROOT/commands/runner-remove.sh" "$@"
|
||||||
|
;;
|
||||||
|
repoint)
|
||||||
|
shift
|
||||||
|
exec "$ROOT/commands/runner-repoint.sh" "$@"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
;;
|
;;
|
||||||
-h|--help|help)
|
-h|--help|help)
|
||||||
usage
|
usage
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,11 @@ else
|
||||||
(cd "$RUNNER_DIR" && runuser -u "$RUNNER_USER" -- env HOME="$USER_HOME" \
|
(cd "$RUNNER_DIR" && runuser -u "$RUNNER_USER" -- env HOME="$USER_HOME" \
|
||||||
./config.sh --url "https://github.com/${REPO}" --token "$RUNNER_TOKEN" \
|
./config.sh --url "https://github.com/${REPO}" --token "$RUNNER_TOKEN" \
|
||||||
--name "$RUNNER_NAME" --labels "$LABELS" --unattended --replace)
|
--name "$RUNNER_NAME" --labels "$LABELS" --unattended --replace)
|
||||||
|
# GitHub owns the labels and the runner does not persist them locally, so
|
||||||
|
# `runner status` and `runner repoint` would have nothing to read. Record
|
||||||
|
# what we registered with — box-local metadata, never a credential.
|
||||||
|
printf '%s\n' "$LABELS" > "$RUNNER_DIR/.rig-labels"
|
||||||
|
chown "$RUNNER_USER:$RUNNER_USER" "$RUNNER_DIR/.rig-labels"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- service -------------------------------------------------------------
|
# --- service -------------------------------------------------------------
|
||||||
|
|
|
||||||
108
commands/runner-remove.sh
Executable file
108
commands/runner-remove.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig runner remove — take the service down and deregister the runner.
|
||||||
|
# Convergent: a box with nothing installed exits 0.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { printf 'rig-runner: %s\n' "$*"; }
|
||||||
|
warn() { printf 'rig-runner: WARNING: %s\n' "$*" >&2; }
|
||||||
|
die() { printf 'rig-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: rig runner remove [options]
|
||||||
|
|
||||||
|
--local wipe this box's registration without contacting GitHub
|
||||||
|
(no token needed)
|
||||||
|
--user <name> unprivileged service user (default: github-runner)
|
||||||
|
|
||||||
|
Stops and uninstalls the systemd service, then deregisters the runner from
|
||||||
|
GitHub. The runner binary and its user stay on the box, so a later
|
||||||
|
`rig runner install` re-registers without downloading anything.
|
||||||
|
|
||||||
|
Provide the short-lived REMOVAL token — not a registration token, they are
|
||||||
|
different endpoints — via the RUNNER_REMOVE_TOKEN env var or the interactive
|
||||||
|
prompt:
|
||||||
|
gh api -X POST repos/<owner/repo>/actions/runners/remove-token
|
||||||
|
It is consumed at deregistration and never written to disk by rig.
|
||||||
|
|
||||||
|
--local is the escape hatch for when the registration is already gone
|
||||||
|
server-side, or you cannot mint a token: the box is cleaned, but a stale
|
||||||
|
offline runner is left listed in the repo — delete it by hand from
|
||||||
|
Settings > Actions > Runners.
|
||||||
|
|
||||||
|
Convergent: safe to re-run; a box with no runner installed exits 0.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- args (validated before the root check, so errors are testable) ---------
|
||||||
|
LOCAL=0
|
||||||
|
RUNNER_USER="github-runner"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--local) LOCAL=1; shift ;;
|
||||||
|
--user)
|
||||||
|
[ $# -ge 2 ] || die "--user needs a value" 2
|
||||||
|
RUNNER_USER="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "unknown flag: $1" 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- validation ------------------------------------------------------------
|
||||||
|
[ "$RUNNER_USER" != "root" ] || die "runner user must not be root" 2
|
||||||
|
|
||||||
|
# --- guards ----------------------------------------------------------------
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
|
|
||||||
|
# --- nothing to remove? -----------------------------------------------------
|
||||||
|
if ! id -u "$RUNNER_USER" >/dev/null 2>&1; then
|
||||||
|
log "no ${RUNNER_USER} user on this box; nothing to remove"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
|
||||||
|
RUNNER_DIR="$USER_HOME/actions-runner"
|
||||||
|
if [ ! -e "$RUNNER_DIR/.runner" ] && [ ! -e "$RUNNER_DIR/.service" ]; then
|
||||||
|
log "no runner registered in ${RUNNER_DIR}; nothing to remove"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- removal token — only when a server-side deregistration is pending -------
|
||||||
|
REMOVE_TOKEN=""
|
||||||
|
if [ -e "$RUNNER_DIR/.runner" ] && [ "$LOCAL" -eq 0 ]; then
|
||||||
|
REMOVE_TOKEN="${RUNNER_REMOVE_TOKEN:-}"
|
||||||
|
if [ -z "$REMOVE_TOKEN" ]; then
|
||||||
|
read -rsp "runner removal token (short-lived): " REMOVE_TOKEN
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
[ -n "$REMOVE_TOKEN" ] || die "empty removal token"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- service ---------------------------------------------------------------
|
||||||
|
# This must come first in BOTH paths. config.sh's removal throws "Uninstall
|
||||||
|
# service first" while the service is configured; and `remove --local` skips
|
||||||
|
# that check entirely, which would otherwise strand a running service pointed
|
||||||
|
# at config that no longer exists.
|
||||||
|
if [ -e "$RUNNER_DIR/.service" ]; then
|
||||||
|
log "stopping and uninstalling the service"
|
||||||
|
(cd "$RUNNER_DIR" && ./svc.sh stop)
|
||||||
|
(cd "$RUNNER_DIR" && ./svc.sh uninstall)
|
||||||
|
else
|
||||||
|
log "no service installed; skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- deregister -------------------------------------------------------------
|
||||||
|
if [ -e "$RUNNER_DIR/.runner" ]; then
|
||||||
|
if [ "$LOCAL" -eq 1 ]; then
|
||||||
|
log "wiping the local registration only (--local)"
|
||||||
|
(cd "$RUNNER_DIR" && runuser -u "$RUNNER_USER" -- env HOME="$USER_HOME" \
|
||||||
|
./config.sh remove --local)
|
||||||
|
warn "a stale offline runner is still listed in the repo — delete it from Settings > Actions > Runners"
|
||||||
|
else
|
||||||
|
log "deregistering from GitHub"
|
||||||
|
(cd "$RUNNER_DIR" && runuser -u "$RUNNER_USER" -- env HOME="$USER_HOME" \
|
||||||
|
./config.sh remove --token "$REMOVE_TOKEN")
|
||||||
|
fi
|
||||||
|
rm -f "$RUNNER_DIR/.rig-labels"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "runner removed; the binary stays at ${RUNNER_DIR} for a future rig runner install"
|
||||||
162
commands/runner-repoint.sh
Executable file
162
commands/runner-repoint.sh
Executable file
|
|
@ -0,0 +1,162 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig runner repoint — move an installed runner from one repository to another.
|
||||||
|
# Deregister, then re-register against the new repo, reusing the binary that is
|
||||||
|
# already on the box.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||||
|
|
||||||
|
log() { printf 'rig-runner: %s\n' "$*"; }
|
||||||
|
warn() { printf 'rig-runner: WARNING: %s\n' "$*" >&2; }
|
||||||
|
die() { printf 'rig-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: rig runner repoint --repo <owner/repo> [options]
|
||||||
|
|
||||||
|
--repo <owner/repo> the repository to move the runner TO (required)
|
||||||
|
--name <name> runner name (default: keep the name it has now)
|
||||||
|
--labels <csv> runner labels (default: the labels rig recorded at
|
||||||
|
install; else ci-runner)
|
||||||
|
--user <name> unprivileged service user (default: github-runner)
|
||||||
|
--local skip the server-side deregistration of the OLD repo
|
||||||
|
(no removal token needed) — leaves a stale offline
|
||||||
|
runner listed there, to delete by hand
|
||||||
|
|
||||||
|
Deregisters the runner from the repository it is on now and registers it
|
||||||
|
against --repo. The runner binary, its user, and its systemd service are
|
||||||
|
reused, so nothing is downloaded.
|
||||||
|
|
||||||
|
Two short-lived tokens, each minted from its OWN repository:
|
||||||
|
|
||||||
|
RUNNER_REMOVE_TOKEN removal token, from the CURRENT repo (not needed
|
||||||
|
with --local)
|
||||||
|
gh api -X POST repos/<current>/actions/runners/remove-token
|
||||||
|
RUNNER_TOKEN registration token, from the repo in --repo
|
||||||
|
gh api -X POST repos/<new>/actions/runners/registration-token
|
||||||
|
|
||||||
|
Either may be typed at the prompt instead. Both are collected BEFORE the
|
||||||
|
runner is touched — a token you turn out not to have should fail while the
|
||||||
|
runner is still registered, not halfway through the move. Neither is written
|
||||||
|
to disk by rig.
|
||||||
|
|
||||||
|
LABELS ARE NOT RECOVERABLE FROM THE BOX: GitHub holds them and the runner
|
||||||
|
does not persist them. rig records what it registered with, but a runner
|
||||||
|
installed before rig did that has nothing to read — repoint then falls back
|
||||||
|
to the ci-runner default and says so. Check your workflows' runs-on.
|
||||||
|
|
||||||
|
Convergent: repointing to the repo it is already on changes nothing, exits 0,
|
||||||
|
and never asks for a token.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- args (validated before the root check, so errors are testable) ---------
|
||||||
|
REPO=""
|
||||||
|
RUNNER_NAME=""
|
||||||
|
LABELS=""
|
||||||
|
RUNNER_USER="github-runner"
|
||||||
|
LOCAL=0
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--repo)
|
||||||
|
[ $# -ge 2 ] || die "--repo needs a value" 2
|
||||||
|
REPO="$2"; shift 2 ;;
|
||||||
|
--name)
|
||||||
|
[ $# -ge 2 ] || die "--name needs a value" 2
|
||||||
|
RUNNER_NAME="$2"; shift 2 ;;
|
||||||
|
--labels)
|
||||||
|
[ $# -ge 2 ] || die "--labels needs a value" 2
|
||||||
|
LABELS="$2"; shift 2 ;;
|
||||||
|
--user)
|
||||||
|
[ $# -ge 2 ] || die "--user needs a value" 2
|
||||||
|
RUNNER_USER="$2"; shift 2 ;;
|
||||||
|
--local) LOCAL=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "unknown flag: $1" 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- validation ------------------------------------------------------------
|
||||||
|
[ -n "$REPO" ] || die "--repo <owner/repo> is required" 2
|
||||||
|
if ! printf '%s' "$REPO" | grep -qE '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
|
||||||
|
die "--repo must be owner/repo" 2
|
||||||
|
fi
|
||||||
|
[ "$RUNNER_USER" != "root" ] || die "runner user must not be root" 2
|
||||||
|
|
||||||
|
# --- guards ----------------------------------------------------------------
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
|
|
||||||
|
id -u "$RUNNER_USER" >/dev/null 2>&1 \
|
||||||
|
|| die "no runner installed (no ${RUNNER_USER} user) — use: rig runner install"
|
||||||
|
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
|
||||||
|
RUNNER_DIR="$USER_HOME/actions-runner"
|
||||||
|
[ -e "$RUNNER_DIR/.runner" ] \
|
||||||
|
|| die "no runner registered in ${RUNNER_DIR} — use: rig runner install"
|
||||||
|
|
||||||
|
# --- what is it registered to now? ------------------------------------------
|
||||||
|
json_field() {
|
||||||
|
grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$1" \
|
||||||
|
| head -n1 | sed 's/.*:[[:space:]]*"//; s/"$//'
|
||||||
|
}
|
||||||
|
CURRENT_URL="$(json_field "$RUNNER_DIR/.runner" gitHubUrl)"
|
||||||
|
TARGET_URL="https://github.com/${REPO}"
|
||||||
|
|
||||||
|
if [ "$CURRENT_URL" = "$TARGET_URL" ]; then
|
||||||
|
log "already registered to ${REPO}; nothing to do"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep the runner's identity across the move unless told otherwise.
|
||||||
|
if [ -z "$RUNNER_NAME" ]; then
|
||||||
|
RUNNER_NAME="$(json_field "$RUNNER_DIR/.runner" agentName)"
|
||||||
|
[ -n "$RUNNER_NAME" ] || die "could not read the current runner name from ${RUNNER_DIR}/.runner"
|
||||||
|
fi
|
||||||
|
if [ -z "$LABELS" ]; then
|
||||||
|
if [ -r "$RUNNER_DIR/.rig-labels" ]; then
|
||||||
|
LABELS="$(cat "$RUNNER_DIR/.rig-labels")"
|
||||||
|
else
|
||||||
|
LABELS="ci-runner"
|
||||||
|
warn "this box has no rig label record (installed before rig kept one)"
|
||||||
|
warn "re-registering with the default labels: ${LABELS}"
|
||||||
|
warn "if your workflows' runs-on expects anything else, ctrl-c and pass --labels"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "moving runner ${RUNNER_NAME} from ${CURRENT_URL} to ${TARGET_URL}"
|
||||||
|
log "labels: ${LABELS}"
|
||||||
|
|
||||||
|
# --- tokens, both up front --------------------------------------------------
|
||||||
|
# Collected before anything is torn down: a missing or expired token must fail
|
||||||
|
# while the runner is still registered and working.
|
||||||
|
if [ "$LOCAL" -eq 0 ]; then
|
||||||
|
RUNNER_REMOVE_TOKEN="${RUNNER_REMOVE_TOKEN:-}"
|
||||||
|
if [ -z "$RUNNER_REMOVE_TOKEN" ]; then
|
||||||
|
read -rsp "removal token for ${CURRENT_URL} (short-lived): " RUNNER_REMOVE_TOKEN
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
[ -n "$RUNNER_REMOVE_TOKEN" ] || die "empty removal token"
|
||||||
|
export RUNNER_REMOVE_TOKEN
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUNNER_TOKEN="${RUNNER_TOKEN:-}"
|
||||||
|
if [ -z "$RUNNER_TOKEN" ]; then
|
||||||
|
read -rsp "registration token for ${TARGET_URL} (short-lived): " RUNNER_TOKEN
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
[ -n "$RUNNER_TOKEN" ] || die "empty registration token"
|
||||||
|
export RUNNER_TOKEN
|
||||||
|
|
||||||
|
# --- move -------------------------------------------------------------------
|
||||||
|
REMOVE_ARGS=(--user "$RUNNER_USER")
|
||||||
|
[ "$LOCAL" -eq 1 ] && REMOVE_ARGS+=(--local)
|
||||||
|
"$HERE/runner-remove.sh" "${REMOVE_ARGS[@]}"
|
||||||
|
|
||||||
|
if ! "$HERE/runner-install.sh" \
|
||||||
|
--repo "$REPO" --name "$RUNNER_NAME" --labels "$LABELS" --user "$RUNNER_USER"
|
||||||
|
then
|
||||||
|
warn "the runner is now deregistered from ${CURRENT_URL} and NOT registered anywhere"
|
||||||
|
die "re-registration failed — fix the cause, then run: rig runner install --repo ${REPO} --name ${RUNNER_NAME} --labels ${LABELS} --user ${RUNNER_USER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "repointed to ${TARGET_URL}"
|
||||||
|
log "verify it shows Idle under that repo's Settings > Actions > Runners, and gone from the old one"
|
||||||
81
commands/runner-status.sh
Executable file
81
commands/runner-status.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig runner status — what is this box's runner registered to?
|
||||||
|
# Read-only: reports what is already on the box. No credential, no network call.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() { printf 'rig-runner: %s\n' "$*"; }
|
||||||
|
die() { printf 'rig-runner: ERROR: %s\n' "$1" >&2; exit "${2:-1}"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: rig runner status [--user <name>]
|
||||||
|
|
||||||
|
--user <name> unprivileged service user (default: github-runner)
|
||||||
|
|
||||||
|
Prints the repository this box's runner is registered to, its runner name,
|
||||||
|
the labels rig recorded when it registered, the install directory, and the
|
||||||
|
systemd unit and its state.
|
||||||
|
|
||||||
|
Reads only the runner's own on-disk config — no GitHub token, no network
|
||||||
|
call. Exits 1 when no runner is installed.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- args (validated before the root check, so errors are testable) ---------
|
||||||
|
RUNNER_USER="github-runner"
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--user)
|
||||||
|
[ $# -ge 2 ] || die "--user needs a value" 2
|
||||||
|
RUNNER_USER="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) die "unknown flag: $1" 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- validation ------------------------------------------------------------
|
||||||
|
[ "$RUNNER_USER" != "root" ] || die "runner user must not be root" 2
|
||||||
|
|
||||||
|
# --- guards ----------------------------------------------------------------
|
||||||
|
[ "$(id -u)" -eq 0 ] || die "must run as root"
|
||||||
|
|
||||||
|
id -u "$RUNNER_USER" >/dev/null 2>&1 \
|
||||||
|
|| die "no runner installed (no ${RUNNER_USER} user on this box)"
|
||||||
|
USER_HOME="$(getent passwd "$RUNNER_USER" | cut -d: -f6)"
|
||||||
|
RUNNER_DIR="$USER_HOME/actions-runner"
|
||||||
|
[ -e "$RUNNER_DIR/.runner" ] \
|
||||||
|
|| die "no runner registered in ${RUNNER_DIR}"
|
||||||
|
|
||||||
|
# --- read the runner's own config -------------------------------------------
|
||||||
|
# .runner is JSON. Kept dependency-free on purpose: a rig-bootstrapped box has
|
||||||
|
# no jq, and installing one to read five fields would be a poor trade.
|
||||||
|
json_field() {
|
||||||
|
grep -o "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$1" \
|
||||||
|
| head -n1 | sed 's/.*:[[:space:]]*"//; s/"$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
REPO_URL="$(json_field "$RUNNER_DIR/.runner" gitHubUrl)"
|
||||||
|
RUNNER_NAME="$(json_field "$RUNNER_DIR/.runner" agentName)"
|
||||||
|
|
||||||
|
# GitHub owns the labels; the runner does not persist them locally. rig records
|
||||||
|
# what it registered with, so a box installed before this existed reports the
|
||||||
|
# honest answer rather than a guess.
|
||||||
|
if [ -r "$RUNNER_DIR/.rig-labels" ]; then
|
||||||
|
LABELS="$(cat "$RUNNER_DIR/.rig-labels")"
|
||||||
|
else
|
||||||
|
LABELS="(not recorded on this box — GitHub holds them; see the repo's Settings > Actions > Runners)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r "$RUNNER_DIR/.service" ]; then
|
||||||
|
UNIT="$(cat "$RUNNER_DIR/.service")"
|
||||||
|
STATE="$(systemctl is-active "$UNIT" 2>/dev/null || true)"
|
||||||
|
SERVICE="${UNIT} (${STATE:-unknown})"
|
||||||
|
else
|
||||||
|
SERVICE="(not installed as a service)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "repo: ${REPO_URL:-unknown}"
|
||||||
|
log "name: ${RUNNER_NAME:-unknown}"
|
||||||
|
log "labels: ${LABELS}"
|
||||||
|
log "dir: ${RUNNER_DIR}"
|
||||||
|
log "service: ${SERVICE}"
|
||||||
32
test/cli.sh
32
test/cli.sh
|
|
@ -80,6 +80,38 @@ else
|
||||||
echo "skip: runner non-root refusal (running as root)"
|
echo "skip: runner non-root refusal (running as root)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
check "runner: bad subcommand exits 2" 2 "usage:" "$ROOT/bin/rig" runner frobnicate
|
||||||
|
|
||||||
|
check "runner status: --help exits 0" 0 "usage:" "$ROOT/commands/runner-status.sh" --help
|
||||||
|
check "runner status: user needs value" 2 "needs a value" "$ROOT/commands/runner-status.sh" --user
|
||||||
|
check "runner status: refuses --user root" 2 "must not be root" "$ROOT/commands/runner-status.sh" --user root
|
||||||
|
check "runner status: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/runner-status.sh" --nope
|
||||||
|
|
||||||
|
check "runner remove: --help exits 0" 0 "usage:" "$ROOT/commands/runner-remove.sh" --help
|
||||||
|
check "runner remove: user needs value" 2 "needs a value" "$ROOT/commands/runner-remove.sh" --user
|
||||||
|
check "runner remove: refuses --user root" 2 "must not be root" "$ROOT/commands/runner-remove.sh" --user root
|
||||||
|
check "runner remove: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/runner-remove.sh" --nope
|
||||||
|
|
||||||
|
check "runner repoint: --help exits 0" 0 "usage:" "$ROOT/commands/runner-repoint.sh" --help
|
||||||
|
check "runner repoint: repo required" 2 "--repo" "$ROOT/commands/runner-repoint.sh"
|
||||||
|
check "runner repoint: repo needs value" 2 "needs a value" "$ROOT/commands/runner-repoint.sh" --repo
|
||||||
|
check "runner repoint: rejects bad slug" 2 "owner/repo" "$ROOT/commands/runner-repoint.sh" --repo not-a-slug
|
||||||
|
check "runner repoint: labels need value" 2 "needs a value" "$ROOT/commands/runner-repoint.sh" --repo acme/widgets --labels
|
||||||
|
check "runner repoint: refuses --user root" 2 "must not be root" "$ROOT/commands/runner-repoint.sh" --repo acme/widgets --user root
|
||||||
|
check "runner repoint: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/runner-repoint.sh" --nope
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
check "runner status: refuses non-root" 1 "must run as root" "$ROOT/commands/runner-status.sh"
|
||||||
|
check "runner remove: refuses non-root" 1 "must run as root" \
|
||||||
|
env RUNNER_REMOVE_TOKEN=x "$ROOT/commands/runner-remove.sh"
|
||||||
|
# --local too: the token-free path must still not be runnable by the runner user.
|
||||||
|
check "runner remove: --local refuses non-root" 1 "must run as root" \
|
||||||
|
"$ROOT/commands/runner-remove.sh" --local
|
||||||
|
check "runner repoint: refuses non-root" 1 "must run as root" \
|
||||||
|
env RUNNER_REMOVE_TOKEN=x RUNNER_TOKEN=y "$ROOT/commands/runner-repoint.sh" --repo acme/widgets
|
||||||
|
else
|
||||||
|
echo "skip: runner status/remove/repoint non-root refusals (running as root)"
|
||||||
|
fi
|
||||||
|
|
||||||
# The dump script ships to control-plane boxes as an embedded heredoc. A syntax
|
# The dump script ships to control-plane boxes as an embedded heredoc. A syntax
|
||||||
# error in it would be invisible here and would first surface at 04:00 on a live
|
# error in it would be invisible here and would first surface at 04:00 on a live
|
||||||
# control plane. Extract it and syntax-check what actually gets written.
|
# control plane. Extract it and syntax-check what actually gets written.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue