feat: add interactive runners.conf configuration wizard
Replaces manual pipe-delimited file editing with a guided setup script matching the configure_env.sh UX pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
357
setup/configure_runners.sh
Executable file
357
setup/configure_runners.sh
Executable file
@@ -0,0 +1,357 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# setup/configure_runners.sh — Interactive runners.conf configuration wizard
|
||||||
|
# Prompts for each runner's fields with validation and progress tracking.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="${SCRIPT_DIR}/.."
|
||||||
|
RUNNERS_CONF="${PROJECT_ROOT}/runners.conf"
|
||||||
|
|
||||||
|
# shellcheck source=../lib/common.sh
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "${PROJECT_ROOT}/lib/common.sh"
|
||||||
|
|
||||||
|
# Colors — only emit ANSI escapes when stdout is a terminal (not piped/redirected)
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
C_RESET='\033[0m'; C_BOLD='\033[1m'; C_GREEN='\033[0;32m'
|
||||||
|
C_YELLOW='\033[0;33m'; C_RED='\033[0;31m'; C_DIM='\033[2m'
|
||||||
|
else
|
||||||
|
C_RESET=''; C_BOLD=''; C_GREEN=''; C_YELLOW=''; C_RED=''; C_DIM=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Load existing runner entries as defaults (idempotent re-runs).
|
||||||
|
# If runners.conf already exists, parse each non-comment line into parallel
|
||||||
|
# arrays so we can pre-fill prompts with previous values.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
EXISTING_NAMES=()
|
||||||
|
EXISTING_HOSTS=()
|
||||||
|
EXISTING_USERS=()
|
||||||
|
EXISTING_PORTS=()
|
||||||
|
EXISTING_PATHS=()
|
||||||
|
EXISTING_LABELS=()
|
||||||
|
EXISTING_TYPES=()
|
||||||
|
|
||||||
|
if [[ -f "$RUNNERS_CONF" ]]; then
|
||||||
|
while IFS='|' read -r name host user port path labels type; do
|
||||||
|
[[ "$name" =~ ^[[:space:]]*# ]] && continue # skip comments
|
||||||
|
[[ -z "$name" ]] && continue # skip blank lines
|
||||||
|
# Trim whitespace from each field (xargs strips leading/trailing spaces)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
EXISTING_COUNT=${#EXISTING_NAMES[@]}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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() {
|
||||||
|
[[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only two runner types exist: docker (Linux containers) and native (macOS binary)
|
||||||
|
validate_runner_type() {
|
||||||
|
[[ "$1" == "docker" || "$1" == "native" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Absolute paths (/) for remote hosts, or ~/ for native macOS runners.
|
||||||
|
# Tilde is stored literally; manage_runner.sh expands it at runtime.
|
||||||
|
validate_runner_path() {
|
||||||
|
# shellcheck disable=SC2088 # tilde intentionally stored as literal string
|
||||||
|
[[ "$1" == /* || "$1" == "~/"* || "$1" == "~" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Prompt function — matches configure_env.sh UX (progress counter, default
|
||||||
|
# values in yellow brackets, validation loop with red error hints).
|
||||||
|
# Sets the global PROMPT_RESULT to the validated user input.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CURRENT_PROMPT=0
|
||||||
|
TOTAL_PROMPTS=0
|
||||||
|
|
||||||
|
prompt_field() {
|
||||||
|
local field_name="$1" # e.g. "name", "ssh_host"
|
||||||
|
local description="$2" # human-readable hint shown in parentheses
|
||||||
|
local validation="$3" # validator key: runner_type, runner_name, ip, port, etc.
|
||||||
|
local default="${4:-}" # pre-filled value shown in [brackets]; Enter accepts it
|
||||||
|
|
||||||
|
CURRENT_PROMPT=$((CURRENT_PROMPT + 1))
|
||||||
|
|
||||||
|
# Progress indicator in dim grey: [3/21]
|
||||||
|
local progress
|
||||||
|
progress=$(printf '[%d/%d]' "$CURRENT_PROMPT" "$TOTAL_PROMPTS")
|
||||||
|
|
||||||
|
# Format: [3/21] field_name (description) [default]: _
|
||||||
|
local prompt_text
|
||||||
|
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")
|
||||||
|
else
|
||||||
|
prompt_text=$(printf '%b%s%b %s (%s): ' "$C_DIM" "$progress" "$C_RESET" "$field_name" "$description")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validation loop — re-prompts on invalid input until the value passes
|
||||||
|
local value
|
||||||
|
while true; do
|
||||||
|
printf '%b' "$prompt_text"
|
||||||
|
read -r value
|
||||||
|
|
||||||
|
# Empty input → accept the default (if one exists)
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
value="$default"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reject empty when no default was available
|
||||||
|
if [[ -z "$value" ]]; then
|
||||||
|
printf '%b Invalid: value cannot be empty%b\n' "$C_RED" "$C_RESET"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Dispatch to the appropriate validator
|
||||||
|
case "$validation" in
|
||||||
|
runner_type)
|
||||||
|
if validate_runner_type "$value"; then break; fi
|
||||||
|
printf '%b Invalid: must be "docker" or "native"%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
runner_name)
|
||||||
|
if validate_runner_name "$value"; then break; fi
|
||||||
|
printf '%b Invalid: alphanumeric, hyphens, and underscores only%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
ip)
|
||||||
|
if validate_ip "$value"; then break; fi
|
||||||
|
printf '%b Invalid IP address format (expected: x.x.x.x)%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
port)
|
||||||
|
if validate_port "$value"; then break; fi
|
||||||
|
printf '%b Invalid port (expected: 1-65535)%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"
|
||||||
|
;;
|
||||||
|
nonempty|*)
|
||||||
|
if validate_nonempty "$value"; then break; fi
|
||||||
|
printf '%b Invalid: value cannot be empty%b\n' "$C_RED" "$C_RESET"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Return the validated value via global (bash has no return-by-value)
|
||||||
|
PROMPT_RESULT="$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Main
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
printf '\n%b╔══════════════════════════════════════════════════════════╗%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 'Press Enter to keep the current value shown in [brackets].\n'
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ask how many runners to configure.
|
||||||
|
# Default is 3 (unraid + fedora + macbook) or the count from existing config.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
local_default=3
|
||||||
|
if [[ $EXISTING_COUNT -gt 0 ]]; then
|
||||||
|
local_default=$EXISTING_COUNT
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n%b── RUNNER COUNT ──────────────────────────────────────────%b\n' "$C_BOLD" "$C_RESET"
|
||||||
|
printf 'How many runners to configure? %b[%s]%b: ' "$C_YELLOW" "$local_default" "$C_RESET"
|
||||||
|
read -r runner_count
|
||||||
|
if [[ -z "$runner_count" ]]; then
|
||||||
|
runner_count=$local_default
|
||||||
|
fi
|
||||||
|
if ! [[ "$runner_count" =~ ^[0-9]+$ ]] || [[ "$runner_count" -lt 1 ]]; then
|
||||||
|
printf '%b Invalid: must be a positive number. Using %d.%b\n' "$C_RED" "$local_default" "$C_RESET"
|
||||||
|
runner_count=$local_default
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Each runner has 7 fields. Native runners auto-fill 3 (host/user/port) but
|
||||||
|
# we still count them toward the total so the progress bar stays consistent.
|
||||||
|
TOTAL_PROMPTS=$((runner_count * 7))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Collect runner definitions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Storage arrays for collected values
|
||||||
|
COLLECTED_NAMES=()
|
||||||
|
COLLECTED_HOSTS=()
|
||||||
|
COLLECTED_USERS=()
|
||||||
|
COLLECTED_PORTS=()
|
||||||
|
COLLECTED_PATHS=()
|
||||||
|
COLLECTED_LABELS=()
|
||||||
|
COLLECTED_TYPES=()
|
||||||
|
|
||||||
|
for ((i = 0; i < runner_count; i++)); do
|
||||||
|
runner_num=$((i + 1))
|
||||||
|
printf '\n%b── RUNNER %d OF %d ──────────────────────────────────────────%b\n' "$C_BOLD" "$runner_num" "$runner_count" "$C_RESET"
|
||||||
|
|
||||||
|
# Defaults from existing config (if available)
|
||||||
|
ex_type="${EXISTING_TYPES[$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) ---
|
||||||
|
# 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"
|
||||||
|
if [[ -z "$name_default" ]]; then
|
||||||
|
case $i in
|
||||||
|
0) name_default="unraid-runner" ;;
|
||||||
|
1) name_default="fedora-runner" ;;
|
||||||
|
2) name_default="macbook-runner" ;;
|
||||||
|
*) name_default="runner-${runner_num}" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
prompt_field "name" "display name in Gitea admin" "runner_name" "$name_default"
|
||||||
|
r_name="$PROMPT_RESULT"
|
||||||
|
|
||||||
|
# --- Conditional SSH fields ---
|
||||||
|
# Native runners execute on the local macOS machine — no SSH needed.
|
||||||
|
# Docker runners deploy to a remote Linux host via SSH.
|
||||||
|
if [[ "$r_type" == "native" ]]; then
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- data_path (where runner binary + config + data live) ---
|
||||||
|
path_default="$ex_path"
|
||||||
|
if [[ -z "$path_default" ]]; then
|
||||||
|
if [[ "$r_type" == "native" ]]; then
|
||||||
|
# shellcheck disable=SC2088 # literal tilde — expanded by manage_runner.sh
|
||||||
|
path_default="~/gitea-runner"
|
||||||
|
else
|
||||||
|
path_default="/mnt/nvme/gitea-runner" # typical Unraid/Fedora NVMe mount
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
prompt_field "data_path" "absolute path for runner data" "runner_path" "$path_default"
|
||||||
|
r_path="$PROMPT_RESULT"
|
||||||
|
|
||||||
|
# --- labels (matched by workflow `runs-on:` directives) ---
|
||||||
|
labels_default="$ex_labels"
|
||||||
|
if [[ -z "$labels_default" ]]; then
|
||||||
|
if [[ "$r_type" == "native" ]]; then labels_default="macos"; else labels_default="linux"; fi
|
||||||
|
fi
|
||||||
|
prompt_field "labels" "workflow runs-on labels" "nonempty" "$labels_default"
|
||||||
|
r_labels="$PROMPT_RESULT"
|
||||||
|
|
||||||
|
# Append this runner's values to the collection arrays
|
||||||
|
COLLECTED_NAMES+=("$r_name")
|
||||||
|
COLLECTED_HOSTS+=("$r_host")
|
||||||
|
COLLECTED_USERS+=("$r_user")
|
||||||
|
COLLECTED_PORTS+=("$r_port")
|
||||||
|
COLLECTED_PATHS+=("$r_path")
|
||||||
|
COLLECTED_LABELS+=("$r_labels")
|
||||||
|
COLLECTED_TYPES+=("$r_type")
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Write runners.conf — overwrites any existing file with the header comment
|
||||||
|
# block and one pipe-delimited line per runner.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
cat > "$RUNNERS_CONF" <<'HEADER'
|
||||||
|
# =============================================================================
|
||||||
|
# runners.conf — Gitea Actions Runner Definitions
|
||||||
|
# Generated by setup/configure_runners.sh — edit manually or re-run wizard.
|
||||||
|
# Use manage_runner.sh to add/remove runners dynamically.
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
for ((i = 0; i < ${#COLLECTED_NAMES[@]}; i++)); do
|
||||||
|
printf '%s|%s|%s|%s|%s|%s|%s\n' \
|
||||||
|
"${COLLECTED_NAMES[$i]}" \
|
||||||
|
"${COLLECTED_HOSTS[$i]}" \
|
||||||
|
"${COLLECTED_USERS[$i]}" \
|
||||||
|
"${COLLECTED_PORTS[$i]}" \
|
||||||
|
"${COLLECTED_PATHS[$i]}" \
|
||||||
|
"${COLLECTED_LABELS[$i]}" \
|
||||||
|
"${COLLECTED_TYPES[$i]}" >> "$RUNNERS_CONF"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# Summary — green border box + table matching manage_runner.sh list format
|
||||||
|
# ===========================================================================
|
||||||
|
printf '\n%b╔══════════════════════════════════════════════════════════╗%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 '%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 %-16s %-10s %-8s %-24s\n' "----" "----" "------" "----" "---------"
|
||||||
|
|
||||||
|
for ((i = 0; i < ${#COLLECTED_NAMES[@]}; i++)); do
|
||||||
|
printf '%-20s %-16s %-10s %-8s %-24s\n' \
|
||||||
|
"${COLLECTED_NAMES[$i]}" \
|
||||||
|
"${COLLECTED_HOSTS[$i]}" \
|
||||||
|
"${COLLECTED_LABELS[$i]}" \
|
||||||
|
"${COLLECTED_TYPES[$i]}" \
|
||||||
|
"${COLLECTED_PATHS[$i]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n Saved to: %s\n\n' "$RUNNERS_CONF"
|
||||||
|
printf 'Next step: run %bsetup/macbook.sh%b to install local prerequisites.\n' "$C_BOLD" "$C_RESET"
|
||||||
Reference in New Issue
Block a user