Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions pmoves/scripts/claws/bootstrap-node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ SCOPE=""
TARGET=""
SKIP_SSH=false
SKIP_GLANCES=false
WITH_RUNNER=false
RUNNER_LANE="ai-lab"
RUNNER_REPO="POWERFULMOVES/PMOVES.AI"

while [[ $# -gt 0 ]]; do
case "$1" in
--scope) SCOPE="$2"; shift 2 ;;
--target) TARGET="$2"; shift 2 ;;
--skip-ssh) SKIP_SSH=true; shift ;;
--skip-glances) SKIP_GLANCES=true; shift ;;
--with-runner) WITH_RUNNER=true; shift ;;
--runner-lane) RUNNER_LANE="$2"; shift 2 ;;
--runner-repo) RUNNER_REPO="$2"; shift 2 ;;
*) echo "Unknown: $1"; exit 1 ;;
esac
done
Expand All @@ -32,11 +38,15 @@ if [[ -z "$SCOPE" || -z "$TARGET" ]]; then
echo "Usage: bootstrap-node.sh --scope <name> --target <user@host>"
echo ""
echo "Options:"
echo " --skip-ssh Skip SSH connectivity check"
echo " --skip-glances Skip Glances installation"
echo " --skip-ssh Skip SSH connectivity check"
echo " --skip-glances Skip Glances installation"
echo " --with-runner Register a GitHub Actions runner (idempotent)"
echo " --runner-lane LANE Runner lane: ai-lab|vps|hotfix (default: ai-lab)"
echo " --runner-repo REPO GitHub repo for runner (default: POWERFULMOVES/PMOVES.AI)"
echo ""
echo "Example:"
echo " bootstrap-node.sh --scope 5090 --target root@pmoves-5090"
echo " bootstrap-node.sh --scope 5090 --target root@pmoves-5090 --with-runner --runner-lane ai-lab"
echo " bootstrap-node.sh --scope kvm4-1 --target root@\$HOSTINGER_KVM4_1_IP"
exit 1
fi
Expand Down Expand Up @@ -83,6 +93,23 @@ ssh "$TARGET" 'which gh > /dev/null 2>&1 && echo " gh: $(gh --version 2>/dev/nu

echo ""

# --- Step 3.5: Runner Registration (optional) ---
if $WITH_RUNNER; then
echo "[3.5/5] Registering GitHub Actions runner (lane: $RUNNER_LANE)..."
"$SCRIPT_DIR/setup-runner.sh" \
--target "$TARGET" \
--repo "$RUNNER_REPO" \
--lane "$RUNNER_LANE" || {
Comment on lines +99 to +102

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mark setup-runner.sh executable or invoke via bash

This call executes setup-runner.sh directly, but the new file is committed with mode 100644 in this commit, so it lacks the executable bit. In the --with-runner path, bootstrap will fail with a permission error before registration runs unless users manually chmod +x the script or invoke it through bash.

Useful? React with 👍 / 👎.

rc=$?
if [[ $rc -eq 2 ]]; then
echo " Drifted runner re-registered — verify online status before proceeding"
else
exit $rc
fi
}
echo ""
fi

# --- Step 4: Glances ---
if ! $SKIP_GLANCES; then
echo "[4/5] Setting up Glances monitoring..."
Expand Down
315 changes: 315 additions & 0 deletions pmoves/scripts/claws/setup-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
#!/usr/bin/env bash
# setup-runner.sh — Idempotent GitHub Actions runner registration on a target node
#
# Usage:
# setup-runner.sh --target <user@host> [OPTIONS]
#
# Options:
# --target HOST SSH target (required)
# --repo OWNER/REPO GitHub repo (default: POWERFULMOVES/PMOVES.AI)
# --lane LANE Runner lane: ai-lab|vps|hotfix|<custom> (default: ai-lab)
# --runner-name NAME Override runner name (default: pmoves-<lane>-<hostname>)
# --skip-docker-check Skip Docker availability check on target
# --dry-run Print actions without executing
#
# Auth (checked in order):
# GITHUB_PAT, GH_TOKEN, GITHUB_TOKEN — PAT passed as ACCESS_TOKEN to runner container
# (fallback) gh api registration token — short-lived (~1h), single-use
#
# Idempotency:
# Case 1 — runner registered + online: skip (no-op, exit 0)
# Case 2 — runner registered + offline: remove stale entry, re-register (exit 2)
# Case 3 — runner not registered: fresh register (exit 0)
#
# Exit codes:
# 0 success (registered or already healthy)
# 1 fatal (no auth, Docker unavailable, SSH unreachable)
# 2 drifted (stale removed + re-registered — caller should verify)

set -euo pipefail

# --- Defaults ---
TARGET=""
REPO="POWERFULMOVES/PMOVES.AI"
LANE="ai-lab"
RUNNER_NAME=""
SKIP_DOCKER_CHECK=false
DRY_RUN=false

# --- Arg parsing ---
while [[ $# -gt 0 ]]; do
case "$1" in
--target) TARGET="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--lane) LANE="$2"; shift 2 ;;
--runner-name) RUNNER_NAME="$2"; shift 2 ;;
--skip-docker-check) SKIP_DOCKER_CHECK=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done

if [[ -z "$TARGET" ]]; then
echo "Usage: setup-runner.sh --target <user@host> [--repo OWNER/REPO] [--lane LANE]"
echo ""
echo "Lanes: ai-lab (default) vps hotfix <custom>"
echo "Labels: ai-lab → self-hosted,ai-lab,gpu,Linux,X64"
echo " vps → self-hosted,vps,Linux,X64"
echo " hotfix → self-hosted,hotfix,Linux,X64"
echo " other → self-hosted,pmoves,<lane>,Linux,X64"
exit 1
fi

# --- Helpers ---
PASS=0; FAIL=0; WARN=0

check() {
local label="$1" status="$2" detail="${3:-}"
if [[ "$status" == "pass" ]]; then
echo " PASS $label${detail:+ — $detail}"
PASS=$((PASS + 1))
elif [[ "$status" == "warn" ]]; then
echo " WARN $label${detail:+ — $detail}"
WARN=$((WARN + 1))
else
echo " FAIL $label${detail:+ — $detail}"
FAIL=$((FAIL + 1))
fi
}

run_or_dry() {
if $DRY_RUN; then
echo " [dry-run] $*"
else
"$@"
fi
}

ssh_or_dry() {
if $DRY_RUN; then
echo " [dry-run] ssh $TARGET: $*"
else
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$TARGET" "$@"
fi
}

# Derive hostname from target (strip user@ prefix)
TARGET_HOST="${TARGET##*@}"

# Derive runner name
if [[ -z "$RUNNER_NAME" ]]; then
RUNNER_NAME="pmoves-${LANE}-${TARGET_HOST}"
fi

# Container name mirrors runner name
CONTAINER_NAME="pmoves-${LANE}-runner"

# Label mapping (matches local_cert_runners.py lanes 33-52)
case "$LANE" in
ai-lab) LABELS="self-hosted,ai-lab,gpu,Linux,X64" ;;
vps) LABELS="self-hosted,vps,Linux,X64" ;;
hotfix) LABELS="self-hosted,hotfix,Linux,X64" ;;
*) LABELS="self-hosted,pmoves,${LANE},Linux,X64" ;;
esac

echo "============================================"
echo " PMOVES Runner Registration"
echo "============================================"
echo " Target: $TARGET"
echo " Repo: $REPO"
echo " Lane: $LANE"
echo " Name: $RUNNER_NAME"
echo " Labels: $LABELS"
$DRY_RUN && echo " Mode: DRY RUN (no changes)"
echo ""

# --- Pre-flight: gh CLI ---
echo "[pre-flight]"
if ! command -v gh > /dev/null 2>&1; then
check "gh CLI" "fail" "not found — run: gh auth login"
exit 1
fi
if ! gh auth status > /dev/null 2>&1; then
check "gh auth" "fail" "not authenticated — run: gh auth login"
exit 1
fi
check "gh CLI" "pass" "$(gh --version 2>/dev/null | head -1)"
echo ""

# --- Pre-flight: SSH ---
echo "[ssh]"
if ! ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$TARGET" "echo ok" > /dev/null 2>&1; then
check "SSH $TARGET" "fail" "unreachable"
exit 1
fi
check "SSH $TARGET" "pass"
echo ""

# --- Pre-flight: Docker on target ---
echo "[docker]"
if ! $SKIP_DOCKER_CHECK; then
if ssh_or_dry "which docker > /dev/null 2>&1" 2>/dev/null; then
DOCKER_VER="unknown (dry-run)"
$DRY_RUN || DOCKER_VER=$(ssh -o ConnectTimeout=5 "$TARGET" "docker --version 2>/dev/null" || echo "unknown")
check "Docker on target" "pass" "$DOCKER_VER"
else
check "Docker on target" "fail" "not found — install Docker before registering runner"
exit 1
fi
else
check "Docker check" "warn" "skipped (--skip-docker-check)"
fi
echo ""

# --- Auth resolution ---
echo "[auth]"
PAT=""
AUTH_MODE=""

for var in GITHUB_PAT GH_TOKEN GITHUB_TOKEN; do
val="${!var:-}"
if [[ -n "$val" && "$val" != "placeholder" && ${#val} -gt 10 ]]; then
PAT="$val"
AUTH_MODE="pat ($var)"
break
fi
done

if [[ -n "$PAT" ]]; then
check "Auth" "pass" "$AUTH_MODE"
else
check "Auth" "warn" "no PAT found — will use short-lived registration token"
AUTH_MODE="registration-token"
fi
echo ""

# --- Check existing runner state ---
echo "[runner state]"
RUNNER_JSON=$(gh api "repos/${REPO}/actions/runners" --paginate 2>/dev/null | \
jq -sc '[.[].runners[] | select(.name == "'"${RUNNER_NAME}"'")]' 2>/dev/null || echo "[]")

RUNNER_COUNT=$(echo "$RUNNER_JSON" | jq 'length')
RUNNER_ID=$(echo "$RUNNER_JSON" | jq -r '.[0].id // empty')
RUNNER_STATUS=$(echo "$RUNNER_JSON" | jq -r '.[0].status // empty')

if [[ "$RUNNER_COUNT" -gt 1 ]]; then
check "Runner uniqueness" "warn" "$RUNNER_COUNT runners named '$RUNNER_NAME' — using first match"
fi

echo " Runner '$RUNNER_NAME': count=$RUNNER_COUNT status=${RUNNER_STATUS:-none}"
echo ""

# --- Case routing ---
if [[ -n "$RUNNER_ID" && "$RUNNER_STATUS" == "online" ]]; then
# Case 1: online — no-op
echo "[case 1: online — skip]"
check "Runner $RUNNER_NAME" "pass" "already online (id=$RUNNER_ID)"
echo ""
echo "=== Results: $PASS passed, $FAIL failed, $WARN warnings ==="
exit 0

elif [[ -n "$RUNNER_ID" && "$RUNNER_STATUS" != "online" ]]; then
# Case 2: drifted — remove + re-register
echo "[case 2: offline/drifted — remove + re-register]"
check "Stale runner detected" "warn" "status=$RUNNER_STATUS id=$RUNNER_ID"

echo " Removing stale runner entry from GitHub..."
run_or_dry gh api --method DELETE "repos/${REPO}/actions/runners/${RUNNER_ID}"

echo " Removing old container on target..."
ssh_or_dry "docker rm -f ${CONTAINER_NAME} > /dev/null 2>&1 || true"
check "Stale cleanup" "pass"
echo ""
CASE=2

else
# Case 3: not registered
echo "[case 3: not registered — fresh register]"
CASE=0
fi

# --- Get registration token ---
echo "[registration]"
REG_TOKEN=$(gh api --method POST "repos/${REPO}/actions/runners/registration-token" \
--jq '.token' 2>/dev/null || true)

if [[ -z "$REG_TOKEN" ]]; then
check "Registration token" "fail" "gh api call failed — check REPO and gh auth scopes"
Comment on lines +233 to +237

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip registration-token API call when PAT mode is selected

The script always requests a GitHub registration token and exits on failure, even when a PAT was already chosen for ACCESS_TOKEN. In environments where local gh auth can list runners but cannot create registration tokens, this causes a hard failure despite PAT-based container registration being otherwise viable, so valid PAT setups can fail unnecessarily.

Useful? React with 👍 / 👎.

exit 1
fi
check "Registration token" "pass" "obtained (valid ~1h)"

# Choose env var name for auth in container
if [[ -n "$PAT" ]]; then
AUTH_ENV_VAR="ACCESS_TOKEN"
AUTH_VAL="$PAT"
else
AUTH_ENV_VAR="RUNNER_TOKEN"
AUTH_VAL="$REG_TOKEN"
fi

# --- Launch runner container on target ---
echo ""
echo "[container]"
VOLUME_PATH="\$HOME/.config/pmoves"

DOCKER_CMD="docker run -d \\
--restart unless-stopped \\
--name ${CONTAINER_NAME} \\
-e RUNNER_ALLOW_RUNNER_REUSE=true \\
-e REPO_URL=https://github.com/${REPO} \\
-e RUNNER_NAME=${RUNNER_NAME} \\
-e LABELS=${LABELS} \\
-e RUNNER_WORKDIR=/tmp/runner-${LANE} \\
-e ${AUTH_ENV_VAR} \\
-v ${VOLUME_PATH}:/root/.config/pmoves \\
myoung34/github-runner:latest"

if $DRY_RUN; then
echo " [dry-run] ssh $TARGET with ${AUTH_ENV_VAR} injected:"
echo " $DOCKER_CMD"
check "Container launch" "pass" "dry-run"
else
# Send auth on stdin so the secret is not present in local ssh argv or the remote command string.
# shellcheck disable=SC2029
if printf '%s\n' "$AUTH_VAL" | ssh -o ConnectTimeout=10 "$TARGET" \
"IFS= read -r ${AUTH_ENV_VAR}; export ${AUTH_ENV_VAR}; ${DOCKER_CMD}" > /dev/null 2>&1; then
check "Container launch" "pass" "$CONTAINER_NAME started"
else
check "Container launch" "fail" "docker run failed on $TARGET"
exit 1
fi
fi
echo ""

# --- Poll until online (skip in dry-run) ---
echo "[verify online]"
if $DRY_RUN; then
check "Runner online" "pass" "dry-run (skipped poll)"
else
RETRIES=8
SLEEP=6
ONLINE=false
for i in $(seq 1 $RETRIES); do
CURRENT_STATUS=$(gh api "repos/${REPO}/actions/runners" \
--jq ".runners[] | select(.name == \"${RUNNER_NAME}\") | .status" 2>/dev/null || echo "")
if [[ "$CURRENT_STATUS" == "online" ]]; then
ONLINE=true
break
fi
echo " Waiting for runner (attempt $i/$RETRIES, status=${CURRENT_STATUS:-unknown})..."
sleep $SLEEP
done

if $ONLINE; then
check "Runner online" "pass" "$RUNNER_NAME"
else
check "Runner online" "warn" "not yet online after $((RETRIES * SLEEP))s — may still be registering"
fi
fi
echo ""

echo "=== Results: $PASS passed, $FAIL failed, $WARN warnings ==="
[[ $FAIL -gt 0 ]] && exit 1
[[ $CASE -eq 2 ]] && exit 2
exit 0
Loading