diff --git a/manage_runner.sh b/manage_runner.sh new file mode 100755 index 0000000..8419b81 --- /dev/null +++ b/manage_runner.sh @@ -0,0 +1,408 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# manage_runner.sh — Add, remove, or list Gitea Actions runners +# Reads runner definitions from runners.conf (pipe-delimited format). +# Supports two runner types: +# docker — Linux hosts, deployed as Docker container via docker-compose +# native — macOS, deployed as binary + launchd service +# Can be used standalone or called by phase3_runners.sh / phase3_teardown.sh. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_INTERNAL_URL GITEA_ADMIN_TOKEN ACT_RUNNER_VERSION + +RUNNERS_CONF="${SCRIPT_DIR}/runners.conf" + +# --------------------------------------------------------------------------- +# Usage +# --------------------------------------------------------------------------- +usage() { + cat >&2 < [options] + +Commands: + add --name Deploy and register a runner + remove --name Stop and deregister a runner + list Show all runners with status + +Options: + --name Runner name as defined in runners.conf + --help Show this help +EOF + exit 1 +} + +# --------------------------------------------------------------------------- +# Parse a runner entry from runners.conf by name +# Sets: RUNNER_NAME, RUNNER_SSH_HOST, RUNNER_SSH_USER, RUNNER_SSH_PORT, +# RUNNER_DATA_PATH, RUNNER_LABELS, RUNNER_TYPE +# Returns 1 if not found. +# --------------------------------------------------------------------------- +parse_runner_entry() { + local target_name="$1" + + if [[ ! -f "$RUNNERS_CONF" ]]; then + log_error "runners.conf not found at $RUNNERS_CONF" + return 1 + fi + + while IFS='|' read -r name host user port path labels type; do + # Skip comments and blank lines + [[ "$name" =~ ^[[:space:]]*# ]] && continue + [[ -z "$name" ]] && continue + + # Trim whitespace from fields + name=$(echo "$name" | xargs) + + if [[ "$name" == "$target_name" ]]; then + RUNNER_NAME="$name" + RUNNER_SSH_HOST=$(echo "$host" | xargs) + RUNNER_SSH_USER=$(echo "$user" | xargs) + RUNNER_SSH_PORT=$(echo "$port" | xargs) + RUNNER_DATA_PATH=$(echo "$path" | xargs) + RUNNER_LABELS=$(echo "$labels" | xargs) + RUNNER_TYPE=$(echo "$type" | xargs) + return 0 + fi + done < "$RUNNERS_CONF" + + log_error "Runner '$target_name' not found in runners.conf" + return 1 +} + +# --------------------------------------------------------------------------- +# List all runner entries from runners.conf (without looking up a name) +# Outputs lines to stdout: name|host|user|port|path|labels|type +# --------------------------------------------------------------------------- +all_runner_entries() { + if [[ ! -f "$RUNNERS_CONF" ]]; then + log_error "runners.conf not found at $RUNNERS_CONF" + return 1 + fi + + while IFS= read -r line; do + # Skip comments and blank lines + [[ "$line" =~ ^[[:space:]]*# ]] && continue + [[ -z "$line" ]] && continue + echo "$line" + done < "$RUNNERS_CONF" +} + +# --------------------------------------------------------------------------- +# Execute a command on the runner's host. +# For "local" hosts (macOS), runs the command directly. +# For remote hosts, SSHs into them — uses direct ssh (not ssh_exec from +# common.sh) because runner hosts have their own SSH creds defined in +# runners.conf, not from the standard *_IP/*_SSH_USER env vars. +# --------------------------------------------------------------------------- +runner_ssh() { + local cmd="$*" + + if [[ "$RUNNER_SSH_HOST" == "local" ]]; then + # macOS runner — execute directly on this machine + eval "$cmd" + else + ssh -o ConnectTimeout=10 \ + -o StrictHostKeyChecking=accept-new \ + -o BatchMode=yes \ + -p "${RUNNER_SSH_PORT}" \ + "${RUNNER_SSH_USER}@${RUNNER_SSH_HOST}" \ + "$cmd" + fi +} + +# --------------------------------------------------------------------------- +# SCP a file to the runner's host. +# For "local" hosts, just copies the file. +# --------------------------------------------------------------------------- +runner_scp() { + local src="$1" dst="$2" + + if [[ "$RUNNER_SSH_HOST" == "local" ]]; then + cp "$src" "$dst" + else + scp -o ConnectTimeout=10 \ + -o StrictHostKeyChecking=accept-new \ + -o BatchMode=yes \ + -P "${RUNNER_SSH_PORT}" \ + "$src" "${RUNNER_SSH_USER}@${RUNNER_SSH_HOST}:${dst}" + fi +} + +# --------------------------------------------------------------------------- +# add_docker_runner — Deploy a runner as a Docker container on a Linux host +# Steps: +# 1. Create data directory +# 2. Render + SCP docker-compose.yml +# 3. Render + SCP runner-config.yaml +# 4. Start container +# The act_runner Docker image auto-registers with Gitea using env vars +# (GITEA_INSTANCE_URL + GITEA_RUNNER_REGISTRATION_TOKEN) on first boot. +# --------------------------------------------------------------------------- +add_docker_runner() { + log_info "Deploying Docker runner '${RUNNER_NAME}' on ${RUNNER_SSH_HOST}..." + + # Check if container is already running + local status + status=$(runner_ssh "docker ps --filter name=gitea-runner-${RUNNER_NAME} --format '{{.Status}}'" 2>/dev/null || true) + if [[ "$status" == *"Up"* ]]; then + log_info "Runner '${RUNNER_NAME}' already running — skipping" + return 0 + fi + + # Create data directory on remote host + runner_ssh "mkdir -p '${RUNNER_DATA_PATH}'" + + # Render docker-compose template with runner-specific vars + local tmpfile + tmpfile=$(mktemp) + export RUNNER_NAME RUNNER_LABELS RUNNER_DATA_PATH + export GITEA_RUNNER_REGISTRATION_TOKEN="${GITEA_RUNNER_REGISTRATION_TOKEN:-}" + render_template "${SCRIPT_DIR}/templates/docker-compose-runner.yml.tpl" "$tmpfile" + runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/docker-compose.yml" + rm -f "$tmpfile" + + # Render runner config + tmpfile=$(mktemp) + render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" + runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml" + rm -f "$tmpfile" + + # Start the container + runner_ssh "cd '${RUNNER_DATA_PATH}' && docker compose up -d 2>/dev/null || docker-compose up -d" + log_success "Docker runner '${RUNNER_NAME}' started on ${RUNNER_SSH_HOST}" +} + +# --------------------------------------------------------------------------- +# add_native_runner — Deploy a runner as a native binary on macOS +# Steps: +# 1. Download act_runner binary for macOS (arm64 or amd64) +# 2. Register with Gitea (--no-interactive) +# 3. Render launchd plist +# 4. Load plist via launchctl +# Native runners are used on macOS because Docker Desktop is heavyweight +# and unreliable for long-running background services. +# --------------------------------------------------------------------------- +add_native_runner() { + log_info "Deploying native runner '${RUNNER_NAME}' on local machine..." + + # Resolve ~ to actual home directory for local execution + RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}" + export RUNNER_DATA_PATH + + local plist_name="com.gitea.runner.${RUNNER_NAME}.plist" + local plist_path="$HOME/Library/LaunchAgents/${plist_name}" + + # Check if launchd service is already loaded + if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${RUNNER_NAME}"; then + log_info "Runner '${RUNNER_NAME}' already loaded in launchd — skipping" + return 0 + fi + + # Create data directory + mkdir -p "${RUNNER_DATA_PATH}" + + # Download act_runner binary if not present + if [[ ! -x "${RUNNER_DATA_PATH}/act_runner" ]]; then + # Detect architecture — Apple Silicon (arm64) vs Intel (x86_64) + local arch + arch=$(uname -m) + case "$arch" in + arm64) arch="arm64" ;; + x86_64) arch="amd64" ;; + *) log_error "Unsupported architecture: $arch"; return 1 ;; + esac + + local download_url="https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-darwin-${arch}" + log_info "Downloading act_runner v${ACT_RUNNER_VERSION} for darwin-${arch}..." + curl -sfL -o "${RUNNER_DATA_PATH}/act_runner" "$download_url" + chmod +x "${RUNNER_DATA_PATH}/act_runner" + log_success "act_runner binary downloaded" + fi + + # Register the runner with Gitea (generates .runner file in data dir) + # --no-interactive skips prompts, --config generates default config if missing + if [[ ! -f "${RUNNER_DATA_PATH}/.runner" ]]; then + log_info "Registering runner with Gitea..." + "${RUNNER_DATA_PATH}/act_runner" register \ + --no-interactive \ + --instance "${GITEA_INTERNAL_URL}" \ + --token "${GITEA_RUNNER_REGISTRATION_TOKEN:-}" \ + --name "${RUNNER_NAME}" \ + --labels "${RUNNER_LABELS}" \ + --config "${RUNNER_DATA_PATH}/config.yaml" + log_success "Runner registered" + fi + + # Render runner config (overwrites any default generated by register) + local tmpfile + tmpfile=$(mktemp) + export RUNNER_NAME RUNNER_LABELS RUNNER_DATA_PATH + render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" + cp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml" + rm -f "$tmpfile" + + # Render launchd plist + tmpfile=$(mktemp) + render_template "${SCRIPT_DIR}/templates/com.gitea.runner.plist.tpl" "$tmpfile" + mkdir -p "$HOME/Library/LaunchAgents" + cp "$tmpfile" "$plist_path" + rm -f "$tmpfile" + + # Load the launchd service + launchctl load "$plist_path" + log_success "Native runner '${RUNNER_NAME}' loaded via launchd" +} + +# --------------------------------------------------------------------------- +# remove_docker_runner — Stop + remove Docker runner container +# --------------------------------------------------------------------------- +remove_docker_runner() { + log_info "Removing Docker runner '${RUNNER_NAME}' from ${RUNNER_SSH_HOST}..." + + # Check if docker-compose file exists + if runner_ssh "test -f '${RUNNER_DATA_PATH}/docker-compose.yml'" 2>/dev/null; then + runner_ssh "cd '${RUNNER_DATA_PATH}' && docker compose down 2>/dev/null || docker-compose down" || true + log_success "Docker runner '${RUNNER_NAME}' stopped" + else + log_info "No docker-compose.yml found for runner '${RUNNER_NAME}' — already removed" + fi +} + +# --------------------------------------------------------------------------- +# remove_native_runner — Unload launchd service + remove binary + plist +# --------------------------------------------------------------------------- +remove_native_runner() { + log_info "Removing native runner '${RUNNER_NAME}' from local machine..." + + # Resolve ~ to actual home directory + RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}" + + local plist_name="com.gitea.runner.${RUNNER_NAME}.plist" + local plist_path="$HOME/Library/LaunchAgents/${plist_name}" + + # Unload launchd service if loaded + if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${RUNNER_NAME}"; then + launchctl unload "$plist_path" 2>/dev/null || true + log_success "Launchd service unloaded" + fi + + # Remove plist file + if [[ -f "$plist_path" ]]; then + rm -f "$plist_path" + log_success "Plist removed: $plist_path" + fi + + # Remove runner data directory (binary, config, registration) + if [[ -d "${RUNNER_DATA_PATH}" ]]; then + printf 'Remove runner data at %s? [y/N] ' "${RUNNER_DATA_PATH}" + read -r confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + rm -rf "${RUNNER_DATA_PATH}" + log_success "Runner data removed" + else + log_info "Runner data preserved" + fi + fi +} + +# --------------------------------------------------------------------------- +# list_runners — Print table of all runners with their Gitea status +# Queries the Gitea admin API for registered runners and cross-references +# with runners.conf to show which are online/offline/unregistered. +# --------------------------------------------------------------------------- +list_runners() { + log_info "Listing runners..." + + # Fetch registered runners from Gitea admin API + local api_runners + api_runners=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]") + + # Print header + printf '%-20s %-16s %-10s %-8s %-10s\n' "NAME" "HOST" "LABELS" "TYPE" "STATUS" + printf '%-20s %-16s %-10s %-8s %-10s\n' "----" "----" "------" "----" "------" + + # For each entry in runners.conf, look up status in API response + while IFS='|' read -r name host user port path labels type; do + # Skip comments and blank lines + [[ "$name" =~ ^[[:space:]]*# ]] && continue + [[ -z "$name" ]] && continue + + name=$(echo "$name" | xargs) + host=$(echo "$host" | xargs) + labels=$(echo "$labels" | xargs) + type=$(echo "$type" | xargs) + + # Search for this runner in the API response by name + local status + status=$(printf '%s' "$api_runners" | jq -r --arg n "$name" '.[] | select(.name == $n) | .status' 2>/dev/null || true) + + if [[ -z "$status" ]]; then + status="not-found" + fi + + printf '%-20s %-16s %-10s %-8s %-10s\n' "$name" "$host" "$labels" "$type" "$status" + done < "$RUNNERS_CONF" +} + +# --------------------------------------------------------------------------- +# Main — parse command and dispatch +# --------------------------------------------------------------------------- +COMMAND="" +RUNNER_ARG_NAME="" + +while [[ $# -gt 0 ]]; do + case "$1" in + add|remove|list) COMMAND="$1"; shift ;; + --name) + if [[ $# -lt 2 ]]; then log_error "--name requires a value"; usage; fi + RUNNER_ARG_NAME="$2"; shift 2 ;; + --help|-h) usage ;; + *) log_error "Unknown argument: $1"; usage ;; + esac +done + +if [[ -z "$COMMAND" ]]; then + log_error "No command specified" + usage +fi + +case "$COMMAND" in + add) + if [[ -z "$RUNNER_ARG_NAME" ]]; then + log_error "add requires --name " + usage + fi + require_vars GITEA_RUNNER_REGISTRATION_TOKEN + parse_runner_entry "$RUNNER_ARG_NAME" + case "$RUNNER_TYPE" in + docker) add_docker_runner ;; + native) add_native_runner ;; + *) log_error "Unknown runner type: $RUNNER_TYPE (must be 'docker' or 'native')"; exit 1 ;; + esac + ;; + remove) + if [[ -z "$RUNNER_ARG_NAME" ]]; then + log_error "remove requires --name " + usage + fi + parse_runner_entry "$RUNNER_ARG_NAME" + case "$RUNNER_TYPE" in + docker) remove_docker_runner ;; + native) remove_native_runner ;; + *) log_error "Unknown runner type: $RUNNER_TYPE"; exit 1 ;; + esac + ;; + list) + list_runners + ;; + *) + log_error "Unknown command: $COMMAND" + usage + ;; +esac diff --git a/phase3_post_check.sh b/phase3_post_check.sh new file mode 100755 index 0000000..09981b3 --- /dev/null +++ b/phase3_post_check.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase3_post_check.sh — Verify Phase 3 (Runners) succeeded +# Checks that every runner defined in runners.conf is registered and online +# in the Gitea admin panel. +# Exits 0 only if ALL checks pass. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_INTERNAL_URL GITEA_ADMIN_TOKEN + +log_info "=== Phase 3 Post-Check ===" + +RUNNERS_CONF="${SCRIPT_DIR}/runners.conf" + +if [[ ! -f "$RUNNERS_CONF" ]]; then + log_error "runners.conf not found" + exit 1 +fi + +PASS=0 +FAIL=0 + +# --------------------------------------------------------------------------- +# Fetch all registered runners from Gitea admin API (single call) +# This avoids making N API calls — one per runner — which would be slow. +# --------------------------------------------------------------------------- +API_RUNNERS=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]") + +# --------------------------------------------------------------------------- +# Check each runner: exists in API response AND status is online/idle/active +# A runner that registered but is not running will show as "offline". +# --------------------------------------------------------------------------- +while IFS='|' read -r name rest; do + # Skip comments and blank lines + [[ "$name" =~ ^[[:space:]]*# ]] && continue + [[ -z "$name" ]] && continue + + name=$(echo "$name" | xargs) + + # Look up runner by name in the API response + local_status=$(printf '%s' "$API_RUNNERS" | jq -r --arg n "$name" '.[] | select(.name == $n) | .status' 2>/dev/null || true) + + if [[ -z "$local_status" ]]; then + log_error "FAIL: Runner '${name}' not found in Gitea admin" + FAIL=$((FAIL + 1)) + elif [[ "$local_status" == "offline" ]]; then + log_error "FAIL: Runner '${name}' is registered but offline" + FAIL=$((FAIL + 1)) + else + log_success "Runner '${name}' is ${local_status}" + PASS=$((PASS + 1)) + fi +done < "$RUNNERS_CONF" + +# --------------------------------------------------------------------------- +# Check: runner count matches runners.conf +# --------------------------------------------------------------------------- +EXPECTED_COUNT=$(grep -v '^\s*#' "$RUNNERS_CONF" | grep -v '^\s*$' | wc -l | xargs) +ACTUAL_COUNT=$(printf '%s' "$API_RUNNERS" | jq 'length' 2>/dev/null || echo 0) + +if [[ "$ACTUAL_COUNT" -ge "$EXPECTED_COUNT" ]]; then + log_success "Runner count OK: ${ACTUAL_COUNT} registered (${EXPECTED_COUNT} expected)" + PASS=$((PASS + 1)) +else + log_error "FAIL: Only ${ACTUAL_COUNT} runners registered (expected ${EXPECTED_COUNT})" + FAIL=$((FAIL + 1)) +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +printf '\n' +log_info "Results: ${PASS} passed, ${FAIL} failed" + +if [[ $FAIL -gt 0 ]]; then + log_error "Phase 3 post-check FAILED" + exit 1 +else + log_success "Phase 3 post-check PASSED — all runners are online" + exit 0 +fi diff --git a/phase3_runners.sh b/phase3_runners.sh new file mode 100755 index 0000000..ebfc4eb --- /dev/null +++ b/phase3_runners.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase3_runners.sh — Deploy Gitea Actions runners from runners.conf +# Depends on: Phase 1 complete (Gitea running on Unraid with admin token) +# Steps: +# 1. Get runner registration token from Gitea admin API +# 2. Save token to .env for reuse by manage_runner.sh +# 3. Deploy each runner defined in runners.conf via manage_runner.sh +# Idempotent: skips already-deployed runners, reuses valid tokens. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_INTERNAL_URL GITEA_ADMIN_TOKEN ACT_RUNNER_VERSION + +phase_header 3 "Gitea Actions Runners" + +RUNNERS_CONF="${SCRIPT_DIR}/runners.conf" + +# --------------------------------------------------------------------------- +# Pre-check: runners.conf must exist with at least one entry +# --------------------------------------------------------------------------- +if [[ ! -f "$RUNNERS_CONF" ]]; then + log_error "runners.conf not found — copy runners.conf.example and fill in values" + exit 1 +fi + +# Count non-comment, non-blank lines to verify there are runners to deploy +RUNNER_COUNT=$(grep -v '^\s*#' "$RUNNERS_CONF" | grep -v '^\s*$' | wc -l | xargs) +if [[ "$RUNNER_COUNT" -eq 0 ]]; then + log_error "No runners defined in runners.conf" + exit 1 +fi +log_info "Found ${RUNNER_COUNT} runner(s) in runners.conf" + +# --------------------------------------------------------------------------- +# Step 1: Get or reuse runner registration token +# The registration token is a shared secret that all runners use to register +# themselves with the Gitea instance. It's retrieved from the admin API and +# saved to .env so manage_runner.sh can use it independently. +# --------------------------------------------------------------------------- +log_step 1 "Getting runner registration token..." + +if [[ -n "${GITEA_RUNNER_REGISTRATION_TOKEN:-}" ]]; then + log_info "Registration token already in .env — reusing" +else + # Gitea returns the token as a JSON object: {"token": "..."} + TOKEN_RESPONSE=$(gitea_api GET "/admin/runners/registration-token") + GITEA_RUNNER_REGISTRATION_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -r '.token') + + if [[ -z "$GITEA_RUNNER_REGISTRATION_TOKEN" ]] || [[ "$GITEA_RUNNER_REGISTRATION_TOKEN" == "null" ]]; then + log_error "Failed to get runner registration token from Gitea" + exit 1 + fi + + save_env_var "GITEA_RUNNER_REGISTRATION_TOKEN" "$GITEA_RUNNER_REGISTRATION_TOKEN" + log_success "Registration token saved to .env" +fi + +# --------------------------------------------------------------------------- +# Step 2: Deploy each runner via manage_runner.sh +# Iterates over every non-comment line in runners.conf, extracts the name +# (first pipe-delimited field), and invokes manage_runner.sh add. +# manage_runner.sh handles its own idempotency (skips already-running runners). +# --------------------------------------------------------------------------- +log_step 2 "Deploying runners..." + +DEPLOYED=0 +FAILED=0 + +while IFS='|' read -r name rest; do + # Skip comments and blank lines + [[ "$name" =~ ^[[:space:]]*# ]] && continue + [[ -z "$name" ]] && continue + + name=$(echo "$name" | xargs) + log_info "Processing runner: ${name}" + + if "${SCRIPT_DIR}/manage_runner.sh" add --name "$name"; then + DEPLOYED=$((DEPLOYED + 1)) + else + log_error "Failed to deploy runner: ${name}" + FAILED=$((FAILED + 1)) + fi +done < "$RUNNERS_CONF" + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +printf '\n' +log_info "Results: ${DEPLOYED} deployed, ${FAILED} failed (out of ${RUNNER_COUNT})" + +if [[ $FAILED -gt 0 ]]; then + log_error "Some runners failed to deploy — check logs above" + exit 1 +fi + +log_success "Phase 3 complete — all ${RUNNER_COUNT} runner(s) deployed" diff --git a/phase3_teardown.sh b/phase3_teardown.sh new file mode 100755 index 0000000..53d1fe2 --- /dev/null +++ b/phase3_teardown.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase3_teardown.sh — Tear down all runners defined in runners.conf +# For each runner: calls manage_runner.sh remove to stop and clean up. +# Clears GITEA_RUNNER_REGISTRATION_TOKEN from .env since it becomes useless +# after all runners are removed. +# Safe to run against already-torn-down runners (no errors). +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_INTERNAL_URL GITEA_ADMIN_TOKEN ACT_RUNNER_VERSION + +log_warn "=== Phase 3 Teardown: Runners ===" + +RUNNERS_CONF="${SCRIPT_DIR}/runners.conf" + +if [[ ! -f "$RUNNERS_CONF" ]]; then + log_info "runners.conf not found — nothing to tear down" + exit 0 +fi + +# --------------------------------------------------------------------------- +# Step 1: Remove each runner via manage_runner.sh +# manage_runner.sh handles its own safety (skips already-removed runners). +# --------------------------------------------------------------------------- +printf 'This will stop and remove all runners. Continue? [y/N] ' +read -r confirm +if [[ "$confirm" =~ ^[Yy]$ ]]; then + while IFS='|' read -r name rest; do + # Skip comments and blank lines + [[ "$name" =~ ^[[:space:]]*# ]] && continue + [[ -z "$name" ]] && continue + + name=$(echo "$name" | xargs) + log_info "Removing runner: ${name}" + "${SCRIPT_DIR}/manage_runner.sh" remove --name "$name" || true + done < "$RUNNERS_CONF" + log_success "All runners removed" +else + log_info "Skipped runner removal" +fi + +# --------------------------------------------------------------------------- +# Step 2: Clear the registration token from .env +# The token is single-use in some Gitea configurations — generating a new one +# on next deploy is safer than reusing a potentially stale token. +# --------------------------------------------------------------------------- +if [[ -n "${GITEA_RUNNER_REGISTRATION_TOKEN:-}" ]]; then + save_env_var "GITEA_RUNNER_REGISTRATION_TOKEN" "" + log_success "GITEA_RUNNER_REGISTRATION_TOKEN cleared from .env" +fi + +log_success "Phase 3 teardown complete"