#!/usr/bin/env bash # Register (or re-register) the GitHub Actions self-hosted runner on igneum-build-1 from this Mac. # infra/build-server/runner/register.sh fetch a registration token with gh, run provision.sh on the box with it # infra/build-server/runner/register.sh --status list the repository's runners (name, status, labels) and the box's unit # infra/build-server/runner/register.sh --host another box (7 October 2026, igneum-build-2): the same, against that ip; # BOX_HOSTNAME, RUNNER_NAME, RUNNER_LABELS, RUNNER_CPUS and RUNNER_JOBS # from this shell's environment travel with it (provision.sh would # otherwise rename the box igneum-build-1), e.g. # BOX_HOSTNAME=igneum-build-2 RUNNER_LABELS=igneum-build-1,igneum-build-2 RUNNER_CPUS=0-31 RUNNER_JOBS=32 \ # infra/build-server/runner/register.sh --host 142.132.249.238 # # The token: `gh api -X POST repos/igneum-network/igneum/actions/runners/registration-token` as igneum-labs (the CLAUDE.md gh # rule: that account must be ACTIVE; any other active account fails here before anything is fetched). It is a one-hour # registration token, not a credential the runner keeps (config.sh writes its own into /opt/actions-runner/.credentials, # owner runner, mode 600). It travels to the box on ssh stdin as the first line, followed by provision.sh itself; it is # never an argument of ssh, never written to a file on the Mac, and provision.sh never logs it. The whole of provision.sh # runs (idempotent, every other step says ok), so the box is also brought up to date. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_SLUG="${IGNEUM_GH_REPO:-igneum-network/igneum}" KEY="${IGNEUM_BUILD_KEY:-$HOME/.ssh/igneum_ed25519}" HOST_LINE="$(head -1 "${IGNEUM_BUILD_HOST_FILE:-$HOME/.config/igneum/build-server}" | tr -d '[:space:]')" IP="${HOST_LINE#*@}" if [ "${1:-}" = --host ]; then IP="${2:-}"; [ -n "$IP" ] || { echo "--host needs an ip" >&2; exit 1; }; shift 2 [ -n "${BOX_HOSTNAME:-}" ] || { echo "--host: set BOX_HOSTNAME (provision.sh would otherwise rename the box igneum-build-1)" >&2; exit 1; } fi [ -n "$IP" ] || { echo "no build server in ~/.config/igneum/build-server (infra/build-server/run-from-mac.sh writes it)" >&2; exit 1; } # the provisioning variables a second box needs, forwarded as assignments in front of the remote shell (values are plain # words: a hostname, a label list, a cpu range, a number; anything else is refused) FWD="" for v in BOX_HOSTNAME RUNNER_NAME RUNNER_LABELS RUNNER_CPUS RUNNER_JOBS; do val="${!v:-}"; [ -n "$val" ] || continue printf '%s' "$val" | grep -qE '^[A-Za-z0-9,._-]+$' || { echo "$v='$val' is not a plain word" >&2; exit 1; } FWD="$FWD $v=$val" done SSH=(ssh -i "$KEY" -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 "root@$IP") gh_josh() { local active active=$(gh auth status 2>/dev/null | awk '/Logged in to github.com account/ { acct=$7 } /Active account: true/ { print acct; exit }') if [ "$active" != igneum-labs ]; then gh auth switch --user igneum-labs >/dev/null 2>&1 || { echo "gh: cannot switch to igneum-labs (gh auth status: ${active:-no active account})" >&2; exit 1; } fi [ "$(gh api user --jq .login 2>/dev/null)" = igneum-labs ] || { echo "gh: the active token is not the igneum-labs login (stored as igneum-labs); refusing" >&2; exit 1; } } if [ "${1:-}" = --status ]; then gh_josh gh api "repos/$REPO_SLUG/actions/runners" --jq '.runners[] | "\(.name)\t\(.status)\tbusy=\(.busy)\t\(([.labels[].name]) | join(","))"' || echo "(no runners or no access)" "${SSH[@]}" 'systemctl list-units --type=service --no-legend "actions.runner.*" ; ls -la /opt/actions-runner/.runner 2>/dev/null || echo "not registered on the box"' exit 0 fi gh_josh echo "fetching a registration token for $REPO_SLUG as igneum-labs (igneum-labs) ..." TOKEN=$(gh api -X POST "repos/$REPO_SLUG/actions/runners/registration-token" --jq .token 2>/dev/null) || { echo "gh refused the registration token: the account needs admin on $REPO_SLUG (gh api repos/$REPO_SLUG --jq .permissions)" >&2; exit 1; } [ -n "$TOKEN" ] || { echo "empty token from gh" >&2; exit 1; } echo "token received (not shown); running provision.sh on root@$IP with it (first line of stdin, then the script)" # the first stdin line is the token, read by the remote shell before bash -s takes the rest as the script; the output is # kept in a temp file so the ssh exit code is read (a filter in the pipe would hide it) and any line carrying the token is # dropped before it is shown (provision.sh never prints it; this is the belt) OUT=$(mktemp); trap 'rm -f "$OUT"' EXIT set +e { printf '%s\n' "$TOKEN"; cat "$HERE/../provision.sh"; } | "${SSH[@]}" "IFS= read -r RUNNER_TOKEN; export RUNNER_TOKEN; MODE=provision$FWD bash -s" > "$OUT" 2>&1 RC=$? set -e grep -v -F "$TOKEN" "$OUT" || true unset TOKEN [ "$RC" = 0 ] || { echo "provision.sh exited $RC on the box" >&2; exit "$RC"; } echo "runners now registered on $REPO_SLUG:" gh api "repos/$REPO_SLUG/actions/runners" --jq '.runners[] | " \(.name)\t\(.status)\t\(([.labels[].name]) | join(","))"'