feat: rework runner config to INI format with full field support
Replace pipe-delimited runners.conf with INI-style sections supporting host resolution, container images, repo-scoped tokens, resource limits, capacity, and SSH key passthrough. All defaults pulled from .env. - Add INI parsing helpers (ini_list_sections, ini_get, ini_set) to common.sh - Add SSH key support (UNRAID_SSH_KEY, FEDORA_SSH_KEY) to ssh_exec/scp_to - Add .env vars: RUNNER_DEFAULT_IMAGE, RUNNER_DEFAULT_CAPACITY, RUNNER_DEFAULT_DATA_PATH, LOCAL_RUNNER_DATA_PATH, LOCAL_REGISTRY - Rewrite manage_runner.sh with host/image/token resolution and resource limits - Rewrite configure_runners.sh wizard for INI format with all 9 fields - Update phase3 scripts to use ini_list_sections instead of pipe parsing - Add runners.conf INI validation to preflight.sh (check 5b) - Update templates to use resolved labels, capacity, and deploy resources Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ UNRAID_SSH_PORT=22 # SSH port (default 22)
|
|||||||
UNRAID_GITEA_PORT=3000 # Port Gitea web UI will listen on
|
UNRAID_GITEA_PORT=3000 # Port Gitea web UI will listen on
|
||||||
UNRAID_GITEA_SSH_PORT=2222 # Port for git-over-SSH (host 22 is taken by SSH server)
|
UNRAID_GITEA_SSH_PORT=2222 # Port for git-over-SSH (host 22 is taken by SSH server)
|
||||||
UNRAID_GITEA_DATA_PATH= # Absolute path on NVMe for Gitea data (e.g. /mnt/nvme/gitea)
|
UNRAID_GITEA_DATA_PATH= # Absolute path on NVMe for Gitea data (e.g. /mnt/nvme/gitea)
|
||||||
|
UNRAID_SSH_KEY= # Path to SSH private key (optional, uses ssh-agent default if empty)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -25,6 +26,7 @@ FEDORA_SSH_PORT=22 # SSH port (default 22)
|
|||||||
FEDORA_GITEA_PORT=3000 # Port Gitea web UI will listen on
|
FEDORA_GITEA_PORT=3000 # Port Gitea web UI will listen on
|
||||||
FEDORA_GITEA_SSH_PORT=2222 # Port for git-over-SSH (host 22 is taken by SSH server)
|
FEDORA_GITEA_SSH_PORT=2222 # Port for git-over-SSH (host 22 is taken by SSH server)
|
||||||
FEDORA_GITEA_DATA_PATH= # Absolute path on NVMe for Gitea data (e.g. /mnt/nvme/gitea)
|
FEDORA_GITEA_DATA_PATH= # Absolute path on NVMe for Gitea data (e.g. /mnt/nvme/gitea)
|
||||||
|
FEDORA_SSH_KEY= # Path to SSH private key (optional, uses ssh-agent default if empty)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -62,9 +64,14 @@ GITEA_BACKUP_ADMIN_TOKEN= # API token for backup instance — do not fil
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# RUNNERS
|
# RUNNERS
|
||||||
# Runner definitions live in runners.conf (see runners.conf.example)
|
# Runner definitions live in runners.conf (INI format, see runners.conf.example)
|
||||||
# Use manage_runner.sh to add/remove runners at any time
|
# Use manage_runner.sh to add/remove runners at any time
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
RUNNER_DEFAULT_IMAGE=catthehacker/ubuntu:act-latest # Default container image for docker runners
|
||||||
|
RUNNER_DEFAULT_CAPACITY=1 # Default max concurrent jobs per runner (positive integer)
|
||||||
|
RUNNER_DEFAULT_DATA_PATH=/mnt/nvme/gitea-runner # Default data path for remote (docker) runners
|
||||||
|
LOCAL_RUNNER_DATA_PATH=~/gitea-runner # Data path for native macOS runner
|
||||||
|
LOCAL_REGISTRY= # Local registry prefix (e.g. registry.local:5000), empty = Docker Hub
|
||||||
# AUTO-POPULATED by phase1 scripts — do not fill manually:
|
# AUTO-POPULATED by phase1 scripts — do not fill manually:
|
||||||
GITEA_RUNNER_REGISTRATION_TOKEN= # Retrieved from Gitea admin panel via API
|
GITEA_RUNNER_REGISTRATION_TOKEN= # Retrieved from Gitea admin panel via API
|
||||||
|
|
||||||
|
|||||||
184
lib/common.sh
184
lib/common.sh
@@ -168,6 +168,19 @@ validate_ssl_mode() {
|
|||||||
[[ "$1" == "letsencrypt" ]] || [[ "$1" == "existing" ]]
|
[[ "$1" == "letsencrypt" ]] || [[ "$1" == "existing" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_positive_integer() {
|
||||||
|
[[ "$1" =~ ^[1-9][0-9]*$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_optional() {
|
||||||
|
return 0 # always valid — value can be empty or anything
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_optional_path() {
|
||||||
|
# shellcheck disable=SC2088 # tilde intentionally stored as literal string
|
||||||
|
[[ -z "$1" ]] || [[ "$1" == /* ]] || [[ "$1" == "~/"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# .env format validation — validate_env()
|
# .env format validation — validate_env()
|
||||||
# Checks every .env variable against its expected type.
|
# Checks every .env variable against its expected type.
|
||||||
@@ -187,6 +200,7 @@ _ENV_VAR_NAMES=(
|
|||||||
GITEA_DOMAIN GITEA_INTERNAL_URL
|
GITEA_DOMAIN GITEA_INTERNAL_URL
|
||||||
GITEA_BACKUP_INTERNAL_URL GITEA_BACKUP_MIRROR_INTERVAL
|
GITEA_BACKUP_INTERNAL_URL GITEA_BACKUP_MIRROR_INTERVAL
|
||||||
BACKUP_STORAGE_PATH BACKUP_RETENTION_COUNT
|
BACKUP_STORAGE_PATH BACKUP_RETENTION_COUNT
|
||||||
|
RUNNER_DEFAULT_IMAGE RUNNER_DEFAULT_CAPACITY RUNNER_DEFAULT_DATA_PATH LOCAL_RUNNER_DATA_PATH
|
||||||
GITHUB_USERNAME GITHUB_TOKEN
|
GITHUB_USERNAME GITHUB_TOKEN
|
||||||
REPO_1_NAME REPO_2_NAME REPO_3_NAME
|
REPO_1_NAME REPO_2_NAME REPO_3_NAME
|
||||||
MIGRATE_ISSUES MIGRATE_LABELS MIGRATE_MILESTONES MIGRATE_WIKI
|
MIGRATE_ISSUES MIGRATE_LABELS MIGRATE_MILESTONES MIGRATE_WIKI
|
||||||
@@ -206,6 +220,7 @@ _ENV_VAR_TYPES=(
|
|||||||
nonempty url
|
nonempty url
|
||||||
url nonempty
|
url nonempty
|
||||||
path integer
|
path integer
|
||||||
|
nonempty positive_integer nonempty nonempty
|
||||||
nonempty nonempty
|
nonempty nonempty
|
||||||
nonempty nonempty nonempty
|
nonempty nonempty nonempty
|
||||||
bool bool bool bool
|
bool bool bool bool
|
||||||
@@ -220,6 +235,10 @@ _ENV_CONDITIONAL_NAMES=(SSL_EMAIL SSL_CERT_PATH SSL_KEY_PATH)
|
|||||||
_ENV_CONDITIONAL_TYPES=(email path path)
|
_ENV_CONDITIONAL_TYPES=(email path path)
|
||||||
_ENV_CONDITIONAL_WHEN=( letsencrypt existing existing)
|
_ENV_CONDITIONAL_WHEN=( letsencrypt existing existing)
|
||||||
|
|
||||||
|
# Optional variables — validated only when non-empty (never required).
|
||||||
|
_ENV_OPTIONAL_NAMES=(UNRAID_SSH_KEY FEDORA_SSH_KEY LOCAL_REGISTRY)
|
||||||
|
_ENV_OPTIONAL_TYPES=(optional_path optional_path nonempty)
|
||||||
|
|
||||||
# Human-readable format hints for error messages.
|
# Human-readable format hints for error messages.
|
||||||
_validator_hint() {
|
_validator_hint() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -227,12 +246,15 @@ _validator_hint() {
|
|||||||
port) echo "expected: 1-65535" ;;
|
port) echo "expected: 1-65535" ;;
|
||||||
email) echo "must contain @" ;;
|
email) echo "must contain @" ;;
|
||||||
path) echo "must start with /" ;;
|
path) echo "must start with /" ;;
|
||||||
|
optional_path) echo "must start with / or ~/ (or be empty)" ;;
|
||||||
url) echo "must start with http:// or https://" ;;
|
url) echo "must start with http:// or https://" ;;
|
||||||
bool) echo "must be true or false" ;;
|
bool) echo "must be true or false" ;;
|
||||||
integer) echo "must be a number" ;;
|
integer) echo "must be a number" ;;
|
||||||
|
positive_integer) echo "must be a positive integer (>= 1)" ;;
|
||||||
nonempty) echo "cannot be empty" ;;
|
nonempty) echo "cannot be empty" ;;
|
||||||
password) echo "must be at least 8 characters" ;;
|
password) echo "must be at least 8 characters" ;;
|
||||||
ssl_mode) echo "must be letsencrypt or existing" ;;
|
ssl_mode) echo "must be letsencrypt or existing" ;;
|
||||||
|
optional) echo "any value or empty" ;;
|
||||||
*) echo "invalid" ;;
|
*) echo "invalid" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -277,6 +299,21 @@ validate_env() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Validate optional variables (only when non-empty)
|
||||||
|
for ((i = 0; i < ${#_ENV_OPTIONAL_NAMES[@]}; i++)); do
|
||||||
|
var_name="${_ENV_OPTIONAL_NAMES[$i]}"
|
||||||
|
var_type="${_ENV_OPTIONAL_TYPES[$i]}"
|
||||||
|
value="${!var_name:-}"
|
||||||
|
|
||||||
|
# Skip if empty — these are optional
|
||||||
|
[[ -z "$value" ]] && continue
|
||||||
|
|
||||||
|
if ! "validate_${var_type}" "$value"; then
|
||||||
|
log_error " → $var_name='$value' ($(_validator_hint "$var_type"))"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
if [[ $errors -gt 0 ]]; then
|
if [[ $errors -gt 0 ]]; then
|
||||||
log_error "$errors .env variable(s) failed format validation"
|
log_error "$errors .env variable(s) failed format validation"
|
||||||
return 1
|
return 1
|
||||||
@@ -349,10 +386,12 @@ ssh_exec() {
|
|||||||
local ip_var="${host_key}_IP"
|
local ip_var="${host_key}_IP"
|
||||||
local user_var="${host_key}_SSH_USER"
|
local user_var="${host_key}_SSH_USER"
|
||||||
local port_var="${host_key}_SSH_PORT"
|
local port_var="${host_key}_SSH_PORT"
|
||||||
|
local key_var="${host_key}_SSH_KEY"
|
||||||
|
|
||||||
local ip="${!ip_var:-}"
|
local ip="${!ip_var:-}"
|
||||||
local user="${!user_var:-}"
|
local user="${!user_var:-}"
|
||||||
local port="${!port_var:-22}"
|
local port="${!port_var:-22}"
|
||||||
|
local key="${!key_var:-}"
|
||||||
|
|
||||||
if [[ -z "$ip" ]] || [[ -z "$user" ]]; then
|
if [[ -z "$ip" ]] || [[ -z "$user" ]]; then
|
||||||
log_error "SSH config incomplete for $host_key: ${ip_var}=${ip:-<empty>}, ${user_var}=${user:-<empty>}"
|
log_error "SSH config incomplete for $host_key: ${ip_var}=${ip:-<empty>}, ${user_var}=${user:-<empty>}"
|
||||||
@@ -362,7 +401,9 @@ ssh_exec() {
|
|||||||
# ConnectTimeout: fail fast if host is unreachable (don't hang for 60s)
|
# ConnectTimeout: fail fast if host is unreachable (don't hang for 60s)
|
||||||
# StrictHostKeyChecking=accept-new: auto-accept new hosts but reject changed keys
|
# StrictHostKeyChecking=accept-new: auto-accept new hosts but reject changed keys
|
||||||
# BatchMode=yes: never prompt for password (fail if key auth doesn't work)
|
# BatchMode=yes: never prompt for password (fail if key auth doesn't work)
|
||||||
ssh -o ConnectTimeout=10 \
|
# ${key:+-i "$key"}: pass -i only when SSH_KEY is set (otherwise use ssh-agent default)
|
||||||
|
ssh ${key:+-i "$key"} \
|
||||||
|
-o ConnectTimeout=10 \
|
||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
-o BatchMode=yes \
|
-o BatchMode=yes \
|
||||||
-p "$port" \
|
-p "$port" \
|
||||||
@@ -381,17 +422,20 @@ scp_to() {
|
|||||||
local ip_var="${host_key}_IP"
|
local ip_var="${host_key}_IP"
|
||||||
local user_var="${host_key}_SSH_USER"
|
local user_var="${host_key}_SSH_USER"
|
||||||
local port_var="${host_key}_SSH_PORT"
|
local port_var="${host_key}_SSH_PORT"
|
||||||
|
local key_var="${host_key}_SSH_KEY"
|
||||||
|
|
||||||
local ip="${!ip_var:-}"
|
local ip="${!ip_var:-}"
|
||||||
local user="${!user_var:-}"
|
local user="${!user_var:-}"
|
||||||
local port="${!port_var:-22}"
|
local port="${!port_var:-22}"
|
||||||
|
local key="${!key_var:-}"
|
||||||
|
|
||||||
if [[ -z "$ip" ]] || [[ -z "$user" ]]; then
|
if [[ -z "$ip" ]] || [[ -z "$user" ]]; then
|
||||||
log_error "SCP config incomplete for $host_key"
|
log_error "SCP config incomplete for $host_key"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
scp -o ConnectTimeout=10 \
|
scp ${key:+-i "$key"} \
|
||||||
|
-o ConnectTimeout=10 \
|
||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
-o BatchMode=yes \
|
-o BatchMode=yes \
|
||||||
-P "$port" \
|
-P "$port" \
|
||||||
@@ -612,6 +656,142 @@ check_remote_min_version() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# INI file parsing — used by runners.conf (INI-style sections)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# List all [section] names from an INI file.
|
||||||
|
# Usage: ini_list_sections "runners.conf"
|
||||||
|
# Outputs one section name per line (without brackets).
|
||||||
|
ini_list_sections() {
|
||||||
|
local file="$1"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sed -n 's/^[[:space:]]*\[\([^]]*\)\].*/\1/p' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get a value from an INI file by section and key.
|
||||||
|
# Usage: ini_get "runners.conf" "unraid-runner" "host" "default_value"
|
||||||
|
# Returns the value (stripped of inline comments and whitespace), or
|
||||||
|
# the default if key not found. Exit code 1 if section not found.
|
||||||
|
ini_get() {
|
||||||
|
local file="$1" section="$2" key="$3" default="${4:-}"
|
||||||
|
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
printf '%s' "$default"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local in_section=false
|
||||||
|
local line k v
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Strip leading/trailing whitespace
|
||||||
|
line="${line#"${line%%[![:space:]]*}"}"
|
||||||
|
line="${line%"${line##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
[[ "$line" == \#* ]] && continue
|
||||||
|
|
||||||
|
# Check for section header
|
||||||
|
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
|
||||||
|
if [[ "${BASH_REMATCH[1]}" == "$section" ]]; then
|
||||||
|
in_section=true
|
||||||
|
elif $in_section; then
|
||||||
|
# Moved past our section without finding the key
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse key = value lines within our section
|
||||||
|
if $in_section && [[ "$line" =~ ^([^=]+)=(.*) ]]; then
|
||||||
|
k="${BASH_REMATCH[1]}"
|
||||||
|
v="${BASH_REMATCH[2]}"
|
||||||
|
# Trim whitespace from key and value
|
||||||
|
k="${k#"${k%%[![:space:]]*}"}"
|
||||||
|
k="${k%"${k##*[![:space:]]}"}"
|
||||||
|
v="${v#"${v%%[![:space:]]*}"}"
|
||||||
|
v="${v%"${v##*[![:space:]]}"}"
|
||||||
|
# Strip inline comment (# preceded by whitespace)
|
||||||
|
v="${v%%[[:space:]]#*}"
|
||||||
|
# Trim trailing whitespace again after comment strip
|
||||||
|
v="${v%"${v##*[![:space:]]}"}"
|
||||||
|
|
||||||
|
if [[ "$k" == "$key" ]]; then
|
||||||
|
printf '%s' "$v"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$file"
|
||||||
|
|
||||||
|
printf '%s' "$default"
|
||||||
|
if $in_section; then
|
||||||
|
return 0 # section found, key missing — return default
|
||||||
|
fi
|
||||||
|
return 1 # section not found
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set a value in an INI file (create section/key if missing).
|
||||||
|
# Usage: ini_set "runners.conf" "unraid-runner" "host" "unraid"
|
||||||
|
ini_set() {
|
||||||
|
local file="$1" section="$2" key="$3" value="$4"
|
||||||
|
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
# Create file with section and key
|
||||||
|
printf '[%s]\n%s = %s\n' "$section" "$key" "$value" > "$file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmpfile
|
||||||
|
tmpfile=$(mktemp)
|
||||||
|
local in_section=false
|
||||||
|
local key_written=false
|
||||||
|
local section_exists=false
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Check for section header
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*\[([^]]+)\] ]]; then
|
||||||
|
local found_section="${BASH_REMATCH[1]}"
|
||||||
|
if $in_section && ! $key_written; then
|
||||||
|
# Leaving our section without writing key — append it
|
||||||
|
printf '%s = %s\n' "$key" "$value" >> "$tmpfile"
|
||||||
|
key_written=true
|
||||||
|
fi
|
||||||
|
if [[ "$found_section" == "$section" ]]; then
|
||||||
|
in_section=true
|
||||||
|
section_exists=true
|
||||||
|
else
|
||||||
|
in_section=false
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$line" >> "$tmpfile"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Replace existing key within our section
|
||||||
|
if $in_section && ! $key_written; then
|
||||||
|
local stripped="${line#"${line%%[![:space:]]*}"}"
|
||||||
|
if [[ "$stripped" =~ ^${key}[[:space:]]*= ]]; then
|
||||||
|
printf '%s = %s\n' "$key" "$value" >> "$tmpfile"
|
||||||
|
key_written=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$line" >> "$tmpfile"
|
||||||
|
done < "$file"
|
||||||
|
|
||||||
|
# Handle end-of-file cases
|
||||||
|
if $in_section && ! $key_written; then
|
||||||
|
printf '%s = %s\n' "$key" "$value" >> "$tmpfile"
|
||||||
|
elif ! $section_exists; then
|
||||||
|
printf '\n[%s]\n%s = %s\n' "$section" "$key" "$value" >> "$tmpfile"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$tmpfile" "$file"
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Install manifest — tracks what each setup script installs for rollback
|
# Install manifest — tracks what each setup script installs for rollback
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
331
manage_runner.sh
331
manage_runner.sh
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# manage_runner.sh — Add, remove, or list Gitea Actions runners
|
# manage_runner.sh — Add, remove, or list Gitea Actions runners
|
||||||
# Reads runner definitions from runners.conf (pipe-delimited format).
|
# Reads runner definitions from runners.conf (INI format).
|
||||||
# Supports two runner types:
|
# Supports two runner types:
|
||||||
# docker — Linux hosts, deployed as Docker container via docker-compose
|
# docker — Linux hosts, deployed as Docker container via docker-compose
|
||||||
# native — macOS, deployed as binary + launchd service
|
# native — macOS, deployed as binary + launchd service
|
||||||
@@ -31,16 +31,19 @@ Commands:
|
|||||||
list Show all runners with status
|
list Show all runners with status
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Runner name as defined in runners.conf
|
--name <name> Runner name (= section name in runners.conf)
|
||||||
--help Show this help
|
--help Show this help
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Parse a runner entry from runners.conf by name
|
# Parse a runner entry from runners.conf (INI format) by section name.
|
||||||
# Sets: RUNNER_NAME, RUNNER_SSH_HOST, RUNNER_SSH_USER, RUNNER_SSH_PORT,
|
# Sets globals: RUNNER_NAME, RUNNER_HOST, RUNNER_TYPE, RUNNER_DATA_PATH,
|
||||||
# RUNNER_DATA_PATH, RUNNER_LABELS, RUNNER_TYPE
|
# RUNNER_LABELS, RUNNER_DEFAULT_IMAGE, RUNNER_REPOS, RUNNER_CAPACITY,
|
||||||
|
# RUNNER_CPU, RUNNER_MEMORY
|
||||||
|
# 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.
|
# Returns 1 if not found.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
parse_runner_entry() {
|
parse_runner_entry() {
|
||||||
@@ -51,63 +54,205 @@ parse_runner_entry() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while IFS='|' read -r name host user port path labels type; do
|
# Check section exists
|
||||||
# Skip comments and blank lines
|
if ! ini_list_sections "$RUNNERS_CONF" | grep -qx "$target_name"; then
|
||||||
[[ "$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"
|
log_error "Runner '$target_name' not found in runners.conf"
|
||||||
return 1
|
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_DEFAULT_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" "${RUNNER_DEFAULT_CAPACITY:-1}")
|
||||||
|
RUNNER_CPU=$(ini_get "$RUNNERS_CONF" "$target_name" "cpu" "")
|
||||||
|
RUNNER_MEMORY=$(ini_get "$RUNNERS_CONF" "$target_name" "memory" "")
|
||||||
|
|
||||||
|
# --- 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 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
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# List all runner entries from runners.conf (without looking up a name)
|
# Resolve the runner's image (with LOCAL_REGISTRY prefix if set).
|
||||||
# Outputs lines to stdout: name|host|user|port|path|labels|type
|
# Sets RUNNER_RESOLVED_IMAGE.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
all_runner_entries() {
|
resolve_runner_image() {
|
||||||
if [[ ! -f "$RUNNERS_CONF" ]]; then
|
local image="${RUNNER_DEFAULT_IMAGE:-${RUNNER_DEFAULT_IMAGE_ENV:-}}"
|
||||||
log_error "runners.conf not found at $RUNNERS_CONF"
|
if [[ -z "$image" ]] && [[ "$RUNNER_TYPE" == "docker" ]]; then
|
||||||
return 1
|
image="${RUNNER_DEFAULT_IMAGE:-catthehacker/ubuntu:act-latest}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while IFS= read -r line; do
|
if [[ -n "$image" ]] && [[ -n "${LOCAL_REGISTRY:-}" ]]; then
|
||||||
# Skip comments and blank lines
|
RUNNER_RESOLVED_IMAGE="${LOCAL_REGISTRY}/${image}"
|
||||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
else
|
||||||
[[ -z "$line" ]] && continue
|
RUNNER_RESOLVED_IMAGE="${image}"
|
||||||
echo "$line"
|
fi
|
||||||
done < "$RUNNERS_CONF"
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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.
|
# Execute a command on the runner's host.
|
||||||
# For "local" hosts (macOS), runs the command directly.
|
# For "local" hosts (macOS), runs the command directly.
|
||||||
# For remote hosts, SSHs into them — uses direct ssh (not ssh_exec from
|
# For remote hosts, SSHs into them using resolved SSH credentials.
|
||||||
# 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() {
|
runner_ssh() {
|
||||||
local cmd="$*"
|
local cmd="$*"
|
||||||
|
|
||||||
if [[ "$RUNNER_SSH_HOST" == "local" ]]; then
|
if [[ "$RUNNER_SSH_HOST" == "local" ]]; then
|
||||||
# macOS runner — execute directly on this machine
|
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
else
|
else
|
||||||
ssh -o ConnectTimeout=10 \
|
# shellcheck disable=SC2086
|
||||||
|
ssh ${RUNNER_SSH_KEY:+-i "$RUNNER_SSH_KEY"} \
|
||||||
|
-o ConnectTimeout=10 \
|
||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
-o BatchMode=yes \
|
-o BatchMode=yes \
|
||||||
-p "${RUNNER_SSH_PORT}" \
|
-p "${RUNNER_SSH_PORT}" \
|
||||||
@@ -126,7 +271,9 @@ runner_scp() {
|
|||||||
if [[ "$RUNNER_SSH_HOST" == "local" ]]; then
|
if [[ "$RUNNER_SSH_HOST" == "local" ]]; then
|
||||||
cp "$src" "$dst"
|
cp "$src" "$dst"
|
||||||
else
|
else
|
||||||
scp -o ConnectTimeout=10 \
|
# shellcheck disable=SC2086
|
||||||
|
scp ${RUNNER_SSH_KEY:+-i "$RUNNER_SSH_KEY"} \
|
||||||
|
-o ConnectTimeout=10 \
|
||||||
-o StrictHostKeyChecking=accept-new \
|
-o StrictHostKeyChecking=accept-new \
|
||||||
-o BatchMode=yes \
|
-o BatchMode=yes \
|
||||||
-P "${RUNNER_SSH_PORT}" \
|
-P "${RUNNER_SSH_PORT}" \
|
||||||
@@ -136,16 +283,13 @@ runner_scp() {
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# add_docker_runner — Deploy a runner as a Docker container on a Linux host
|
# 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() {
|
add_docker_runner() {
|
||||||
log_info "Deploying Docker runner '${RUNNER_NAME}' on ${RUNNER_SSH_HOST}..."
|
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
|
# Check if container is already running
|
||||||
local status
|
local status
|
||||||
@@ -158,44 +302,41 @@ add_docker_runner() {
|
|||||||
# Create data directory on remote host
|
# Create data directory on remote host
|
||||||
runner_ssh "mkdir -p '${RUNNER_DATA_PATH}'"
|
runner_ssh "mkdir -p '${RUNNER_DATA_PATH}'"
|
||||||
|
|
||||||
# Render docker-compose template with runner-specific vars
|
# Render docker-compose template
|
||||||
local tmpfile
|
local tmpfile
|
||||||
tmpfile=$(mktemp)
|
tmpfile=$(mktemp)
|
||||||
export RUNNER_NAME RUNNER_LABELS RUNNER_DATA_PATH
|
export RUNNER_NAME RUNNER_DATA_PATH RUNNER_LABELS_CSV RUNNER_REG_TOKEN RUNNER_DEPLOY_RESOURCES
|
||||||
export GITEA_RUNNER_REGISTRATION_TOKEN="${GITEA_RUNNER_REGISTRATION_TOKEN:-}"
|
|
||||||
render_template "${SCRIPT_DIR}/templates/docker-compose-runner.yml.tpl" "$tmpfile" \
|
render_template "${SCRIPT_DIR}/templates/docker-compose-runner.yml.tpl" "$tmpfile" \
|
||||||
"\${ACT_RUNNER_VERSION} \${RUNNER_NAME} \${GITEA_INTERNAL_URL} \${GITEA_RUNNER_REGISTRATION_TOKEN} \${RUNNER_LABELS} \${RUNNER_DATA_PATH}"
|
"\${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"
|
runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/docker-compose.yml"
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
|
|
||||||
# Render runner config
|
# Render runner config
|
||||||
tmpfile=$(mktemp)
|
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" \
|
render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" \
|
||||||
"\${RUNNER_NAME} \${RUNNER_LABELS}"
|
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY}"
|
||||||
runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
runner_scp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
|
|
||||||
# Start the container
|
# Start the container
|
||||||
runner_ssh "cd '${RUNNER_DATA_PATH}' && docker compose up -d 2>/dev/null || docker-compose up -d"
|
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}"
|
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 — 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() {
|
add_native_runner() {
|
||||||
# Native runners use launchctl + macOS-specific paths — must be macOS
|
|
||||||
require_local_os "Darwin" "Native runner deployment requires macOS (uses launchctl)"
|
require_local_os "Darwin" "Native runner deployment requires macOS (uses launchctl)"
|
||||||
|
|
||||||
log_info "Deploying native runner '${RUNNER_NAME}' on local machine..."
|
log_info "Deploying native runner '${RUNNER_NAME}' on local machine..."
|
||||||
|
|
||||||
|
build_runner_labels
|
||||||
|
resolve_registration_token
|
||||||
|
|
||||||
# Resolve ~ to actual home directory for local execution
|
# Resolve ~ to actual home directory for local execution
|
||||||
RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}"
|
RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}"
|
||||||
export RUNNER_DATA_PATH
|
export RUNNER_DATA_PATH
|
||||||
@@ -214,7 +355,6 @@ add_native_runner() {
|
|||||||
|
|
||||||
# Download act_runner binary if not present
|
# Download act_runner binary if not present
|
||||||
if [[ ! -x "${RUNNER_DATA_PATH}/act_runner" ]]; then
|
if [[ ! -x "${RUNNER_DATA_PATH}/act_runner" ]]; then
|
||||||
# Detect architecture — Apple Silicon (arm64) vs Intel (x86_64)
|
|
||||||
local arch
|
local arch
|
||||||
arch=$(uname -m)
|
arch=$(uname -m)
|
||||||
case "$arch" in
|
case "$arch" in
|
||||||
@@ -230,26 +370,26 @@ add_native_runner() {
|
|||||||
log_success "act_runner binary downloaded"
|
log_success "act_runner binary downloaded"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Register the runner with Gitea (generates .runner file in data dir)
|
# Register the runner with Gitea
|
||||||
# --no-interactive skips prompts, --config generates default config if missing
|
|
||||||
if [[ ! -f "${RUNNER_DATA_PATH}/.runner" ]]; then
|
if [[ ! -f "${RUNNER_DATA_PATH}/.runner" ]]; then
|
||||||
log_info "Registering runner with Gitea..."
|
log_info "Registering runner with Gitea..."
|
||||||
"${RUNNER_DATA_PATH}/act_runner" register \
|
"${RUNNER_DATA_PATH}/act_runner" register \
|
||||||
--no-interactive \
|
--no-interactive \
|
||||||
--instance "${GITEA_INTERNAL_URL}" \
|
--instance "${GITEA_INTERNAL_URL}" \
|
||||||
--token "${GITEA_RUNNER_REGISTRATION_TOKEN:-}" \
|
--token "${RUNNER_REG_TOKEN}" \
|
||||||
--name "${RUNNER_NAME}" \
|
--name "${RUNNER_NAME}" \
|
||||||
--labels "${RUNNER_LABELS}" \
|
--labels "${RUNNER_LABELS_CSV}" \
|
||||||
--config "${RUNNER_DATA_PATH}/config.yaml"
|
--config "${RUNNER_DATA_PATH}/config.yaml"
|
||||||
log_success "Runner registered"
|
log_success "Runner registered"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Render runner config (overwrites any default generated by register)
|
# Render runner config
|
||||||
local tmpfile
|
local tmpfile
|
||||||
tmpfile=$(mktemp)
|
tmpfile=$(mktemp)
|
||||||
export RUNNER_NAME RUNNER_LABELS RUNNER_DATA_PATH
|
# 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" \
|
render_template "${SCRIPT_DIR}/templates/runner-config.yaml.tpl" "$tmpfile" \
|
||||||
"\${RUNNER_NAME} \${RUNNER_LABELS}"
|
"\${RUNNER_NAME} \${RUNNER_LABELS_YAML} \${RUNNER_CAPACITY}"
|
||||||
cp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
cp "$tmpfile" "${RUNNER_DATA_PATH}/config.yaml"
|
||||||
rm -f "$tmpfile"
|
rm -f "$tmpfile"
|
||||||
|
|
||||||
@@ -270,9 +410,8 @@ add_native_runner() {
|
|||||||
# remove_docker_runner — Stop + remove Docker runner container
|
# remove_docker_runner — Stop + remove Docker runner container
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
remove_docker_runner() {
|
remove_docker_runner() {
|
||||||
log_info "Removing Docker runner '${RUNNER_NAME}' from ${RUNNER_SSH_HOST}..."
|
log_info "Removing Docker runner '${RUNNER_NAME}' from ${RUNNER_HOST} (${RUNNER_SSH_HOST})..."
|
||||||
|
|
||||||
# Check if docker-compose file exists
|
|
||||||
if runner_ssh "test -f '${RUNNER_DATA_PATH}/docker-compose.yml'" 2>/dev/null; then
|
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
|
runner_ssh "cd '${RUNNER_DATA_PATH}' && docker compose down 2>/dev/null || docker-compose down" || true
|
||||||
log_success "Docker runner '${RUNNER_NAME}' stopped"
|
log_success "Docker runner '${RUNNER_NAME}' stopped"
|
||||||
@@ -285,30 +424,25 @@ remove_docker_runner() {
|
|||||||
# remove_native_runner — Unload launchd service + remove binary + plist
|
# remove_native_runner — Unload launchd service + remove binary + plist
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
remove_native_runner() {
|
remove_native_runner() {
|
||||||
# Native runners use launchctl + macOS-specific paths — must be macOS
|
|
||||||
require_local_os "Darwin" "Native runner removal requires macOS (uses launchctl)"
|
require_local_os "Darwin" "Native runner removal requires macOS (uses launchctl)"
|
||||||
|
|
||||||
log_info "Removing native runner '${RUNNER_NAME}' from local machine..."
|
log_info "Removing native runner '${RUNNER_NAME}' from local machine..."
|
||||||
|
|
||||||
# Resolve ~ to actual home directory
|
|
||||||
RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}"
|
RUNNER_DATA_PATH="${RUNNER_DATA_PATH/#\~/$HOME}"
|
||||||
|
|
||||||
local plist_name="com.gitea.runner.${RUNNER_NAME}.plist"
|
local plist_name="com.gitea.runner.${RUNNER_NAME}.plist"
|
||||||
local plist_path="$HOME/Library/LaunchAgents/${plist_name}"
|
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
|
if launchctl list 2>/dev/null | grep -q "com.gitea.runner.${RUNNER_NAME}"; then
|
||||||
launchctl unload "$plist_path" 2>/dev/null || true
|
launchctl unload "$plist_path" 2>/dev/null || true
|
||||||
log_success "Launchd service unloaded"
|
log_success "Launchd service unloaded"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove plist file
|
|
||||||
if [[ -f "$plist_path" ]]; then
|
if [[ -f "$plist_path" ]]; then
|
||||||
rm -f "$plist_path"
|
rm -f "$plist_path"
|
||||||
log_success "Plist removed: $plist_path"
|
log_success "Plist removed: $plist_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove runner data directory (binary, config, registration)
|
|
||||||
if [[ -d "${RUNNER_DATA_PATH}" ]]; then
|
if [[ -d "${RUNNER_DATA_PATH}" ]]; then
|
||||||
printf 'Remove runner data at %s? [y/N] ' "${RUNNER_DATA_PATH}"
|
printf 'Remove runner data at %s? [y/N] ' "${RUNNER_DATA_PATH}"
|
||||||
read -r confirm
|
read -r confirm
|
||||||
@@ -323,41 +457,35 @@ remove_native_runner() {
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# list_runners — Print table of all runners with their Gitea status
|
# 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() {
|
list_runners() {
|
||||||
log_info "Listing runners..."
|
log_info "Listing runners..."
|
||||||
|
|
||||||
# Fetch registered runners from Gitea admin API
|
if [[ ! -f "$RUNNERS_CONF" ]]; then
|
||||||
|
log_error "runners.conf not found at $RUNNERS_CONF"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local api_runners
|
local api_runners
|
||||||
api_runners=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]")
|
api_runners=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]")
|
||||||
|
|
||||||
# Print header
|
printf '%-20s %-10s %-10s %-8s %-6s %-10s\n' "NAME" "HOST" "LABELS" "TYPE" "CAP" "STATUS"
|
||||||
printf '%-20s %-16s %-10s %-8s %-10s\n' "NAME" "HOST" "LABELS" "TYPE" "STATUS"
|
printf '%-20s %-10s %-10s %-8s %-6s %-10s\n' "----" "----" "------" "----" "---" "------"
|
||||||
printf '%-20s %-16s %-10s %-8s %-10s\n' "----" "----" "------" "----" "------"
|
|
||||||
|
|
||||||
# For each entry in runners.conf, look up status in API response
|
local name host labels runner_type capacity status
|
||||||
while IFS='|' read -r name host user port path labels type; do
|
while IFS= read -r name; do
|
||||||
# Skip comments and blank lines
|
host=$(ini_get "$RUNNERS_CONF" "$name" "host" "")
|
||||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
labels=$(ini_get "$RUNNERS_CONF" "$name" "labels" "")
|
||||||
[[ -z "$name" ]] && continue
|
runner_type=$(ini_get "$RUNNERS_CONF" "$name" "type" "")
|
||||||
|
capacity=$(ini_get "$RUNNERS_CONF" "$name" "capacity" "1")
|
||||||
|
|
||||||
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)
|
status=$(printf '%s' "$api_runners" | jq -r --arg n "$name" '.[] | select(.name == $n) | .status' 2>/dev/null || true)
|
||||||
|
|
||||||
if [[ -z "$status" ]]; then
|
if [[ -z "$status" ]]; then
|
||||||
status="not-found"
|
status="not-found"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%-20s %-16s %-10s %-8s %-10s\n' "$name" "$host" "$labels" "$type" "$status"
|
printf '%-20s %-10s %-10s %-8s %-6s %-10s\n' "$name" "$host" "$labels" "$runner_type" "$capacity" "$status"
|
||||||
done < "$RUNNERS_CONF"
|
done < <(ini_list_sections "$RUNNERS_CONF")
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -388,7 +516,6 @@ case "$COMMAND" in
|
|||||||
log_error "add requires --name <runner_name>"
|
log_error "add requires --name <runner_name>"
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
require_vars GITEA_RUNNER_REGISTRATION_TOKEN
|
|
||||||
parse_runner_entry "$RUNNER_ARG_NAME"
|
parse_runner_entry "$RUNNER_ARG_NAME"
|
||||||
case "$RUNNER_TYPE" in
|
case "$RUNNER_TYPE" in
|
||||||
docker) add_docker_runner ;;
|
docker) add_docker_runner ;;
|
||||||
|
|||||||
@@ -36,13 +36,9 @@ API_RUNNERS=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]")
|
|||||||
# Check each runner: exists in API response AND status is online/idle/active
|
# 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".
|
# A runner that registered but is not running will show as "offline".
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
while IFS='|' read -r name rest; do
|
while IFS= read -r name; do
|
||||||
# Skip comments and blank lines
|
|
||||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
name=$(echo "$name" | xargs)
|
|
||||||
|
|
||||||
# Look up runner by name in the API response
|
# 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)
|
local_status=$(printf '%s' "$API_RUNNERS" | jq -r --arg n "$name" '.[] | select(.name == $n) | .status' 2>/dev/null || true)
|
||||||
|
|
||||||
@@ -56,12 +52,12 @@ while IFS='|' read -r name rest; do
|
|||||||
log_success "Runner '${name}' is ${local_status}"
|
log_success "Runner '${name}' is ${local_status}"
|
||||||
PASS=$((PASS + 1))
|
PASS=$((PASS + 1))
|
||||||
fi
|
fi
|
||||||
done < "$RUNNERS_CONF"
|
done < <(ini_list_sections "$RUNNERS_CONF")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Check: runner count matches runners.conf
|
# Check: runner count matches runners.conf
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
EXPECTED_COUNT=$(grep -Evc '^[[:space:]]*($|#)' "$RUNNERS_CONF")
|
EXPECTED_COUNT=$(ini_list_sections "$RUNNERS_CONF" | wc -l | xargs)
|
||||||
ACTUAL_COUNT=$(printf '%s' "$API_RUNNERS" | jq 'length' 2>/dev/null || echo 0)
|
ACTUAL_COUNT=$(printf '%s' "$API_RUNNERS" | jq 'length' 2>/dev/null || echo 0)
|
||||||
|
|
||||||
if [[ "$ACTUAL_COUNT" -ge "$EXPECTED_COUNT" ]]; then
|
if [[ "$ACTUAL_COUNT" -ge "$EXPECTED_COUNT" ]]; then
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ if [[ ! -f "$RUNNERS_CONF" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Count non-comment, non-blank lines to verify there are runners to deploy
|
# Count INI sections to verify there are runners to deploy
|
||||||
RUNNER_COUNT=$(grep -Evc '^[[:space:]]*($|#)' "$RUNNERS_CONF")
|
RUNNER_COUNT=$(ini_list_sections "$RUNNERS_CONF" | wc -l | xargs)
|
||||||
if [[ "$RUNNER_COUNT" -eq 0 ]]; then
|
if [[ "$RUNNER_COUNT" -eq 0 ]]; then
|
||||||
log_error "No runners defined in runners.conf"
|
log_error "No runners defined in runners.conf"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -63,8 +63,7 @@ fi
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Step 2: Deploy each runner via manage_runner.sh
|
# Step 2: Deploy each runner via manage_runner.sh
|
||||||
# Iterates over every non-comment line in runners.conf, extracts the name
|
# Iterates over every INI section in runners.conf (each section = one runner).
|
||||||
# (first pipe-delimited field), and invokes manage_runner.sh add.
|
|
||||||
# manage_runner.sh handles its own idempotency (skips already-running runners).
|
# manage_runner.sh handles its own idempotency (skips already-running runners).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log_step 2 "Deploying runners..."
|
log_step 2 "Deploying runners..."
|
||||||
@@ -72,12 +71,8 @@ log_step 2 "Deploying runners..."
|
|||||||
DEPLOYED=0
|
DEPLOYED=0
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
||||||
while IFS='|' read -r name rest; do
|
while IFS= read -r name; do
|
||||||
# Skip comments and blank lines
|
|
||||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
name=$(echo "$name" | xargs)
|
|
||||||
log_info "Processing runner: ${name}"
|
log_info "Processing runner: ${name}"
|
||||||
|
|
||||||
if "${SCRIPT_DIR}/manage_runner.sh" add --name "$name"; then
|
if "${SCRIPT_DIR}/manage_runner.sh" add --name "$name"; then
|
||||||
@@ -86,7 +81,7 @@ while IFS='|' read -r name rest; do
|
|||||||
log_error "Failed to deploy runner: ${name}"
|
log_error "Failed to deploy runner: ${name}"
|
||||||
FAILED=$((FAILED + 1))
|
FAILED=$((FAILED + 1))
|
||||||
fi
|
fi
|
||||||
done < "$RUNNERS_CONF"
|
done < <(ini_list_sections "$RUNNERS_CONF")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
|
|||||||
@@ -31,15 +31,11 @@ fi
|
|||||||
printf 'This will stop and remove all runners. Continue? [y/N] '
|
printf 'This will stop and remove all runners. Continue? [y/N] '
|
||||||
read -r confirm
|
read -r confirm
|
||||||
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
if [[ "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
while IFS='|' read -r name rest; do
|
while IFS= read -r name; do
|
||||||
# Skip comments and blank lines
|
|
||||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
name=$(echo "$name" | xargs)
|
|
||||||
log_info "Removing runner: ${name}"
|
log_info "Removing runner: ${name}"
|
||||||
"${SCRIPT_DIR}/manage_runner.sh" remove --name "$name" || true
|
"${SCRIPT_DIR}/manage_runner.sh" remove --name "$name" || true
|
||||||
done < "$RUNNERS_CONF"
|
done < <(ini_list_sections "$RUNNERS_CONF")
|
||||||
log_success "All runners removed"
|
log_success "All runners removed"
|
||||||
else
|
else
|
||||||
log_info "Skipped runner removal"
|
log_info "Skipped runner removal"
|
||||||
|
|||||||
191
preflight.sh
191
preflight.sh
@@ -99,6 +99,122 @@ if [[ ! -f "${SCRIPT_DIR}/runners.conf" ]]; then
|
|||||||
log_error " → runners.conf not found. Copy runners.conf.example to runners.conf."
|
log_error " → runners.conf not found. Copy runners.conf.example to runners.conf."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Check 5b: runners.conf INI format validation
|
||||||
|
# Validates required fields, enum values, and format per runner section.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# shellcheck disable=SC2329
|
||||||
|
check_runners_ini_format() {
|
||||||
|
local conf="${SCRIPT_DIR}/runners.conf"
|
||||||
|
[[ -f "$conf" ]] || return 1
|
||||||
|
|
||||||
|
local errors=0
|
||||||
|
local section
|
||||||
|
|
||||||
|
while IFS= read -r section; do
|
||||||
|
[[ -z "$section" ]] && continue
|
||||||
|
|
||||||
|
# Required fields
|
||||||
|
local host type data_path labels capacity repos
|
||||||
|
host=$(ini_get "$conf" "$section" "host" "")
|
||||||
|
type=$(ini_get "$conf" "$section" "type" "")
|
||||||
|
data_path=$(ini_get "$conf" "$section" "data_path" "")
|
||||||
|
labels=$(ini_get "$conf" "$section" "labels" "")
|
||||||
|
capacity=$(ini_get "$conf" "$section" "capacity" "")
|
||||||
|
repos=$(ini_get "$conf" "$section" "repos" "all")
|
||||||
|
|
||||||
|
# host: must be unraid, fedora, local, or custom
|
||||||
|
case "$host" in
|
||||||
|
unraid|fedora|local|custom) ;;
|
||||||
|
"")
|
||||||
|
log_error " → [$section] host is empty (must be unraid, fedora, local, or custom)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error " → [$section] host='$host' (must be unraid, fedora, local, or custom)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# type: must be docker or native
|
||||||
|
case "$type" in
|
||||||
|
docker|native) ;;
|
||||||
|
"")
|
||||||
|
log_error " → [$section] type is empty (must be docker or native)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error " → [$section] type='$type' (must be docker or native)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# data_path: must start with / or ~/
|
||||||
|
# shellcheck disable=SC2088 # tilde intentionally stored as literal string
|
||||||
|
if [[ -z "$data_path" ]]; then
|
||||||
|
log_error " → [$section] data_path is empty"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
elif [[ "$data_path" != /* ]] && [[ "$data_path" != "~/"* ]] && [[ "$data_path" != "~" ]]; then
|
||||||
|
log_error " → [$section] data_path='$data_path' (must start with / or ~/)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# labels: non-empty
|
||||||
|
if [[ -z "$labels" ]]; then
|
||||||
|
log_error " → [$section] labels is empty"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# capacity: positive integer >= 1
|
||||||
|
if [[ -n "$capacity" ]] && ! [[ "$capacity" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
log_error " → [$section] capacity='$capacity' (must be positive integer >= 1)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# repos: must be "all" or a known REPO_*_NAME
|
||||||
|
if [[ "$repos" != "all" ]] && [[ -n "$repos" ]]; then
|
||||||
|
local repos_valid=false
|
||||||
|
for var in REPO_1_NAME REPO_2_NAME REPO_3_NAME; do
|
||||||
|
if [[ "$repos" == "${!var:-}" ]]; then repos_valid=true; break; fi
|
||||||
|
done
|
||||||
|
if ! $repos_valid; then
|
||||||
|
log_error " → [$section] repos='$repos' (must be 'all' or a known repo name)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# docker runner: default_image should be set
|
||||||
|
if [[ "$type" == "docker" ]]; then
|
||||||
|
local default_image
|
||||||
|
default_image=$(ini_get "$conf" "$section" "default_image" "")
|
||||||
|
if [[ -z "$default_image" ]] && [[ -z "${RUNNER_DEFAULT_IMAGE:-}" ]]; then
|
||||||
|
log_error " → [$section] default_image is empty and RUNNER_DEFAULT_IMAGE not set in .env"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# custom host: extra SSH keys required
|
||||||
|
if [[ "$host" == "custom" ]]; then
|
||||||
|
local ssh_host ssh_user
|
||||||
|
ssh_host=$(ini_get "$conf" "$section" "ssh_host" "")
|
||||||
|
ssh_user=$(ini_get "$conf" "$section" "ssh_user" "")
|
||||||
|
if [[ -z "$ssh_host" ]]; then
|
||||||
|
log_error " → [$section] ssh_host is empty (required for host=custom)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
if [[ -z "$ssh_user" ]]; then
|
||||||
|
log_error " → [$section] ssh_user is empty (required for host=custom)"
|
||||||
|
errors=$((errors + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(ini_list_sections "$conf")
|
||||||
|
|
||||||
|
[[ $errors -eq 0 ]]
|
||||||
|
}
|
||||||
|
if [[ -f "${SCRIPT_DIR}/runners.conf" ]]; then
|
||||||
|
check 5b "runners.conf INI format validation" check_runners_ini_format
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Load env for remaining checks (may fail if .env missing)
|
# Load env for remaining checks (may fail if .env missing)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -118,6 +234,7 @@ REQUIRED_VARS=(
|
|||||||
GITEA_BACKUP_INTERNAL_URL BACKUP_STORAGE_PATH
|
GITEA_BACKUP_INTERNAL_URL BACKUP_STORAGE_PATH
|
||||||
GITHUB_USERNAME GITHUB_TOKEN
|
GITHUB_USERNAME GITHUB_TOKEN
|
||||||
REPO_1_NAME REPO_2_NAME REPO_3_NAME
|
REPO_1_NAME REPO_2_NAME REPO_3_NAME
|
||||||
|
RUNNER_DEFAULT_IMAGE RUNNER_DEFAULT_DATA_PATH LOCAL_RUNNER_DATA_PATH
|
||||||
GITHUB_MIRROR_TOKEN
|
GITHUB_MIRROR_TOKEN
|
||||||
NGINX_CONTAINER_NAME NGINX_CONF_PATH SSL_MODE
|
NGINX_CONTAINER_NAME NGINX_CONF_PATH SSL_MODE
|
||||||
)
|
)
|
||||||
@@ -262,11 +379,31 @@ if [[ "$SKIP_PORT_CHECKS" == "true" ]]; then
|
|||||||
else
|
else
|
||||||
check_port_unraid() {
|
check_port_unraid() {
|
||||||
local port="${UNRAID_GITEA_PORT:-3000}"
|
local port="${UNRAID_GITEA_PORT:-3000}"
|
||||||
! ssh_exec UNRAID "ss -tlnp | grep -q ':${port} '" 2>/dev/null
|
local ss_output
|
||||||
|
local grep_rc
|
||||||
|
|
||||||
|
# Fail closed on SSH/remote command errors.
|
||||||
|
ss_output=$(ssh_exec UNRAID "ss -tlnp" 2>/dev/null) || return 1
|
||||||
|
|
||||||
|
# grep exit codes:
|
||||||
|
# 0 => match found (port in use) => FAIL check
|
||||||
|
# 1 => no match (port free) => PASS check
|
||||||
|
# >1 => grep error => FAIL check
|
||||||
|
if printf '%s\n' "$ss_output" | grep -q ":${port} "; then
|
||||||
|
grep_rc=0
|
||||||
|
else
|
||||||
|
grep_rc=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$grep_rc" in
|
||||||
|
0) return 1 ;;
|
||||||
|
1) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
check 13 "Port ${UNRAID_GITEA_PORT:-3000} free on Unraid" check_port_unraid
|
check 13 "Port ${UNRAID_GITEA_PORT:-3000} free on Unraid" check_port_unraid
|
||||||
if ! check_port_unraid 2>/dev/null; then
|
if ! check_port_unraid 2>/dev/null; then
|
||||||
log_error " → Port ${UNRAID_GITEA_PORT:-3000} already in use on Unraid."
|
log_error " → Port check failed or port ${UNRAID_GITEA_PORT:-3000} is already in use on Unraid."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -274,11 +411,31 @@ else
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
check_port_fedora() {
|
check_port_fedora() {
|
||||||
local port="${FEDORA_GITEA_PORT:-3000}"
|
local port="${FEDORA_GITEA_PORT:-3000}"
|
||||||
! ssh_exec FEDORA "ss -tlnp | grep -q ':${port} '" 2>/dev/null
|
local ss_output
|
||||||
|
local grep_rc
|
||||||
|
|
||||||
|
# Fail closed on SSH/remote command errors.
|
||||||
|
ss_output=$(ssh_exec FEDORA "ss -tlnp" 2>/dev/null) || return 1
|
||||||
|
|
||||||
|
# grep exit codes:
|
||||||
|
# 0 => match found (port in use) => FAIL check
|
||||||
|
# 1 => no match (port free) => PASS check
|
||||||
|
# >1 => grep error => FAIL check
|
||||||
|
if printf '%s\n' "$ss_output" | grep -q ":${port} "; then
|
||||||
|
grep_rc=0
|
||||||
|
else
|
||||||
|
grep_rc=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$grep_rc" in
|
||||||
|
0) return 1 ;;
|
||||||
|
1) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
check 14 "Port ${FEDORA_GITEA_PORT:-3000} free on Fedora" check_port_fedora
|
check 14 "Port ${FEDORA_GITEA_PORT:-3000} free on Fedora" check_port_fedora
|
||||||
if ! check_port_fedora 2>/dev/null; then
|
if ! check_port_fedora 2>/dev/null; then
|
||||||
log_error " → Port ${FEDORA_GITEA_PORT:-3000} already in use on Fedora."
|
log_error " → Port check failed or port ${FEDORA_GITEA_PORT:-3000} is already in use on Fedora."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -286,9 +443,19 @@ fi
|
|||||||
# Check 15: DNS resolves
|
# Check 15: DNS resolves
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
check_dns() {
|
check_dns() {
|
||||||
|
# Fail closed when required values are missing.
|
||||||
|
[[ -n "${GITEA_DOMAIN:-}" ]] || return 1
|
||||||
|
[[ -n "${UNRAID_IP:-}" ]] || return 1
|
||||||
|
|
||||||
local resolved
|
local resolved
|
||||||
resolved=$(dig +short "${GITEA_DOMAIN:-}" 2>/dev/null | head -1)
|
resolved=$(dig +short "${GITEA_DOMAIN}" A 2>/dev/null | sed '/^[[:space:]]*$/d') || return 1
|
||||||
[[ "$resolved" == "${UNRAID_IP:-}" ]]
|
[[ -n "$resolved" ]] || return 1
|
||||||
|
|
||||||
|
# Pass only if one of the domain's A records exactly matches UNRAID_IP.
|
||||||
|
if printf '%s\n' "$resolved" | grep -Fxq "${UNRAID_IP}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
check 15 "DNS: ${GITEA_DOMAIN:-<not set>} resolves to ${UNRAID_IP:-<not set>}" check_dns
|
check 15 "DNS: ${GITEA_DOMAIN:-<not set>} resolves to ${UNRAID_IP:-<not set>}" check_dns
|
||||||
if ! check_dns 2>/dev/null; then
|
if ! check_dns 2>/dev/null; then
|
||||||
@@ -312,13 +479,21 @@ fi
|
|||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
check_github_repos() {
|
check_github_repos() {
|
||||||
local all_ok=0
|
local all_ok=0
|
||||||
|
|
||||||
|
if [[ -z "${GITHUB_USERNAME:-}" ]]; then
|
||||||
|
log_error " → GITHUB_USERNAME is empty"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
for var in REPO_1_NAME REPO_2_NAME REPO_3_NAME; do
|
for var in REPO_1_NAME REPO_2_NAME REPO_3_NAME; do
|
||||||
local repo="${!var:-}"
|
local repo="${!var:-}"
|
||||||
if [[ -z "$repo" ]]; then
|
if [[ -z "$repo" ]]; then
|
||||||
|
log_error " → ${var} is empty"
|
||||||
|
all_ok=1
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if ! curl -sf -H "Authorization: token ${GITHUB_TOKEN:-}" "https://api.github.com/repos/${GITHUB_USERNAME:-}/${repo}" -o /dev/null 2>/dev/null; then
|
if ! curl -sf -H "Authorization: token ${GITHUB_TOKEN:-}" "https://api.github.com/repos/${GITHUB_USERNAME}/${repo}" -o /dev/null 2>/dev/null; then
|
||||||
log_error " → GitHub repo ${repo} not found under ${GITHUB_USERNAME:-}"
|
log_error " → GitHub repo ${repo} not found under ${GITHUB_USERNAME}"
|
||||||
all_ok=1
|
all_ok=1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,20 +1,82 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# runners.conf — Gitea Actions Runner Definitions
|
# runners.conf — Gitea Actions Runner Definitions (INI format)
|
||||||
# Copy to runners.conf and edit. One runner per line.
|
# Copy to runners.conf and edit, or generate interactively with:
|
||||||
|
# ./setup/configure_runners.sh
|
||||||
# Use manage_runner.sh to add/remove runners dynamically.
|
# Use manage_runner.sh to add/remove runners dynamically.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
#
|
#
|
||||||
# FORMAT: name|ssh_host|ssh_user|ssh_port|data_path|labels|type
|
# Each [section] defines one runner. Section name = runner display name in Gitea.
|
||||||
#
|
#
|
||||||
# name — Display name in Gitea admin panel
|
# FIELD REFERENCE:
|
||||||
# ssh_host — IP address or hostname (for local machine, use "local")
|
|
||||||
# ssh_user — SSH username (ignored if ssh_host is "local")
|
|
||||||
# ssh_port — SSH port (ignored if ssh_host is "local")
|
|
||||||
# data_path — Absolute path for runner binary + data on that machine
|
|
||||||
# labels — Comma-separated runner labels (used in workflow runs-on)
|
|
||||||
# type — "docker" (Linux, runs jobs in containers) or "native" (macOS, runs jobs on host)
|
|
||||||
#
|
#
|
||||||
# EXAMPLES:
|
# host — enum: unraid, fedora, local, custom
|
||||||
unraid-runner|192.168.1.10|root|22|/mnt/nvme/gitea-runner|linux|docker
|
# Known hosts resolve SSH from .env (IP, user, port, key).
|
||||||
fedora-runner|192.168.1.20|user|22|/mnt/nvme/gitea-runner|linux|docker
|
# "local" runs on this machine. "custom" requires extra keys
|
||||||
macbook-runner|local|_|_|~/gitea-runner|macos|native
|
# in the section: ssh_host, ssh_user, ssh_port, ssh_key.
|
||||||
|
#
|
||||||
|
# type — enum: docker, native
|
||||||
|
# "docker" = Linux container on remote host via Docker Compose.
|
||||||
|
# "native" = macOS binary + launchd on local machine.
|
||||||
|
#
|
||||||
|
# data_path — Absolute path (/ or ~/) where act_runner stores its binary,
|
||||||
|
# config, and job cache. NOT Gitea's data path.
|
||||||
|
# Default: RUNNER_DEFAULT_DATA_PATH (docker), LOCAL_RUNNER_DATA_PATH (native)
|
||||||
|
#
|
||||||
|
# labels — Workflow runs-on value (e.g. linux, macos, ubuntu-latest).
|
||||||
|
# Combined with default_image at deploy time to produce
|
||||||
|
# act_runner label spec: "linux:docker://image" or "macos:host".
|
||||||
|
#
|
||||||
|
# default_image — Docker image for job execution (docker runners only).
|
||||||
|
# Default: RUNNER_DEFAULT_IMAGE from .env.
|
||||||
|
# If LOCAL_REGISTRY is set, resolved as LOCAL_REGISTRY/image.
|
||||||
|
# Leave empty for native runners.
|
||||||
|
#
|
||||||
|
# repos — Token hint (not an act_runner setting): controls which
|
||||||
|
# registration token manage_runner.sh fetches at deploy time.
|
||||||
|
# "all" = instance-level token (runner available to all repos).
|
||||||
|
# Repo name = fetch repo-level token from Gitea API.
|
||||||
|
#
|
||||||
|
# capacity — Positive integer (>= 1). Max concurrent jobs.
|
||||||
|
# Default: RUNNER_DEFAULT_CAPACITY from .env.
|
||||||
|
# Constrained by host CPU/RAM. 0 and negative are invalid.
|
||||||
|
#
|
||||||
|
# cpu — Docker CPU limit (e.g. 2.0, 0.5). Ignored for native.
|
||||||
|
# Empty = no limit.
|
||||||
|
#
|
||||||
|
# memory — Docker memory limit (e.g. 2g, 512m). Ignored for native.
|
||||||
|
# Empty = no limit.
|
||||||
|
#
|
||||||
|
# STARTER ENTRIES (uncomment and edit):
|
||||||
|
|
||||||
|
#[unraid-runner]
|
||||||
|
#host = unraid
|
||||||
|
#type = docker
|
||||||
|
#data_path = /mnt/nvme/gitea-runner
|
||||||
|
#labels = linux
|
||||||
|
#default_image = catthehacker/ubuntu:act-latest
|
||||||
|
#repos = all
|
||||||
|
#capacity = 1
|
||||||
|
#cpu =
|
||||||
|
#memory =
|
||||||
|
|
||||||
|
#[fedora-runner]
|
||||||
|
#host = fedora
|
||||||
|
#type = docker
|
||||||
|
#data_path = /mnt/nvme/gitea-runner
|
||||||
|
#labels = linux
|
||||||
|
#default_image = catthehacker/ubuntu:act-latest
|
||||||
|
#repos = all
|
||||||
|
#capacity = 1
|
||||||
|
#cpu =
|
||||||
|
#memory =
|
||||||
|
|
||||||
|
#[macbook-runner]
|
||||||
|
#host = local
|
||||||
|
#type = native
|
||||||
|
#data_path = ~/gitea-runner
|
||||||
|
#labels = macos
|
||||||
|
#default_image =
|
||||||
|
#repos = all
|
||||||
|
#capacity = 1
|
||||||
|
#cpu =
|
||||||
|
#memory =
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ done < "$ENV_FILE"
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Prompt function
|
# Prompt function
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
TOTAL_PROMPTS=50
|
TOTAL_PROMPTS=59
|
||||||
CURRENT_PROMPT=0
|
CURRENT_PROMPT=0
|
||||||
LAST_SECTION=""
|
LAST_SECTION=""
|
||||||
|
|
||||||
@@ -122,6 +122,11 @@ prompt_var() {
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Optional fields accept any value including empty
|
||||||
|
if [[ "$validation" == "optional" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
case "$validation" in
|
case "$validation" in
|
||||||
ip)
|
ip)
|
||||||
if validate_ip "$value"; then break; fi
|
if validate_ip "$value"; then break; fi
|
||||||
@@ -151,6 +156,10 @@ prompt_var() {
|
|||||||
if validate_integer "$value"; then break; fi
|
if validate_integer "$value"; then break; fi
|
||||||
printf '%b Invalid: must be a number%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid: must be a number%b\n' "$C_RED" "$C_RESET"
|
||||||
;;
|
;;
|
||||||
|
positive_integer)
|
||||||
|
if validate_positive_integer "$value"; then break; fi
|
||||||
|
printf '%b Invalid: must be a positive integer (>= 1)%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
password)
|
password)
|
||||||
if validate_password "$value"; then break; fi
|
if validate_password "$value"; then break; fi
|
||||||
printf '%b Invalid: password must be at least 8 characters%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid: password must be at least 8 characters%b\n' "$C_RED" "$C_RESET"
|
||||||
@@ -218,6 +227,7 @@ prompt_var "UNRAID_SSH_PORT" "SSH port"
|
|||||||
prompt_var "UNRAID_GITEA_PORT" "Port Gitea web UI will listen on" port "3000" "UNRAID SERVER"
|
prompt_var "UNRAID_GITEA_PORT" "Port Gitea web UI will listen on" port "3000" "UNRAID SERVER"
|
||||||
prompt_var "UNRAID_GITEA_SSH_PORT" "Port for git-over-SSH" port "2222" "UNRAID SERVER"
|
prompt_var "UNRAID_GITEA_SSH_PORT" "Port for git-over-SSH" port "2222" "UNRAID SERVER"
|
||||||
prompt_var "UNRAID_GITEA_DATA_PATH" "Absolute path on NVMe for Gitea data" path "" "UNRAID SERVER"
|
prompt_var "UNRAID_GITEA_DATA_PATH" "Absolute path on NVMe for Gitea data" path "" "UNRAID SERVER"
|
||||||
|
prompt_var "UNRAID_SSH_KEY" "Path to SSH private key (empty = ssh-agent)" optional "" "UNRAID SERVER"
|
||||||
|
|
||||||
# --- FEDORA SERVER ---
|
# --- FEDORA SERVER ---
|
||||||
prompt_var "FEDORA_IP" "Static IP of Fedora server" ip "" "FEDORA SERVER"
|
prompt_var "FEDORA_IP" "Static IP of Fedora server" ip "" "FEDORA SERVER"
|
||||||
@@ -226,6 +236,7 @@ prompt_var "FEDORA_SSH_PORT" "SSH port"
|
|||||||
prompt_var "FEDORA_GITEA_PORT" "Port Gitea web UI will listen on" port "3000" "FEDORA SERVER"
|
prompt_var "FEDORA_GITEA_PORT" "Port Gitea web UI will listen on" port "3000" "FEDORA SERVER"
|
||||||
prompt_var "FEDORA_GITEA_SSH_PORT" "Port for git-over-SSH" port "2222" "FEDORA SERVER"
|
prompt_var "FEDORA_GITEA_SSH_PORT" "Port for git-over-SSH" port "2222" "FEDORA SERVER"
|
||||||
prompt_var "FEDORA_GITEA_DATA_PATH" "Absolute path on NVMe for Gitea data" path "" "FEDORA SERVER"
|
prompt_var "FEDORA_GITEA_DATA_PATH" "Absolute path on NVMe for Gitea data" path "" "FEDORA SERVER"
|
||||||
|
prompt_var "FEDORA_SSH_KEY" "Path to SSH private key (empty = ssh-agent)" optional "" "FEDORA SERVER"
|
||||||
|
|
||||||
# --- GITEA SHARED CREDENTIALS ---
|
# --- GITEA SHARED CREDENTIALS ---
|
||||||
prompt_var "GITEA_ADMIN_USER" "Admin username (same on both instances)" nonempty "" "GITEA SHARED CREDENTIALS"
|
prompt_var "GITEA_ADMIN_USER" "Admin username (same on both instances)" nonempty "" "GITEA SHARED CREDENTIALS"
|
||||||
@@ -258,6 +269,14 @@ prompt_var "MIGRATE_LABELS" "Migrate GitHub labels" bool
|
|||||||
prompt_var "MIGRATE_MILESTONES" "Migrate GitHub milestones" bool "false" "REPOSITORIES"
|
prompt_var "MIGRATE_MILESTONES" "Migrate GitHub milestones" bool "false" "REPOSITORIES"
|
||||||
prompt_var "MIGRATE_WIKI" "Migrate GitHub wiki" bool "false" "REPOSITORIES"
|
prompt_var "MIGRATE_WIKI" "Migrate GitHub wiki" bool "false" "REPOSITORIES"
|
||||||
|
|
||||||
|
# --- RUNNERS ---
|
||||||
|
prompt_var "RUNNER_DEFAULT_IMAGE" "Default container image for docker runners" nonempty "catthehacker/ubuntu:act-latest" "RUNNERS"
|
||||||
|
prompt_var "RUNNER_DEFAULT_CAPACITY" "Default max concurrent jobs per runner" positive_integer "1" "RUNNERS"
|
||||||
|
prompt_var "RUNNER_DEFAULT_DATA_PATH" "Default data path for remote (docker) runners" nonempty "/mnt/nvme/gitea-runner" "RUNNERS"
|
||||||
|
# shellcheck disable=SC2088 # tilde intentionally stored as literal (expanded at runtime)
|
||||||
|
prompt_var "LOCAL_RUNNER_DATA_PATH" "Data path for native macOS runner" nonempty "~/gitea-runner" "RUNNERS"
|
||||||
|
prompt_var "LOCAL_REGISTRY" "Local registry prefix (empty = Docker Hub)" optional "" "RUNNERS"
|
||||||
|
|
||||||
# --- GITHUB MIRROR ---
|
# --- GITHUB MIRROR ---
|
||||||
prompt_var "GITHUB_MIRROR_TOKEN" "GitHub PAT with repo write scope" nonempty "" "GITHUB MIRROR"
|
prompt_var "GITHUB_MIRROR_TOKEN" "GitHub PAT with repo write scope" nonempty "" "GITHUB MIRROR"
|
||||||
prompt_var "GITHUB_MIRROR_INTERVAL" "How often Gitea pushes to GitHub" nonempty "8h" "GITHUB MIRROR"
|
prompt_var "GITHUB_MIRROR_INTERVAL" "How often Gitea pushes to GitHub" nonempty "8h" "GITHUB MIRROR"
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ set -euo pipefail
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# setup/configure_runners.sh — Interactive runners.conf configuration wizard
|
# setup/configure_runners.sh — Interactive runners.conf configuration wizard
|
||||||
# Prompts for each runner's fields with validation and progress tracking.
|
# Prompts for each runner's fields with validation and progress tracking.
|
||||||
|
# Writes INI-style runners.conf (see runners.conf.example for format).
|
||||||
|
# Defaults are pulled from .env — nothing is hardcoded.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
@@ -14,6 +16,9 @@ RUNNERS_CONF="${PROJECT_ROOT}/runners.conf"
|
|||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
source "${PROJECT_ROOT}/lib/common.sh"
|
source "${PROJECT_ROOT}/lib/common.sh"
|
||||||
|
|
||||||
|
# Load .env for defaults (RUNNER_DEFAULT_IMAGE, RUNNER_DEFAULT_CAPACITY, etc.)
|
||||||
|
load_env
|
||||||
|
|
||||||
# Colors — only emit ANSI escapes when stdout is a terminal (not piped/redirected)
|
# Colors — only emit ANSI escapes when stdout is a terminal (not piped/redirected)
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
C_RESET='\033[0m'; C_BOLD='\033[1m'; C_GREEN='\033[0;32m'
|
C_RESET='\033[0m'; C_BOLD='\033[1m'; C_GREEN='\033[0;32m'
|
||||||
@@ -24,62 +29,56 @@ fi
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Load existing runner entries as defaults (idempotent re-runs).
|
# Load existing runner entries as defaults (idempotent re-runs).
|
||||||
# If runners.conf already exists, parse each non-comment line into parallel
|
# If runners.conf already exists and is INI format, we read existing values
|
||||||
# arrays so we can pre-fill prompts with previous values.
|
# to pre-fill prompts when re-running the wizard.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
EXISTING_NAMES=()
|
EXISTING_SECTIONS=()
|
||||||
EXISTING_HOSTS=()
|
|
||||||
EXISTING_USERS=()
|
|
||||||
EXISTING_PORTS=()
|
|
||||||
EXISTING_PATHS=()
|
|
||||||
EXISTING_LABELS=()
|
|
||||||
EXISTING_TYPES=()
|
|
||||||
|
|
||||||
if [[ -f "$RUNNERS_CONF" ]]; then
|
if [[ -f "$RUNNERS_CONF" ]]; then
|
||||||
while IFS='|' read -r name host user port path labels type; do
|
while IFS= read -r sec; do
|
||||||
[[ "$name" =~ ^[[:space:]]*# ]] && continue # skip comments
|
[[ -z "$sec" ]] && continue
|
||||||
[[ -z "$name" ]] && continue # skip blank lines
|
EXISTING_SECTIONS+=("$sec")
|
||||||
# Trim whitespace from each field (xargs strips leading/trailing spaces)
|
done < <(ini_list_sections "$RUNNERS_CONF")
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_NAMES+=("$(echo "$name" | xargs)")
|
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_HOSTS+=("$(echo "$host" | xargs)")
|
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_USERS+=("$(echo "$user" | xargs)")
|
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_PORTS+=("$(echo "$port" | xargs)")
|
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_PATHS+=("$(echo "$path" | xargs)")
|
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_LABELS+=("$(echo "$labels" | xargs)")
|
|
||||||
# shellcheck disable=SC2005
|
|
||||||
EXISTING_TYPES+=("$(echo "$type" | xargs)")
|
|
||||||
done < "$RUNNERS_CONF"
|
|
||||||
fi
|
fi
|
||||||
|
EXISTING_COUNT=${#EXISTING_SECTIONS[@]}
|
||||||
EXISTING_COUNT=${#EXISTING_NAMES[@]}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Validation helpers
|
# Validation helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Must start with alphanumeric; rest can include hyphens and underscores.
|
|
||||||
# Matches the naming convention Gitea uses in its admin panel.
|
|
||||||
validate_runner_name() {
|
validate_runner_name() {
|
||||||
[[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]
|
[[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only two runner types exist: docker (Linux containers) and native (macOS binary)
|
|
||||||
validate_runner_type() {
|
validate_runner_type() {
|
||||||
[[ "$1" == "docker" || "$1" == "native" ]]
|
[[ "$1" == "docker" || "$1" == "native" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Absolute paths (/) for remote hosts, or ~/ for native macOS runners.
|
validate_runner_host() {
|
||||||
# Tilde is stored literally; manage_runner.sh expands it at runtime.
|
[[ "$1" == "unraid" || "$1" == "fedora" || "$1" == "local" || "$1" == "custom" ]]
|
||||||
|
}
|
||||||
|
|
||||||
validate_runner_path() {
|
validate_runner_path() {
|
||||||
# shellcheck disable=SC2088 # tilde intentionally stored as literal string
|
# shellcheck disable=SC2088 # tilde intentionally stored as literal string
|
||||||
[[ "$1" == /* || "$1" == "~/"* || "$1" == "~" ]]
|
[[ "$1" == /* || "$1" == "~/"* || "$1" == "~" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_runner_repos() {
|
||||||
|
if [[ "$1" == "all" ]]; then return 0; fi
|
||||||
|
# Check against known REPO_*_NAME values
|
||||||
|
[[ "$1" == "${REPO_1_NAME:-}" ]] || [[ "$1" == "${REPO_2_NAME:-}" ]] || [[ "$1" == "${REPO_3_NAME:-}" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_capacity() {
|
||||||
|
[[ "$1" =~ ^[1-9][0-9]*$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_docker_cpu() {
|
||||||
|
[[ -z "$1" ]] || [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_docker_memory() {
|
||||||
|
[[ -z "$1" ]] || [[ "$1" =~ ^[0-9]+[kmgKMG]?$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Prompt function — matches configure_env.sh UX (progress counter, default
|
# Prompt function — matches configure_env.sh UX (progress counter, default
|
||||||
# values in yellow brackets, validation loop with red error hints).
|
# values in yellow brackets, validation loop with red error hints).
|
||||||
@@ -91,16 +90,15 @@ TOTAL_PROMPTS=0
|
|||||||
prompt_field() {
|
prompt_field() {
|
||||||
local field_name="$1" # e.g. "name", "ssh_host"
|
local field_name="$1" # e.g. "name", "ssh_host"
|
||||||
local description="$2" # human-readable hint shown in parentheses
|
local description="$2" # human-readable hint shown in parentheses
|
||||||
local validation="$3" # validator key: runner_type, runner_name, ip, port, etc.
|
local validation="$3" # validator key
|
||||||
local default="${4:-}" # pre-filled value shown in [brackets]; Enter accepts it
|
local default="${4:-}" # pre-filled value shown in [brackets]; Enter accepts it
|
||||||
|
local allow_empty="${5:-false}" # true if empty is a valid answer
|
||||||
|
|
||||||
CURRENT_PROMPT=$((CURRENT_PROMPT + 1))
|
CURRENT_PROMPT=$((CURRENT_PROMPT + 1))
|
||||||
|
|
||||||
# Progress indicator in dim grey: [3/21]
|
|
||||||
local progress
|
local progress
|
||||||
progress=$(printf '[%d/%d]' "$CURRENT_PROMPT" "$TOTAL_PROMPTS")
|
progress=$(printf '[%d/%d]' "$CURRENT_PROMPT" "$TOTAL_PROMPTS")
|
||||||
|
|
||||||
# Format: [3/21] field_name (description) [default]: _
|
|
||||||
local prompt_text
|
local prompt_text
|
||||||
if [[ -n "$default" ]]; then
|
if [[ -n "$default" ]]; then
|
||||||
prompt_text=$(printf '%b%s%b %s (%s) %b[%s]%b: ' "$C_DIM" "$progress" "$C_RESET" "$field_name" "$description" "$C_YELLOW" "$default" "$C_RESET")
|
prompt_text=$(printf '%b%s%b %s (%s) %b[%s]%b: ' "$C_DIM" "$progress" "$C_RESET" "$field_name" "$description" "$C_YELLOW" "$default" "$C_RESET")
|
||||||
@@ -108,32 +106,57 @@ prompt_field() {
|
|||||||
prompt_text=$(printf '%b%s%b %s (%s): ' "$C_DIM" "$progress" "$C_RESET" "$field_name" "$description")
|
prompt_text=$(printf '%b%s%b %s (%s): ' "$C_DIM" "$progress" "$C_RESET" "$field_name" "$description")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validation loop — re-prompts on invalid input until the value passes
|
|
||||||
local value
|
local value
|
||||||
while true; do
|
while true; do
|
||||||
printf '%b' "$prompt_text"
|
printf '%b' "$prompt_text"
|
||||||
read -r value
|
read -r value
|
||||||
|
|
||||||
# Empty input → accept the default (if one exists)
|
|
||||||
if [[ -z "$value" ]]; then
|
if [[ -z "$value" ]]; then
|
||||||
value="$default"
|
value="$default"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reject empty when no default was available
|
# Allow empty when explicitly permitted (optional fields)
|
||||||
|
if [[ -z "$value" ]] && [[ "$allow_empty" == "true" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$value" ]]; then
|
if [[ -z "$value" ]]; then
|
||||||
printf '%b Invalid: value cannot be empty%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid: value cannot be empty%b\n' "$C_RED" "$C_RESET"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Dispatch to the appropriate validator
|
|
||||||
case "$validation" in
|
case "$validation" in
|
||||||
|
runner_name)
|
||||||
|
if validate_runner_name "$value"; then break; fi
|
||||||
|
printf '%b Invalid: alphanumeric, hyphens, and underscores only%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
runner_type)
|
runner_type)
|
||||||
if validate_runner_type "$value"; then break; fi
|
if validate_runner_type "$value"; then break; fi
|
||||||
printf '%b Invalid: must be "docker" or "native"%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid: must be "docker" or "native"%b\n' "$C_RED" "$C_RESET"
|
||||||
;;
|
;;
|
||||||
runner_name)
|
runner_host)
|
||||||
if validate_runner_name "$value"; then break; fi
|
if validate_runner_host "$value"; then break; fi
|
||||||
printf '%b Invalid: alphanumeric, hyphens, and underscores only%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid: must be unraid, fedora, local, or custom%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
runner_path)
|
||||||
|
if validate_runner_path "$value"; then break; fi
|
||||||
|
printf '%b Invalid path (must start with / or ~/)%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
runner_repos)
|
||||||
|
if validate_runner_repos "$value"; then break; fi
|
||||||
|
printf '%b Invalid: must be "all" or a known repo name%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
capacity)
|
||||||
|
if validate_capacity "$value"; then break; fi
|
||||||
|
printf '%b Invalid: must be a positive integer (>= 1)%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
docker_cpu)
|
||||||
|
if validate_docker_cpu "$value"; then break; fi
|
||||||
|
printf '%b Invalid: must be a positive number (e.g. 2.0, 0.5) or empty%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
docker_memory)
|
||||||
|
if validate_docker_memory "$value"; then break; fi
|
||||||
|
printf '%b Invalid: must be Docker memory format (e.g. 2g, 512m) or empty%b\n' "$C_RED" "$C_RESET"
|
||||||
;;
|
;;
|
||||||
ip)
|
ip)
|
||||||
if validate_ip "$value"; then break; fi
|
if validate_ip "$value"; then break; fi
|
||||||
@@ -143,9 +166,9 @@ prompt_field() {
|
|||||||
if validate_port "$value"; then break; fi
|
if validate_port "$value"; then break; fi
|
||||||
printf '%b Invalid port (expected: 1-65535)%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid port (expected: 1-65535)%b\n' "$C_RED" "$C_RESET"
|
||||||
;;
|
;;
|
||||||
runner_path)
|
optional_path)
|
||||||
if validate_runner_path "$value"; then break; fi
|
if validate_optional_path "$value"; then break; fi
|
||||||
printf '%b Invalid path (must start with / or ~/)%b\n' "$C_RED" "$C_RESET"
|
printf '%b Invalid path (must start with / or ~/ or be empty)%b\n' "$C_RED" "$C_RESET"
|
||||||
;;
|
;;
|
||||||
nonempty|*)
|
nonempty|*)
|
||||||
if validate_nonempty "$value"; then break; fi
|
if validate_nonempty "$value"; then break; fi
|
||||||
@@ -154,7 +177,6 @@ prompt_field() {
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Return the validated value via global (bash has no return-by-value)
|
|
||||||
PROMPT_RESULT="$value"
|
PROMPT_RESULT="$value"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +188,7 @@ printf '\n%b╔═════════════════════
|
|||||||
printf '%b║ Gitea Migration — Runner Setup ║%b\n' "$C_BOLD" "$C_RESET"
|
printf '%b║ Gitea Migration — Runner Setup ║%b\n' "$C_BOLD" "$C_RESET"
|
||||||
printf '%b╚══════════════════════════════════════════════════════════╝%b\n\n' "$C_BOLD" "$C_RESET"
|
printf '%b╚══════════════════════════════════════════════════════════╝%b\n\n' "$C_BOLD" "$C_RESET"
|
||||||
printf 'Press Enter to keep the current value shown in [brackets].\n'
|
printf 'Press Enter to keep the current value shown in [brackets].\n'
|
||||||
|
printf 'Defaults are pulled from .env variables.\n'
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Ask how many runners to configure.
|
# Ask how many runners to configure.
|
||||||
@@ -187,47 +210,28 @@ if ! [[ "$runner_count" =~ ^[0-9]+$ ]] || [[ "$runner_count" -lt 1 ]]; then
|
|||||||
runner_count=$local_default
|
runner_count=$local_default
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Each runner has 7 fields. Native runners auto-fill 3 (host/user/port) but
|
# Prompt count per runner:
|
||||||
# we still count them toward the total so the progress bar stays consistent.
|
# name(1) + host(1) + type(1) + data_path(1) + labels(1) + default_image(1) +
|
||||||
TOTAL_PROMPTS=$((runner_count * 7))
|
# repos(1) + capacity(1) + cpu(1) + memory(1) = 10
|
||||||
|
# Custom host adds: ssh_host(1) + ssh_user(1) + ssh_port(1) + ssh_key(1) = 4
|
||||||
|
# We estimate max 10 per runner for progress display
|
||||||
|
TOTAL_PROMPTS=$((runner_count * 10))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Collect runner definitions
|
# Collect runner definitions
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Storage arrays for collected values
|
# Storage arrays for collected INI data
|
||||||
COLLECTED_NAMES=()
|
COLLECTED_NAMES=()
|
||||||
COLLECTED_HOSTS=()
|
declare -A COLLECTED_DATA # COLLECTED_DATA["name:key"] = value
|
||||||
COLLECTED_USERS=()
|
|
||||||
COLLECTED_PORTS=()
|
|
||||||
COLLECTED_PATHS=()
|
|
||||||
COLLECTED_LABELS=()
|
|
||||||
COLLECTED_TYPES=()
|
|
||||||
|
|
||||||
for ((i = 0; i < runner_count; i++)); do
|
for ((i = 0; i < runner_count; i++)); do
|
||||||
runner_num=$((i + 1))
|
runner_num=$((i + 1))
|
||||||
printf '\n%b── RUNNER %d OF %d ──────────────────────────────────────────%b\n' "$C_BOLD" "$runner_num" "$runner_count" "$C_RESET"
|
printf '\n%b── RUNNER %d OF %d ──────────────────────────────────────────%b\n' "$C_BOLD" "$runner_num" "$runner_count" "$C_RESET"
|
||||||
|
|
||||||
# Defaults from existing config (if available)
|
# Existing defaults from previous runners.conf (if available)
|
||||||
ex_type="${EXISTING_TYPES[$i]:-}"
|
ex_name="${EXISTING_SECTIONS[$i]:-}"
|
||||||
ex_name="${EXISTING_NAMES[$i]:-}"
|
|
||||||
ex_host="${EXISTING_HOSTS[$i]:-}"
|
|
||||||
ex_user="${EXISTING_USERS[$i]:-}"
|
|
||||||
ex_port="${EXISTING_PORTS[$i]:-}"
|
|
||||||
ex_path="${EXISTING_PATHS[$i]:-}"
|
|
||||||
ex_labels="${EXISTING_LABELS[$i]:-}"
|
|
||||||
|
|
||||||
# --- type (asked first — drives conditionals below) ---
|
# --- name ---
|
||||||
# Like SSL_MODE in configure_env.sh, type determines which fields are prompted
|
|
||||||
# vs auto-filled. Docker runners need SSH creds; native runners run locally.
|
|
||||||
type_default="$ex_type"
|
|
||||||
if [[ -z "$type_default" ]]; then
|
|
||||||
# Smart default: first two runners are docker (Unraid/Fedora), third is native (MacBook)
|
|
||||||
if [[ $i -lt 2 ]]; then type_default="docker"; else type_default="native"; fi
|
|
||||||
fi
|
|
||||||
prompt_field "type" "docker (Linux) or native (macOS)" "runner_type" "$type_default"
|
|
||||||
r_type="$PROMPT_RESULT"
|
|
||||||
|
|
||||||
# --- name (shown in Gitea admin panel) ---
|
|
||||||
name_default="$ex_name"
|
name_default="$ex_name"
|
||||||
if [[ -z "$name_default" ]]; then
|
if [[ -z "$name_default" ]]; then
|
||||||
case $i in
|
case $i in
|
||||||
@@ -237,120 +241,229 @@ for ((i = 0; i < runner_count; i++)); do
|
|||||||
*) name_default="runner-${runner_num}" ;;
|
*) name_default="runner-${runner_num}" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
prompt_field "name" "display name in Gitea admin" "runner_name" "$name_default"
|
prompt_field "name" "runner display name in Gitea" "runner_name" "$name_default"
|
||||||
r_name="$PROMPT_RESULT"
|
r_name="$PROMPT_RESULT"
|
||||||
|
COLLECTED_NAMES+=("$r_name")
|
||||||
|
|
||||||
# --- Conditional SSH fields ---
|
# --- host ---
|
||||||
# Native runners execute on the local macOS machine — no SSH needed.
|
host_default=""
|
||||||
# Docker runners deploy to a remote Linux host via SSH.
|
if [[ -n "$ex_name" ]]; then
|
||||||
if [[ "$r_type" == "native" ]]; then
|
host_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "host" "")
|
||||||
r_host="local"
|
|
||||||
r_user="_"
|
|
||||||
r_port="_"
|
|
||||||
printf '%b → ssh_host=local, ssh_user=_, ssh_port=_ (native runner)%b\n' "$C_DIM" "$C_RESET"
|
|
||||||
# Increment counter for the 3 skipped prompts to keep progress accurate
|
|
||||||
CURRENT_PROMPT=$((CURRENT_PROMPT + 3))
|
|
||||||
else
|
|
||||||
# --- ssh_host (IP of remote Linux host) ---
|
|
||||||
prompt_field "ssh_host" "IP address of remote host" "ip" "${ex_host:-}"
|
|
||||||
r_host="$PROMPT_RESULT"
|
|
||||||
|
|
||||||
# --- ssh_user ---
|
|
||||||
user_default="$ex_user"
|
|
||||||
if [[ -z "$user_default" ]]; then user_default="root"; fi
|
|
||||||
prompt_field "ssh_user" "SSH username" "nonempty" "$user_default"
|
|
||||||
r_user="$PROMPT_RESULT"
|
|
||||||
|
|
||||||
# --- ssh_port ---
|
|
||||||
port_default="$ex_port"
|
|
||||||
if [[ -z "$port_default" ]]; then port_default="22"; fi
|
|
||||||
prompt_field "ssh_port" "SSH port" "port" "$port_default"
|
|
||||||
r_port="$PROMPT_RESULT"
|
|
||||||
fi
|
fi
|
||||||
|
if [[ -z "$host_default" ]]; then
|
||||||
|
case $i in
|
||||||
|
0) host_default="unraid" ;;
|
||||||
|
1) host_default="fedora" ;;
|
||||||
|
2) host_default="local" ;;
|
||||||
|
*) host_default="unraid" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
prompt_field "host" "unraid, fedora, local, or custom" "runner_host" "$host_default"
|
||||||
|
r_host="$PROMPT_RESULT"
|
||||||
|
COLLECTED_DATA["${r_name}:host"]="$r_host"
|
||||||
|
|
||||||
# --- data_path (where runner binary + config + data live) ---
|
# Show resolved SSH details for known hosts
|
||||||
path_default="$ex_path"
|
case "$r_host" in
|
||||||
|
unraid)
|
||||||
|
printf '%b → SSH: %s@%s:%s%b\n' "$C_DIM" "${UNRAID_SSH_USER:-<not set>}" "${UNRAID_IP:-<not set>}" "${UNRAID_SSH_PORT:-22}" "$C_RESET"
|
||||||
|
if [[ -n "${UNRAID_SSH_KEY:-}" ]]; then
|
||||||
|
printf '%b → SSH key: %s%b\n' "$C_DIM" "${UNRAID_SSH_KEY}" "$C_RESET"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
printf '%b → SSH: %s@%s:%s%b\n' "$C_DIM" "${FEDORA_SSH_USER:-<not set>}" "${FEDORA_IP:-<not set>}" "${FEDORA_SSH_PORT:-22}" "$C_RESET"
|
||||||
|
if [[ -n "${FEDORA_SSH_KEY:-}" ]]; then
|
||||||
|
printf '%b → SSH key: %s%b\n' "$C_DIM" "${FEDORA_SSH_KEY}" "$C_RESET"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
local)
|
||||||
|
printf '%b → Runs on this machine (no SSH)%b\n' "$C_DIM" "$C_RESET"
|
||||||
|
;;
|
||||||
|
custom)
|
||||||
|
# Prompt for custom SSH details
|
||||||
|
ex_ssh_host=""
|
||||||
|
ex_ssh_user=""
|
||||||
|
ex_ssh_port=""
|
||||||
|
ex_ssh_key=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
ex_ssh_host=$(ini_get "$RUNNERS_CONF" "$ex_name" "ssh_host" "")
|
||||||
|
ex_ssh_user=$(ini_get "$RUNNERS_CONF" "$ex_name" "ssh_user" "")
|
||||||
|
ex_ssh_port=$(ini_get "$RUNNERS_CONF" "$ex_name" "ssh_port" "22")
|
||||||
|
ex_ssh_key=$(ini_get "$RUNNERS_CONF" "$ex_name" "ssh_key" "")
|
||||||
|
fi
|
||||||
|
prompt_field "ssh_host" "IP address of remote host" "ip" "$ex_ssh_host"
|
||||||
|
COLLECTED_DATA["${r_name}:ssh_host"]="$PROMPT_RESULT"
|
||||||
|
prompt_field "ssh_user" "SSH username" "nonempty" "${ex_ssh_user:-root}"
|
||||||
|
COLLECTED_DATA["${r_name}:ssh_user"]="$PROMPT_RESULT"
|
||||||
|
prompt_field "ssh_port" "SSH port" "port" "${ex_ssh_port:-22}"
|
||||||
|
COLLECTED_DATA["${r_name}:ssh_port"]="$PROMPT_RESULT"
|
||||||
|
prompt_field "ssh_key" "path to SSH key (empty = ssh-agent)" "optional_path" "$ex_ssh_key" "true"
|
||||||
|
COLLECTED_DATA["${r_name}:ssh_key"]="$PROMPT_RESULT"
|
||||||
|
# Adjust total prompts (added 4 custom fields, but we already allocated 10)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- type ---
|
||||||
|
type_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
type_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "type" "")
|
||||||
|
fi
|
||||||
|
if [[ -z "$type_default" ]]; then
|
||||||
|
if [[ "$r_host" == "local" ]]; then type_default="native"; else type_default="docker"; fi
|
||||||
|
fi
|
||||||
|
prompt_field "type" "docker (Linux) or native (macOS)" "runner_type" "$type_default"
|
||||||
|
r_type="$PROMPT_RESULT"
|
||||||
|
COLLECTED_DATA["${r_name}:type"]="$r_type"
|
||||||
|
|
||||||
|
# --- data_path ---
|
||||||
|
path_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
path_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "data_path" "")
|
||||||
|
fi
|
||||||
if [[ -z "$path_default" ]]; then
|
if [[ -z "$path_default" ]]; then
|
||||||
if [[ "$r_type" == "native" ]]; then
|
if [[ "$r_type" == "native" ]]; then
|
||||||
# shellcheck disable=SC2088 # literal tilde — expanded by manage_runner.sh
|
path_default="${LOCAL_RUNNER_DATA_PATH:-~/gitea-runner}"
|
||||||
path_default="~/gitea-runner"
|
|
||||||
else
|
else
|
||||||
path_default="/mnt/nvme/gitea-runner" # typical Unraid/Fedora NVMe mount
|
path_default="${RUNNER_DEFAULT_DATA_PATH:-/mnt/nvme/gitea-runner}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
prompt_field "data_path" "absolute path for runner data" "runner_path" "$path_default"
|
prompt_field "data_path" "absolute path for runner data" "runner_path" "$path_default"
|
||||||
r_path="$PROMPT_RESULT"
|
COLLECTED_DATA["${r_name}:data_path"]="$PROMPT_RESULT"
|
||||||
|
|
||||||
# --- labels (matched by workflow `runs-on:` directives) ---
|
# --- labels ---
|
||||||
labels_default="$ex_labels"
|
labels_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
labels_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "labels" "")
|
||||||
|
fi
|
||||||
if [[ -z "$labels_default" ]]; then
|
if [[ -z "$labels_default" ]]; then
|
||||||
if [[ "$r_type" == "native" ]]; then labels_default="macos"; else labels_default="linux"; fi
|
if [[ "$r_type" == "native" ]]; then labels_default="macos"; else labels_default="linux"; fi
|
||||||
fi
|
fi
|
||||||
prompt_field "labels" "workflow runs-on labels" "nonempty" "$labels_default"
|
prompt_field "labels" "workflow runs-on value" "nonempty" "$labels_default"
|
||||||
r_labels="$PROMPT_RESULT"
|
COLLECTED_DATA["${r_name}:labels"]="$PROMPT_RESULT"
|
||||||
|
|
||||||
# Append this runner's values to the collection arrays
|
# --- default_image (skip for native) ---
|
||||||
COLLECTED_NAMES+=("$r_name")
|
if [[ "$r_type" == "docker" ]]; then
|
||||||
COLLECTED_HOSTS+=("$r_host")
|
image_default=""
|
||||||
COLLECTED_USERS+=("$r_user")
|
if [[ -n "$ex_name" ]]; then
|
||||||
COLLECTED_PORTS+=("$r_port")
|
image_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "default_image" "")
|
||||||
COLLECTED_PATHS+=("$r_path")
|
fi
|
||||||
COLLECTED_LABELS+=("$r_labels")
|
if [[ -z "$image_default" ]]; then
|
||||||
COLLECTED_TYPES+=("$r_type")
|
image_default="${RUNNER_DEFAULT_IMAGE:-catthehacker/ubuntu:act-latest}"
|
||||||
|
fi
|
||||||
|
prompt_field "default_image" "Docker image for job execution" "nonempty" "$image_default"
|
||||||
|
COLLECTED_DATA["${r_name}:default_image"]="$PROMPT_RESULT"
|
||||||
|
else
|
||||||
|
COLLECTED_DATA["${r_name}:default_image"]=""
|
||||||
|
CURRENT_PROMPT=$((CURRENT_PROMPT + 1)) # skip but count for progress
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- repos ---
|
||||||
|
repos_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
repos_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "repos" "all")
|
||||||
|
fi
|
||||||
|
if [[ -z "$repos_default" ]]; then repos_default="all"; fi
|
||||||
|
# Build hint with known repo names
|
||||||
|
repos_hint="token scope: all"
|
||||||
|
for var in REPO_1_NAME REPO_2_NAME REPO_3_NAME; do
|
||||||
|
if [[ -n "${!var:-}" ]]; then
|
||||||
|
repos_hint="${repos_hint}, ${!var}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
prompt_field "repos" "$repos_hint" "runner_repos" "$repos_default"
|
||||||
|
COLLECTED_DATA["${r_name}:repos"]="$PROMPT_RESULT"
|
||||||
|
|
||||||
|
# --- capacity ---
|
||||||
|
cap_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
cap_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "capacity" "")
|
||||||
|
fi
|
||||||
|
if [[ -z "$cap_default" ]]; then
|
||||||
|
cap_default="${RUNNER_DEFAULT_CAPACITY:-1}"
|
||||||
|
fi
|
||||||
|
prompt_field "capacity" "max concurrent jobs (>= 1)" "capacity" "$cap_default"
|
||||||
|
COLLECTED_DATA["${r_name}:capacity"]="$PROMPT_RESULT"
|
||||||
|
|
||||||
|
# --- cpu (skip for native) ---
|
||||||
|
if [[ "$r_type" == "docker" ]]; then
|
||||||
|
cpu_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
cpu_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "cpu" "")
|
||||||
|
fi
|
||||||
|
prompt_field "cpu" "Docker CPU limit (e.g. 2.0, empty = no limit)" "docker_cpu" "$cpu_default" "true"
|
||||||
|
COLLECTED_DATA["${r_name}:cpu"]="$PROMPT_RESULT"
|
||||||
|
else
|
||||||
|
COLLECTED_DATA["${r_name}:cpu"]=""
|
||||||
|
CURRENT_PROMPT=$((CURRENT_PROMPT + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- memory (skip for native) ---
|
||||||
|
if [[ "$r_type" == "docker" ]]; then
|
||||||
|
mem_default=""
|
||||||
|
if [[ -n "$ex_name" ]]; then
|
||||||
|
mem_default=$(ini_get "$RUNNERS_CONF" "$ex_name" "memory" "")
|
||||||
|
fi
|
||||||
|
prompt_field "memory" "Docker memory limit (e.g. 2g, empty = no limit)" "docker_memory" "$mem_default" "true"
|
||||||
|
COLLECTED_DATA["${r_name}:memory"]="$PROMPT_RESULT"
|
||||||
|
else
|
||||||
|
COLLECTED_DATA["${r_name}:memory"]=""
|
||||||
|
CURRENT_PROMPT=$((CURRENT_PROMPT + 1))
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Write runners.conf — overwrites any existing file with the header comment
|
# Write runners.conf — INI format with header comment block
|
||||||
# block and one pipe-delimited line per runner.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
cat > "$RUNNERS_CONF" <<'HEADER'
|
cat > "$RUNNERS_CONF" <<'HEADER'
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# runners.conf — Gitea Actions Runner Definitions
|
# runners.conf — Gitea Actions Runner Definitions (INI format)
|
||||||
# Generated by setup/configure_runners.sh — edit manually or re-run wizard.
|
# Generated by setup/configure_runners.sh — edit manually or re-run wizard.
|
||||||
# Use manage_runner.sh to add/remove runners dynamically.
|
# Use manage_runner.sh to add/remove runners dynamically.
|
||||||
|
# See runners.conf.example for field reference.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
#
|
|
||||||
# FORMAT: name|ssh_host|ssh_user|ssh_port|data_path|labels|type
|
|
||||||
#
|
|
||||||
# name — Display name in Gitea admin panel
|
|
||||||
# ssh_host — IP address or hostname (for local machine, use "local")
|
|
||||||
# ssh_user — SSH username (ignored if ssh_host is "local")
|
|
||||||
# ssh_port — SSH port (ignored if ssh_host is "local")
|
|
||||||
# data_path — Absolute path for runner binary + data on that machine
|
|
||||||
# labels — Comma-separated runner labels (used in workflow runs-on)
|
|
||||||
# type — "docker" (Linux, runs jobs in containers) or "native" (macOS, runs jobs on host)
|
|
||||||
#
|
|
||||||
HEADER
|
HEADER
|
||||||
|
|
||||||
for ((i = 0; i < ${#COLLECTED_NAMES[@]}; i++)); do
|
for r_name in "${COLLECTED_NAMES[@]}"; do
|
||||||
printf '%s|%s|%s|%s|%s|%s|%s\n' \
|
printf '[%s]\n' "$r_name" >> "$RUNNERS_CONF"
|
||||||
"${COLLECTED_NAMES[$i]}" \
|
|
||||||
"${COLLECTED_HOSTS[$i]}" \
|
# Write fields in canonical order
|
||||||
"${COLLECTED_USERS[$i]}" \
|
for key in host type data_path labels default_image repos capacity cpu memory; do
|
||||||
"${COLLECTED_PORTS[$i]}" \
|
local_val="${COLLECTED_DATA["${r_name}:${key}"]:-}"
|
||||||
"${COLLECTED_PATHS[$i]}" \
|
printf '%-14s= %s\n' "$key" "$local_val" >> "$RUNNERS_CONF"
|
||||||
"${COLLECTED_LABELS[$i]}" \
|
done
|
||||||
"${COLLECTED_TYPES[$i]}" >> "$RUNNERS_CONF"
|
|
||||||
|
# Write custom SSH fields if host=custom
|
||||||
|
if [[ "${COLLECTED_DATA["${r_name}:host"]:-}" == "custom" ]]; then
|
||||||
|
for key in ssh_host ssh_user ssh_port ssh_key; do
|
||||||
|
local_val="${COLLECTED_DATA["${r_name}:${key}"]:-}"
|
||||||
|
printf '%-14s= %s\n' "$key" "$local_val" >> "$RUNNERS_CONF"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n' >> "$RUNNERS_CONF"
|
||||||
done
|
done
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Summary — green border box + table matching manage_runner.sh list format
|
# Summary — green border box + table
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
printf '\n%b╔══════════════════════════════════════════════════════════╗%b\n' "$C_GREEN" "$C_RESET"
|
printf '\n%b╔══════════════════════════════════════════════════════════╗%b\n' "$C_GREEN" "$C_RESET"
|
||||||
printf '%b║ Runner Configuration Complete ║%b\n' "$C_GREEN" "$C_RESET"
|
printf '%b║ Runner Configuration Complete ║%b\n' "$C_GREEN" "$C_RESET"
|
||||||
printf '%b╚══════════════════════════════════════════════════════════╝%b\n\n' "$C_GREEN" "$C_RESET"
|
printf '%b╚══════════════════════════════════════════════════════════╝%b\n\n' "$C_GREEN" "$C_RESET"
|
||||||
|
|
||||||
printf '%bRunners configured:%b %d\n\n' "$C_BOLD" "$C_RESET" "${#COLLECTED_NAMES[@]}"
|
printf '%bRunners configured:%b %d\n\n' "$C_BOLD" "$C_RESET" "${#COLLECTED_NAMES[@]}"
|
||||||
printf '%-20s %-16s %-10s %-8s %-24s\n' "NAME" "HOST" "LABELS" "TYPE" "DATA PATH"
|
printf '%-20s %-10s %-8s %-10s %-6s %-10s %-24s\n' "NAME" "HOST" "TYPE" "LABELS" "CAP" "REPOS" "DATA PATH"
|
||||||
printf '%-20s %-16s %-10s %-8s %-24s\n' "----" "----" "------" "----" "---------"
|
printf '%-20s %-10s %-8s %-10s %-6s %-10s %-24s\n' "----" "----" "----" "------" "---" "-----" "---------"
|
||||||
|
|
||||||
for ((i = 0; i < ${#COLLECTED_NAMES[@]}; i++)); do
|
for r_name in "${COLLECTED_NAMES[@]}"; do
|
||||||
printf '%-20s %-16s %-10s %-8s %-24s\n' \
|
printf '%-20s %-10s %-8s %-10s %-6s %-10s %-24s\n' \
|
||||||
"${COLLECTED_NAMES[$i]}" \
|
"$r_name" \
|
||||||
"${COLLECTED_HOSTS[$i]}" \
|
"${COLLECTED_DATA["${r_name}:host"]:-}" \
|
||||||
"${COLLECTED_LABELS[$i]}" \
|
"${COLLECTED_DATA["${r_name}:type"]:-}" \
|
||||||
"${COLLECTED_TYPES[$i]}" \
|
"${COLLECTED_DATA["${r_name}:labels"]:-}" \
|
||||||
"${COLLECTED_PATHS[$i]}"
|
"${COLLECTED_DATA["${r_name}:capacity"]:-}" \
|
||||||
|
"${COLLECTED_DATA["${r_name}:repos"]:-}" \
|
||||||
|
"${COLLECTED_DATA["${r_name}:data_path"]:-}"
|
||||||
done
|
done
|
||||||
|
|
||||||
printf '\n Saved to: %s\n\n' "$RUNNERS_CONF"
|
printf '\n Saved to: %s\n\n' "$RUNNERS_CONF"
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
- GITEA_INSTANCE_URL=${GITEA_INTERNAL_URL}
|
- GITEA_INSTANCE_URL=${GITEA_INTERNAL_URL}
|
||||||
- GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}
|
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_REG_TOKEN}
|
||||||
- GITEA_RUNNER_NAME=${RUNNER_NAME}
|
- GITEA_RUNNER_NAME=${RUNNER_NAME}
|
||||||
- GITEA_RUNNER_LABELS=${RUNNER_LABELS}
|
- GITEA_RUNNER_LABELS=${RUNNER_LABELS_CSV}
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ${RUNNER_DATA_PATH}:/data
|
- ${RUNNER_DATA_PATH}:/data
|
||||||
|
${RUNNER_DEPLOY_RESOURCES}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ log:
|
|||||||
runner:
|
runner:
|
||||||
name: ${RUNNER_NAME}
|
name: ${RUNNER_NAME}
|
||||||
labels:
|
labels:
|
||||||
- "${RUNNER_LABELS}:docker://node:20-bookworm"
|
${RUNNER_LABELS_YAML}
|
||||||
capacity: 1
|
capacity: ${RUNNER_CAPACITY}
|
||||||
timeout: 3h
|
timeout: 3h
|
||||||
insecure: false
|
insecure: false
|
||||||
fetch_timeout: 5s
|
fetch_timeout: 5s
|
||||||
|
|||||||
Reference in New Issue
Block a user