643 lines
24 KiB
Bash
Executable File
643 lines
24 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 (INI 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 (= section name in runners.conf)
|
|
--help Show this help
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parse a runner entry from runners.conf (INI format) by section name.
|
|
# Sets globals: RUNNER_NAME, RUNNER_HOST, RUNNER_TYPE, RUNNER_DATA_PATH,
|
|
# RUNNER_LABELS, RUNNER_SECTION_IMAGE, RUNNER_REPOS, RUNNER_CAPACITY,
|
|
# RUNNER_CPU, RUNNER_MEMORY, RUNNER_BOOT
|
|
# Also resolves: RUNNER_SSH_HOST, RUNNER_SSH_USER, RUNNER_SSH_PORT,
|
|
# RUNNER_SSH_KEY (from .env or custom section keys)
|
|
# 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
|
|
|
|
# Check section exists
|
|
if ! ini_list_sections "$RUNNERS_CONF" | grep -qx "$target_name"; then
|
|
log_error "Runner '$target_name' not found in runners.conf"
|
|
return 1
|
|
fi
|
|
|
|
RUNNER_NAME="$target_name"
|
|
RUNNER_HOST=$(ini_get "$RUNNERS_CONF" "$target_name" "host" "")
|
|
RUNNER_TYPE=$(ini_get "$RUNNERS_CONF" "$target_name" "type" "")
|
|
RUNNER_DATA_PATH=$(ini_get "$RUNNERS_CONF" "$target_name" "data_path" "")
|
|
RUNNER_LABELS=$(ini_get "$RUNNERS_CONF" "$target_name" "labels" "")
|
|
RUNNER_SECTION_IMAGE=$(ini_get "$RUNNERS_CONF" "$target_name" "default_image" "")
|
|
RUNNER_REPOS=$(ini_get "$RUNNERS_CONF" "$target_name" "repos" "all")
|
|
RUNNER_CAPACITY=$(ini_get "$RUNNERS_CONF" "$target_name" "capacity" "1")
|
|
RUNNER_CPU=$(ini_get "$RUNNERS_CONF" "$target_name" "cpu" "")
|
|
RUNNER_MEMORY=$(ini_get "$RUNNERS_CONF" "$target_name" "memory" "")
|
|
# boot: controls launchd install location for native runners.
|
|
# "true" → /Library/LaunchDaemons/ (starts at boot, requires sudo)
|
|
# "false" (default) → ~/Library/LaunchAgents/ (starts at login)
|
|
RUNNER_BOOT=$(ini_get "$RUNNERS_CONF" "$target_name" "boot" "false")
|
|
|
|
# --- Host resolution ---
|
|
case "$RUNNER_HOST" in
|
|
unraid)
|
|
RUNNER_SSH_HOST="${UNRAID_IP:-}"
|
|
RUNNER_SSH_USER="${UNRAID_SSH_USER:-}"
|
|
RUNNER_SSH_PORT="${UNRAID_SSH_PORT:-22}"
|
|
RUNNER_SSH_KEY="${UNRAID_SSH_KEY:-}"
|
|
;;
|
|
fedora)
|
|
RUNNER_SSH_HOST="${FEDORA_IP:-}"
|
|
RUNNER_SSH_USER="${FEDORA_SSH_USER:-}"
|
|
RUNNER_SSH_PORT="${FEDORA_SSH_PORT:-22}"
|
|
RUNNER_SSH_KEY="${FEDORA_SSH_KEY:-}"
|
|
;;
|
|
local)
|
|
RUNNER_SSH_HOST="local"
|
|
RUNNER_SSH_USER=""
|
|
RUNNER_SSH_PORT=""
|
|
RUNNER_SSH_KEY=""
|
|
;;
|
|
custom)
|
|
RUNNER_SSH_HOST=$(ini_get "$RUNNERS_CONF" "$target_name" "ssh_host" "")
|
|
RUNNER_SSH_USER=$(ini_get "$RUNNERS_CONF" "$target_name" "ssh_user" "")
|
|
RUNNER_SSH_PORT=$(ini_get "$RUNNERS_CONF" "$target_name" "ssh_port" "22")
|
|
RUNNER_SSH_KEY=$(ini_get "$RUNNERS_CONF" "$target_name" "ssh_key" "")
|
|
;;
|
|
*)
|
|
log_error "Runner '$target_name': unknown host '$RUNNER_HOST' (must be unraid, fedora, local, or custom)"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
# --- Validate SSH credentials for remote hosts ---
|
|
if [[ "$RUNNER_SSH_HOST" != "local" ]]; then
|
|
if [[ -z "$RUNNER_SSH_HOST" ]]; then
|
|
log_error "Runner '$target_name': SSH host is empty (check .env for ${RUNNER_HOST^^}_IP)"
|
|
return 1
|
|
fi
|
|
if [[ -z "$RUNNER_SSH_USER" ]]; then
|
|
log_error "Runner '$target_name': SSH user is empty (check .env for ${RUNNER_HOST^^}_SSH_USER)"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# --- Validate required fields ---
|
|
if [[ -z "$RUNNER_TYPE" ]]; then
|
|
log_error "Runner '$target_name': type is empty (must be docker or native)"
|
|
return 1
|
|
fi
|
|
if [[ "$RUNNER_TYPE" != "docker" ]] && [[ "$RUNNER_TYPE" != "native" ]]; then
|
|
log_error "Runner '$target_name': type='$RUNNER_TYPE' (must be docker or native)"
|
|
return 1
|
|
fi
|
|
if [[ -z "$RUNNER_DATA_PATH" ]]; then
|
|
log_error "Runner '$target_name': data_path is empty"
|
|
return 1
|
|
fi
|
|
if [[ -z "$RUNNER_LABELS" ]]; then
|
|
log_error "Runner '$target_name': labels is empty"
|
|
return 1
|
|
fi
|
|
if ! [[ "$RUNNER_CAPACITY" =~ ^[1-9][0-9]*$ ]]; then
|
|
log_error "Runner '$target_name': capacity='$RUNNER_CAPACITY' (must be positive integer >= 1)"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Resolve the runner's image (with LOCAL_REGISTRY prefix if set).
|
|
# Sets RUNNER_RESOLVED_IMAGE.
|
|
# ---------------------------------------------------------------------------
|
|
resolve_runner_image() {
|
|
# Per-runner image from runners.conf takes priority, then .env global, then hardcoded fallback
|
|
local image="${RUNNER_SECTION_IMAGE:-${RUNNER_DEFAULT_IMAGE:-}}"
|
|
if [[ -z "$image" ]] && [[ "$RUNNER_TYPE" == "docker" ]]; then
|
|
image="catthehacker/ubuntu:act-latest"
|
|
fi
|
|
|
|
if [[ -n "$image" ]] && [[ -n "${LOCAL_REGISTRY:-}" ]]; then
|
|
RUNNER_RESOLVED_IMAGE="${LOCAL_REGISTRY}/${image}"
|
|
else
|
|
RUNNER_RESOLVED_IMAGE="${image}"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build act_runner label specs from labels + default_image.
|
|
# Docker: "linux:docker://image" | Native: "macos:host"
|
|
# Sets RUNNER_LABELS_CSV and RUNNER_LABELS_YAML.
|
|
# ---------------------------------------------------------------------------
|
|
build_runner_labels() {
|
|
resolve_runner_image
|
|
|
|
local labels_csv=""
|
|
local labels_yaml=""
|
|
local IFS=','
|
|
# shellcheck disable=SC2206
|
|
local -a parts=($RUNNER_LABELS)
|
|
|
|
local label spec
|
|
for label in "${parts[@]}"; do
|
|
label=$(echo "$label" | xargs)
|
|
[[ -z "$label" ]] && continue
|
|
|
|
if [[ "$label" == *:* ]]; then
|
|
# Already a full spec (e.g. linux:docker://node:20)
|
|
spec="$label"
|
|
elif [[ "$RUNNER_TYPE" == "docker" ]]; then
|
|
spec="${label}:docker://${RUNNER_RESOLVED_IMAGE}"
|
|
else
|
|
spec="${label}:host"
|
|
fi
|
|
|
|
if [[ -z "$labels_csv" ]]; then
|
|
labels_csv="$spec"
|
|
else
|
|
labels_csv="${labels_csv},${spec}"
|
|
fi
|
|
# shellcheck disable=SC2089 # intentional — value rendered via envsubst, not shell expansion
|
|
labels_yaml="${labels_yaml} - \"${spec}\"
|
|
"
|
|
done
|
|
|
|
export RUNNER_LABELS_CSV="$labels_csv"
|
|
# shellcheck disable=SC2090 # intentional — value rendered via envsubst, not shell expansion
|
|
export RUNNER_LABELS_YAML="$labels_yaml"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Resolve registration token based on repos field.
|
|
# repos=all → instance-level token from .env
|
|
# repos=<name> → fetch repo-level token from Gitea API
|
|
# Sets RUNNER_REG_TOKEN.
|
|
# ---------------------------------------------------------------------------
|
|
resolve_registration_token() {
|
|
if [[ "$RUNNER_REPOS" == "all" ]] || [[ -z "$RUNNER_REPOS" ]]; then
|
|
RUNNER_REG_TOKEN="${GITEA_RUNNER_REGISTRATION_TOKEN:-}"
|
|
else
|
|
# Fetch repo-level registration token from Gitea API
|
|
local owner="${GITEA_ORG_NAME:-}"
|
|
if [[ -z "$owner" ]]; then
|
|
log_error "GITEA_ORG_NAME is empty — cannot fetch repo-level token"
|
|
return 1
|
|
fi
|
|
log_info "Fetching registration token for repo '${owner}/${RUNNER_REPOS}'..."
|
|
local response
|
|
response=$(gitea_api GET "/repos/${owner}/${RUNNER_REPOS}/actions/runners/registration-token" 2>/dev/null) || {
|
|
log_error "Failed to fetch registration token for repo '${RUNNER_REPOS}'"
|
|
return 1
|
|
}
|
|
RUNNER_REG_TOKEN=$(printf '%s' "$response" | jq -r '.token // empty' 2>/dev/null)
|
|
if [[ -z "$RUNNER_REG_TOKEN" ]]; then
|
|
log_error "Empty registration token returned for repo '${RUNNER_REPOS}'"
|
|
return 1
|
|
fi
|
|
fi
|
|
export RUNNER_REG_TOKEN
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build Docker deploy.resources block (only when cpu/memory are set).
|
|
# Sets RUNNER_DEPLOY_RESOURCES.
|
|
# ---------------------------------------------------------------------------
|
|
build_deploy_resources() {
|
|
if [[ -n "$RUNNER_CPU" ]] || [[ -n "$RUNNER_MEMORY" ]]; then
|
|
local block=" deploy:\n resources:\n limits:"
|
|
if [[ -n "$RUNNER_CPU" ]]; then
|
|
block="${block}\n cpus: \"${RUNNER_CPU}\""
|
|
fi
|
|
if [[ -n "$RUNNER_MEMORY" ]]; then
|
|
block="${block}\n memory: \"${RUNNER_MEMORY}\""
|
|
fi
|
|
RUNNER_DEPLOY_RESOURCES=$(printf '%b' "$block")
|
|
else
|
|
RUNNER_DEPLOY_RESOURCES=""
|
|
fi
|
|
export RUNNER_DEPLOY_RESOURCES
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Execute a command on the runner's host.
|
|
# For "local" hosts (macOS), runs the command directly.
|
|
# For remote hosts, SSHs into them using resolved SSH credentials.
|
|
# ---------------------------------------------------------------------------
|
|
runner_ssh() {
|
|
local cmd="$*"
|
|
|
|
if [[ "$RUNNER_SSH_HOST" == "local" ]]; then
|
|
eval "$cmd"
|
|
else
|
|
# shellcheck disable=SC2086
|
|
ssh ${RUNNER_SSH_KEY:+-i "$RUNNER_SSH_KEY"} \
|
|
-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
|
|
# shellcheck disable=SC2086
|
|
scp ${RUNNER_SSH_KEY:+-i "$RUNNER_SSH_KEY"} \
|
|
-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
|
|
# ---------------------------------------------------------------------------
|
|
add_docker_runner() {
|
|
log_info "Deploying Docker runner '${RUNNER_NAME}' on ${RUNNER_HOST} (${RUNNER_SSH_HOST})..."
|
|
|
|
build_runner_labels
|
|
resolve_registration_token
|
|
build_deploy_resources
|
|
|
|
# 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
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
export RUNNER_NAME RUNNER_DATA_PATH RUNNER_LABELS_CSV RUNNER_REG_TOKEN RUNNER_DEPLOY_RESOURCES
|
|
render_template "${SCRIPT_DIR}/templates/docker-compose-runner.yml.tpl" "$tmpfile" \
|
|
"\${ACT_RUNNER_VERSION} \${RUNNER_NAME} \${GITEA_INTERNAL_URL} \${RUNNER_REG_TOKEN} \${RUNNER_LABELS_CSV} \${RUNNER_DATA_PATH} \${RUNNER_DEPLOY_RESOURCES}"
|
|
runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/docker-compose.yml"
|
|
rm -f "$tmpfile"
|
|
|
|
# Render runner config
|
|
tmpfile=$(mktemp)
|
|
# shellcheck disable=SC2090 # intentional — RUNNER_LABELS_YAML rendered via envsubst
|
|
export RUNNER_LABELS_YAML
|
|
export RUNNER_CAPACITY
|
|
render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" \
|
|
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY}"
|
|
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_HOST} (${RUNNER_SSH_HOST})"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# add_native_runner — Deploy a runner as a native binary on macOS
|
|
# ---------------------------------------------------------------------------
|
|
add_native_runner() {
|
|
require_local_os "Darwin" "Native runner deployment requires macOS (uses launchctl)"
|
|
|
|
log_info "Deploying native runner '${RUNNER_NAME}' on local machine..."
|
|
|
|
build_runner_labels
|
|
resolve_registration_token
|
|
|
|
# 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"
|
|
|
|
# Route plist to LaunchDaemons (boot) or LaunchAgents (login) based on boot flag.
|
|
# LaunchDaemons start at boot before any user logs in — useful for headless Macs.
|
|
# LaunchAgents start when the user logs in — no elevated privileges needed.
|
|
local plist_dir
|
|
if [[ "$RUNNER_BOOT" == "true" ]]; then
|
|
plist_dir="/Library/LaunchDaemons"
|
|
else
|
|
plist_dir="$HOME/Library/LaunchAgents"
|
|
fi
|
|
local plist_path="${plist_dir}/${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
|
|
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
|
|
|
|
# Render runner config (must exist before registration — act_runner reads it)
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
# shellcheck disable=SC2090 # intentional — RUNNER_LABELS_YAML rendered via envsubst
|
|
export RUNNER_NAME RUNNER_DATA_PATH RUNNER_LABELS_YAML RUNNER_CAPACITY
|
|
render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" \
|
|
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY}"
|
|
cp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
|
rm -f "$tmpfile"
|
|
|
|
# Register the runner with Gitea
|
|
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 "${RUNNER_REG_TOKEN}" \
|
|
--name "${RUNNER_NAME}" \
|
|
--labels "${RUNNER_LABELS_CSV}" \
|
|
--config "${RUNNER_DATA_PATH}/config.yaml"
|
|
log_success "Runner registered"
|
|
fi
|
|
|
|
# Render launchd plist.
|
|
# When boot=true, insert a <key>UserName</key> entry so the daemon runs as
|
|
# the deploying user instead of root (LaunchDaemons default to root).
|
|
# When boot=false (LaunchAgent), the block is empty — agents run as the user.
|
|
if [[ "$RUNNER_BOOT" == "true" ]]; then
|
|
RUNNER_PLIST_USERNAME_BLOCK="$(printf ' <key>UserName</key>\n <string>%s</string>\n' "$(whoami)")"
|
|
else
|
|
RUNNER_PLIST_USERNAME_BLOCK=""
|
|
fi
|
|
export RUNNER_PLIST_USERNAME_BLOCK
|
|
|
|
tmpfile=$(mktemp)
|
|
render_template "${SCRIPT_DIR}/templates/com.gitea.runner.plist.tpl" "$tmpfile" \
|
|
"\${RUNNER_NAME} \${RUNNER_DATA_PATH} \${RUNNER_PLIST_USERNAME_BLOCK}"
|
|
mkdir -p "$plist_dir"
|
|
|
|
# LaunchDaemons lives in a system directory — requires sudo to write.
|
|
if [[ "$RUNNER_BOOT" == "true" ]]; then
|
|
sudo cp "$tmpfile" "$plist_path"
|
|
else
|
|
cp "$tmpfile" "$plist_path"
|
|
fi
|
|
rm -f "$tmpfile"
|
|
|
|
# Install newsyslog config for log rotation (daily, 5 archives, 50 MB max each).
|
|
# newsyslog is macOS's built-in log rotator — configs in /etc/newsyslog.d/ are
|
|
# picked up automatically. Requires sudo for /etc/newsyslog.d/ write access.
|
|
local newsyslog_conf="/etc/newsyslog.d/com.gitea.runner.${RUNNER_NAME}.conf"
|
|
if [[ ! -f "$newsyslog_conf" ]]; then
|
|
tmpfile=$(mktemp)
|
|
render_template "${SCRIPT_DIR}/templates/com.gitea.runner.newsyslog.conf.tpl" "$tmpfile" \
|
|
"\${RUNNER_NAME} \${RUNNER_DATA_PATH}"
|
|
sudo cp "$tmpfile" "$newsyslog_conf"
|
|
rm -f "$tmpfile"
|
|
log_success "Log rotation installed: $newsyslog_conf"
|
|
fi
|
|
|
|
# Load the launchd service.
|
|
# LaunchDaemons require sudo to load/unload; LaunchAgents do not.
|
|
if [[ "$RUNNER_BOOT" == "true" ]]; then
|
|
sudo launchctl load "$plist_path"
|
|
log_success "Native runner '${RUNNER_NAME}' loaded via launchd (boot daemon — starts at boot)"
|
|
else
|
|
launchctl load "$plist_path"
|
|
log_success "Native runner '${RUNNER_NAME}' loaded via launchd (login agent — starts at login)"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# remove_docker_runner — Stop + remove Docker runner container
|
|
# ---------------------------------------------------------------------------
|
|
remove_docker_runner() {
|
|
log_info "Removing Docker runner '${RUNNER_NAME}' from ${RUNNER_HOST} (${RUNNER_SSH_HOST})..."
|
|
|
|
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
|
|
# Remove docker-compose.yml — it contains the registration token in plaintext
|
|
runner_ssh "rm -f '${RUNNER_DATA_PATH}/docker-compose.yml'" 2>/dev/null || true
|
|
log_success "Docker runner '${RUNNER_NAME}' stopped and compose file removed"
|
|
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() {
|
|
require_local_os "Darwin" "Native runner removal requires macOS (uses launchctl)"
|
|
|
|
log_info "Removing native runner '${RUNNER_NAME}' from local machine..."
|
|
|
|
RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}"
|
|
|
|
local plist_name="com.gitea.runner.${RUNNER_NAME}.plist"
|
|
|
|
# The plist may live in either LaunchDaemons (boot=true) or LaunchAgents (boot=false).
|
|
# Check both directories so removal works regardless of how the runner was originally
|
|
# deployed — the runner.conf boot flag may have changed since deployment.
|
|
local plist_path=""
|
|
local needs_sudo=false
|
|
if [[ -f "/Library/LaunchDaemons/${plist_name}" ]]; then
|
|
plist_path="/Library/LaunchDaemons/${plist_name}"
|
|
needs_sudo=true
|
|
elif [[ -f "$HOME/Library/LaunchAgents/${plist_name}" ]]; then
|
|
plist_path="$HOME/Library/LaunchAgents/${plist_name}"
|
|
fi
|
|
|
|
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${RUNNER_NAME}"; then
|
|
if [[ -n "$plist_path" ]]; then
|
|
if $needs_sudo; then
|
|
sudo launchctl unload "$plist_path" 2>/dev/null || true
|
|
else
|
|
launchctl unload "$plist_path" 2>/dev/null || true
|
|
fi
|
|
log_success "Launchd service unloaded"
|
|
else
|
|
# Plist file is gone but launchctl still shows the service — force-remove by label.
|
|
launchctl remove "com.gitea.runner.${RUNNER_NAME}" 2>/dev/null || \
|
|
sudo launchctl remove "com.gitea.runner.${RUNNER_NAME}" 2>/dev/null || true
|
|
log_warn "Plist not found on disk — removed service by label"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$plist_path" ]] && [[ -f "$plist_path" ]]; then
|
|
if $needs_sudo; then
|
|
sudo rm -f "$plist_path"
|
|
else
|
|
rm -f "$plist_path"
|
|
fi
|
|
log_success "Plist removed: $plist_path"
|
|
fi
|
|
|
|
# Remove newsyslog rotation config
|
|
local newsyslog_conf="/etc/newsyslog.d/com.gitea.runner.${RUNNER_NAME}.conf"
|
|
if [[ -f "$newsyslog_conf" ]]; then
|
|
sudo rm -f "$newsyslog_conf"
|
|
log_success "Log rotation config removed: $newsyslog_conf"
|
|
fi
|
|
|
|
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
|
|
# ---------------------------------------------------------------------------
|
|
list_runners() {
|
|
log_info "Listing runners..."
|
|
|
|
if [[ ! -f "$RUNNERS_CONF" ]]; then
|
|
log_error "runners.conf not found at $RUNNERS_CONF"
|
|
return 1
|
|
fi
|
|
|
|
local api_runners
|
|
api_runners=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]")
|
|
|
|
printf '%-20s %-10s %-10s %-8s %-6s %-10s\n' "NAME" "HOST" "LABELS" "TYPE" "CAP" "STATUS"
|
|
printf '%-20s %-10s %-10s %-8s %-6s %-10s\n' "----" "----" "------" "----" "---" "------"
|
|
|
|
local name host labels runner_type capacity status
|
|
while IFS= read -r name; do
|
|
host=$(ini_get "$RUNNERS_CONF" "$name" "host" "")
|
|
labels=$(ini_get "$RUNNERS_CONF" "$name" "labels" "")
|
|
runner_type=$(ini_get "$RUNNERS_CONF" "$name" "type" "")
|
|
capacity=$(ini_get "$RUNNERS_CONF" "$name" "capacity" "1")
|
|
|
|
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 %-10s %-10s %-8s %-6s %-10s\n' "$name" "$host" "$labels" "$runner_type" "$capacity" "$status"
|
|
done < <(ini_list_sections "$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
|
|
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
|