#!/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"