#!/usr/bin/env bash set -euo pipefail # ============================================================================= # setup/configure_env.sh — Interactive .env configuration wizard # Prompts for every required variable with validation and progress tracking. # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="${SCRIPT_DIR}/.." ENV_FILE="${PROJECT_ROOT}/.env" ENV_EXAMPLE="${PROJECT_ROOT}/.env.example" # shellcheck source=../lib/common.sh # shellcheck disable=SC1091 source "${PROJECT_ROOT}/lib/common.sh" # Colors 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_CYAN='\033[0;36m'; C_DIM='\033[2m' else C_RESET=''; C_BOLD=''; C_GREEN=''; C_YELLOW=''; C_RED=''; C_CYAN=''; C_DIM='' fi # --------------------------------------------------------------------------- # Initialize .env if it doesn't exist # --------------------------------------------------------------------------- if [[ ! -f "$ENV_FILE" ]]; then if [[ ! -f "$ENV_EXAMPLE" ]]; then printf '%b[ERROR]%b .env.example not found at %s\n' "$C_RED" "$C_RESET" "$ENV_EXAMPLE" >&2 exit 1 fi cp "$ENV_EXAMPLE" "$ENV_FILE" printf '%b[INFO]%b Created .env from .env.example\n' "$C_CYAN" "$C_RESET" >&2 fi # --------------------------------------------------------------------------- # Load current values # --------------------------------------------------------------------------- # Lookup a variable's current value from .env (bash 3.2 compatible — no assoc arrays) get_env_val() { local key="$1" default="${2:-}" local line val line=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | head -1) || true if [[ -z "$line" ]]; then printf '%s' "$default" return 0 fi val="${line#*=}" # Strip inline comment val="${val%%#*}" # Trim trailing whitespace val="${val%"${val##*[![:space:]]}"}" printf '%s' "$val" } # --------------------------------------------------------------------------- # Validation functions — sourced from lib/common.sh (validate_ip, validate_port, # validate_email, validate_path, validate_url, validate_bool, validate_integer, # validate_nonempty, validate_password, validate_ssl_mode) # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Prompt function # --------------------------------------------------------------------------- TOTAL_PROMPTS=57 CURRENT_PROMPT=0 LAST_SECTION="" # Collected SSL_MODE for conditional logic COLLECTED_SSL_MODE="" prompt_var() { local var_name="$1" local description="$2" local validation="$3" local default="${4:-}" local section="$5" CURRENT_PROMPT=$((CURRENT_PROMPT + 1)) # Print section header when entering a new section if [[ "$section" != "$LAST_SECTION" ]]; then LAST_SECTION="$section" printf '\n%b── %s ──────────────────────────────────────────%b\n' "$C_BOLD" "$section" "$C_RESET" fi # Determine current value (from .env or default) local current current=$(get_env_val "$var_name" "$default") # Build prompt local progress progress=$(printf '[%d/%d]' "$CURRENT_PROMPT" "$TOTAL_PROMPTS") local prompt_text if [[ -n "$current" ]]; then # Mask passwords if [[ "$validation" == "password" ]]; then prompt_text=$(printf '%b%s%b %s (%s) %b[****]%b: ' "$C_DIM" "$progress" "$C_RESET" "$var_name" "$description" "$C_YELLOW" "$C_RESET") else prompt_text=$(printf '%b%s%b %s (%s) %b[%s]%b: ' "$C_DIM" "$progress" "$C_RESET" "$var_name" "$description" "$C_YELLOW" "$current" "$C_RESET") fi else prompt_text=$(printf '%b%s%b %s (%s): ' "$C_DIM" "$progress" "$C_RESET" "$var_name" "$description") fi # Read input with validation loop local value while true; do printf '%b' "$prompt_text" read -r value # Use current/default if empty if [[ -z "$value" ]]; then value="$current" fi # Validate if [[ -z "$value" ]] && [[ "$validation" != "optional" ]]; then printf '%b Invalid: value cannot be empty%b\n' "$C_RED" "$C_RESET" continue fi # Optional fields accept any value including empty if [[ "$validation" == "optional" ]]; then break fi case "$validation" in 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" ;; email) if validate_email "$value"; then break; fi printf '%b Invalid email (must contain @)%b\n' "$C_RED" "$C_RESET" ;; path) if validate_path "$value"; then break; fi printf '%b Invalid path (must start with /)%b\n' "$C_RED" "$C_RESET" ;; url) if validate_url "$value"; then break; fi printf '%b Invalid URL (must start with http:// or https://)%b\n' "$C_RED" "$C_RESET" ;; bool) if validate_bool "$value"; then break; fi printf '%b Invalid: must be true or false%b\n' "$C_RED" "$C_RESET" ;; integer) if validate_integer "$value"; then break; fi 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) if validate_password "$value"; then break; fi printf '%b Invalid: password must be at least 8 characters%b\n' "$C_RED" "$C_RESET" ;; ssl_mode) if validate_ssl_mode "$value"; then break; fi printf '%b Invalid: must be "letsencrypt" or "existing"%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 # Write to .env write_env_var "$var_name" "$value" # Track SSL mode for conditional prompts if [[ "$var_name" == "SSL_MODE" ]]; then COLLECTED_SSL_MODE="$value" fi } # --------------------------------------------------------------------------- # Write a variable to .env file (preserving structure) # --------------------------------------------------------------------------- write_env_var() { local key="$1" value="$2" if grep -q "^${key}=" "$ENV_FILE"; then # Replace existing line (preserve inline comment if present in .env.example) local comment="" if grep -q "^${key}=" "$ENV_EXAMPLE" 2>/dev/null; then comment=$(grep "^${key}=" "$ENV_EXAMPLE" | sed "s/^${key}=[^#]*//" | sed 's/^[[:space:]]*//') fi if [[ -n "$comment" ]]; then # Pad value to align comment local padded padded=$(printf '%-30s' "$value") sed -i.bak "s|^${key}=.*|${key}=${padded}${comment}|" "$ENV_FILE" else sed -i.bak "s|^${key}=.*|${key}=${value}|" "$ENV_FILE" fi rm -f "${ENV_FILE}.bak" else printf '%s=%s\n' "$key" "$value" >> "$ENV_FILE" fi } # =========================================================================== # Main — walk through all variables in section order # =========================================================================== printf '\n%b╔══════════════════════════════════════════════════════════╗%b\n' "$C_BOLD" "$C_RESET" printf '%b║ Gitea Migration — Environment 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' printf 'Auto-populated variables (tokens) will be skipped.\n' # --- UNRAID SERVER --- prompt_var "UNRAID_IP" "Static IP of Unraid server" ip "" "UNRAID SERVER" prompt_var "UNRAID_SSH_USER" "SSH username for Unraid" nonempty "" "UNRAID SERVER" prompt_var "UNRAID_SSH_PORT" "SSH port" port "22" "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_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 --- prompt_var "FEDORA_IP" "Static IP of Fedora server" ip "" "FEDORA SERVER" prompt_var "FEDORA_SSH_USER" "SSH username for Fedora" nonempty "" "FEDORA SERVER" prompt_var "FEDORA_SSH_PORT" "SSH port" port "22" "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_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 --- prompt_var "GITEA_ADMIN_USER" "Admin username (same on both instances)" nonempty "" "GITEA SHARED CREDENTIALS" prompt_var "GITEA_ADMIN_PASSWORD" "Admin password (min 8 chars)" password "" "GITEA SHARED CREDENTIALS" prompt_var "GITEA_ADMIN_EMAIL" "Admin email" email "" "GITEA SHARED CREDENTIALS" prompt_var "GITEA_ORG_NAME" "Organization name (e.g. mifi-llc)" nonempty "" "GITEA SHARED CREDENTIALS" prompt_var "GITEA_INSTANCE_NAME" "Display name for Gitea (e.g. MIFI Git)" nonempty "" "GITEA SHARED CREDENTIALS" prompt_var "GITEA_DB_TYPE" "Database type" nonempty "sqlite3" "GITEA SHARED CREDENTIALS" prompt_var "GITEA_VERSION" "Gitea Docker image tag" nonempty "1.23" "GITEA SHARED CREDENTIALS" prompt_var "ACT_RUNNER_VERSION" "act_runner version" nonempty "0.2.11" "GITEA SHARED CREDENTIALS" # --- GITEA PRIMARY INSTANCE --- prompt_var "GITEA_DOMAIN" "Public domain pointing to Unraid" nonempty "" "GITEA PRIMARY INSTANCE" prompt_var "GITEA_INTERNAL_URL" "Internal URL (e.g. http://IP:3000)" url "" "GITEA PRIMARY INSTANCE" # --- GITEA BACKUP INSTANCE --- prompt_var "GITEA_BACKUP_INTERNAL_URL" "Internal URL of Fedora Gitea" url "" "GITEA BACKUP INSTANCE" prompt_var "GITEA_BACKUP_MIRROR_INTERVAL" "How often Fedora pulls from Unraid" nonempty "8h" "GITEA BACKUP INSTANCE" prompt_var "BACKUP_STORAGE_PATH" "Path on Fedora for backup archives" path "" "GITEA BACKUP INSTANCE" prompt_var "BACKUP_RETENTION_COUNT" "Number of backup archives to keep" integer "5" "GITEA BACKUP INSTANCE" # --- REPOSITORIES --- prompt_var "GITHUB_USERNAME" "GitHub username or org name" nonempty "" "REPOSITORIES" prompt_var "GITHUB_TOKEN" "GitHub personal access token (repo read)" nonempty "" "REPOSITORIES" prompt_var "REPO_1_NAME" "First repo name (exact match)" nonempty "" "REPOSITORIES" prompt_var "REPO_2_NAME" "Second repo name (exact match)" nonempty "" "REPOSITORIES" prompt_var "REPO_3_NAME" "Third repo name (exact match)" nonempty "" "REPOSITORIES" prompt_var "MIGRATE_ISSUES" "Migrate GitHub issues" bool "false" "REPOSITORIES" prompt_var "MIGRATE_LABELS" "Migrate GitHub labels" bool "true" "REPOSITORIES" prompt_var "MIGRATE_MILESTONES" "Migrate GitHub milestones" 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 --- 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" # --- NGINX REVERSE PROXY --- prompt_var "NGINX_CONTAINER_NAME" "Name of existing Nginx Docker container" nonempty "" "NGINX REVERSE PROXY" prompt_var "NGINX_CONF_PATH" "Host path to Nginx conf.d directory" path "" "NGINX REVERSE PROXY" prompt_var "SSL_MODE" "SSL mode: letsencrypt or existing" ssl_mode "letsencrypt" "NGINX REVERSE PROXY" # Conditional SSL prompts if [[ "$COLLECTED_SSL_MODE" == "letsencrypt" ]]; then prompt_var "SSL_EMAIL" "Email for Let's Encrypt" email "" "NGINX REVERSE PROXY" # Skip cert path prompts but still count them for progress CURRENT_PROMPT=$((CURRENT_PROMPT + 2)) else # Skip email prompt but count it CURRENT_PROMPT=$((CURRENT_PROMPT + 1)) prompt_var "SSL_CERT_PATH" "Absolute path to SSL cert on Unraid" path "" "NGINX REVERSE PROXY" prompt_var "SSL_KEY_PATH" "Absolute path to SSL key on Unraid" path "" "NGINX REVERSE PROXY" fi # --- BRANCH PROTECTION --- prompt_var "PROTECTED_BRANCH" "Branch to protect across all repos" nonempty "main" "BRANCH PROTECTION" prompt_var "REQUIRE_PR_REVIEW" "Require PR review before merge" bool "false" "BRANCH PROTECTION" prompt_var "REQUIRED_APPROVALS" "Number of approvals required" integer "1" "BRANCH PROTECTION" # --- SECURITY --- prompt_var "SEMGREP_VERSION" "Semgrep OSS version" nonempty "latest" "SECURITY" prompt_var "TRIVY_VERSION" "Trivy version" nonempty "latest" "SECURITY" prompt_var "GITLEAKS_VERSION" "Gitleaks version" nonempty "latest" "SECURITY" prompt_var "SECURITY_FAIL_ON_ERROR" "Block PR merge if security scan fails" bool "true" "SECURITY" # =========================================================================== # Summary # =========================================================================== printf '\n%b╔══════════════════════════════════════════════════════════╗%b\n' "$C_GREEN" "$C_RESET" printf '%b║ Configuration Complete ║%b\n' "$C_GREEN" "$C_RESET" printf '%b╚══════════════════════════════════════════════════════════╝%b\n\n' "$C_GREEN" "$C_RESET" printf '%bSummary:%b\n' "$C_BOLD" "$C_RESET" printf ' Unraid: %s@%s:%s\n' "$(get_env_val UNRAID_SSH_USER)" "$(get_env_val UNRAID_IP)" "$(get_env_val UNRAID_SSH_PORT 22)" printf ' Fedora: %s@%s:%s\n' "$(get_env_val FEDORA_SSH_USER)" "$(get_env_val FEDORA_IP)" "$(get_env_val FEDORA_SSH_PORT 22)" printf ' Gitea: %s (admin: %s, password: ****)\n' "$(get_env_val GITEA_DOMAIN)" "$(get_env_val GITEA_ADMIN_USER)" printf ' Org: %s\n' "$(get_env_val GITEA_ORG_NAME)" printf ' Repos: %s, %s, %s\n' "$(get_env_val REPO_1_NAME)" "$(get_env_val REPO_2_NAME)" "$(get_env_val REPO_3_NAME)" printf ' SSL: %s\n' "${COLLECTED_SSL_MODE}" printf ' .env saved: %s\n\n' "$ENV_FILE" printf 'Next step: run %bsetup/macbook.sh%b to install local prerequisites.\n' "$C_BOLD" "$C_RESET"