From fcd966f97d224684bb4af2b2faeba26d4c4a5c80 Mon Sep 17 00:00:00 2001 From: S Date: Sat, 28 Feb 2026 22:21:14 -0500 Subject: [PATCH] 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 --- setup/configure_runners.sh | 357 +++++++++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100755 setup/configure_runners.sh diff --git a/setup/configure_runners.sh b/setup/configure_runners.sh new file mode 100755 index 0000000..a0b0084 --- /dev/null +++ b/setup/configure_runners.sh @@ -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"