- lib/common.sh: add require_local_os, require_remote_os, require_remote_pkg_manager - setup/macbook.sh: require macOS (Darwin) - setup/unraid.sh: require remote is Linux - setup/fedora.sh: require remote is Linux + has dnf (RPM-based) - manage_runner.sh: native runner add/remove requires macOS - run_all.sh: control plane must be macOS - preflight.sh: 3 new checks (1: local=macOS, 2: Unraid=Linux, 3: Fedora=Linux+dnf) - phase5_migrate_pipelines.sh: fix sed -i to be portable (no macOS-only syntax) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
415 lines
14 KiB
Bash
Executable File
415 lines
14 KiB
Bash
Executable File
#!/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 <<EOF
|
|
Usage: $(basename "$0") <command> [options]
|
|
|
|
Commands:
|
|
add --name <runner_name> Deploy and register a runner
|
|
remove --name <runner_name> Stop and deregister a runner
|
|
list Show all runners with status
|
|
|
|
Options:
|
|
--name <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() {
|
|
# Native runners use launchctl + macOS-specific paths — must be macOS
|
|
require_local_os "Darwin" "Native runner deployment requires macOS (uses launchctl)"
|
|
|
|
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() {
|
|
# Native runners use launchctl + macOS-specific paths — must be macOS
|
|
require_local_os "Darwin" "Native runner removal requires macOS (uses launchctl)"
|
|
|
|
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 <runner_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 <runner_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
|