rig/commands/runner-remove.sh
dan-claude-bot e72663ef62 fix: headless credential prompts refuse loudly, naming their variable (#42)
A bare 'read -rsp' with no tty exits non-zero and set -e ends the script
with no output at all: the release drill watched 'rig runner remove' exit 1
in complete silence, and a guest bootstrap stop mid-log the same way. Every
prompt now checks for a tty first and dies naming the variable that
unblocks an unattended run; every read is || die-guarded so EOF at a real
prompt also gets a last word. The no-bare-read test swept up runner
repoint's two prompts, which the issue had not counted.

Fixes #42

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:15:08 +00:00

112 lines
4.5 KiB
Bash
Executable file

#!/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:-}"
# Prompt only on a tty: headless, a bare `read` dies under set -e with no
# message at all — the drill hit exactly this (wrong env var, exit 1, zero
# output). Refuse loudly, naming both the variable and the tokenless out.
if [ -z "$REMOVE_TOKEN" ]; then
[ -t 0 ] || die "RUNNER_REMOVE_TOKEN is unset and stdin is not a tty — set RUNNER_REMOVE_TOKEN to run unattended, or use --local"
read -rsp "runner removal token (short-lived): " REMOVE_TOKEN || { echo; die "no removal token read (EOF) — set RUNNER_REMOVE_TOKEN to run unattended, or use --local"; }
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"