From 720197bb10b56d74deb28f4c110897054273dc72 Mon Sep 17 00:00:00 2001 From: S Date: Thu, 26 Feb 2026 19:00:13 -0600 Subject: [PATCH] feat: add OS compatibility checks before running platform-specific logic - lib/common.sh: add require_local_os, require_remote_os, require_remote_pkg_manager - setup/macbook.sh: require macOS (Darwin) - setup/unraid.sh: require remote is Linux - setup/fedora.sh: require remote is Linux + has dnf (RPM-based) - manage_runner.sh: native runner add/remove requires macOS - run_all.sh: control plane must be macOS - preflight.sh: 3 new checks (1: local=macOS, 2: Unraid=Linux, 3: Fedora=Linux+dnf) - phase5_migrate_pipelines.sh: fix sed -i to be portable (no macOS-only syntax) Co-Authored-By: Claude Opus 4.6 --- lib/common.sh | 49 ++++++++++++++++ manage_runner.sh | 6 ++ phase5_migrate_pipelines.sh | 9 ++- preflight.sh | 113 +++++++++++++++++++++++++----------- run_all.sh | 5 ++ setup/fedora.sh | 9 +++ setup/macbook.sh | 5 ++ setup/unraid.sh | 6 ++ 8 files changed, 164 insertions(+), 38 deletions(-) diff --git a/lib/common.sh b/lib/common.sh index 647247c..25a70a4 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -123,6 +123,55 @@ require_vars() { done } +# --------------------------------------------------------------------------- +# OS compatibility checks +# --------------------------------------------------------------------------- + +# Verify the local machine is running the expected OS. +# Usage: require_local_os "Darwin" "This script requires macOS" +# os_type: "Darwin" for macOS, "Linux" for Linux +# Checks `uname -s` against the expected value. +require_local_os() { + local expected="$1" msg="${2:-This script requires ${1}}" + local actual + actual="$(uname -s)" + if [[ "$actual" != "$expected" ]]; then + log_error "$msg" + log_error "Detected OS: $actual (expected: $expected)" + return 1 + fi +} + +# Verify a remote machine (via SSH) is running the expected OS. +# Usage: require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine" +# host_key: SSH host key (UNRAID, FEDORA) — uses ssh_exec +# os_type: expected `uname -s` output +require_remote_os() { + local host_key="$1" expected="$2" msg="${3:-Remote host $1 must be running $2}" + local actual + actual="$(ssh_exec "$host_key" "uname -s" 2>/dev/null)" || { + log_error "Cannot determine OS on ${host_key} (SSH failed)" + return 1 + } + if [[ "$actual" != "$expected" ]]; then + log_error "$msg" + log_error "${host_key} detected OS: $actual (expected: $expected)" + return 1 + fi +} + +# Check if a remote host has a specific package manager available. +# Usage: require_remote_pkg_manager "FEDORA" "dnf" "Fedora requires dnf" +require_remote_pkg_manager() { + local host_key="$1" pkg_mgr="$2" + local msg="${3:-${host_key} requires ${pkg_mgr}}" + if ! ssh_exec "$host_key" "command -v $pkg_mgr" &>/dev/null; then + log_error "$msg" + log_error "${host_key} does not have '${pkg_mgr}' — is this the right machine?" + return 1 + fi +} + # --------------------------------------------------------------------------- # SSH # --------------------------------------------------------------------------- diff --git a/manage_runner.sh b/manage_runner.sh index 8419b81..29db783 100755 --- a/manage_runner.sh +++ b/manage_runner.sh @@ -189,6 +189,9 @@ add_docker_runner() { # and unreliable for long-running background services. # --------------------------------------------------------------------------- add_native_runner() { + # Native runners use launchctl + macOS-specific paths — must be macOS + require_local_os "Darwin" "Native runner deployment requires macOS (uses launchctl)" + log_info "Deploying native runner '${RUNNER_NAME}' on local machine..." # Resolve ~ to actual home directory for local execution @@ -278,6 +281,9 @@ remove_docker_runner() { # remove_native_runner — Unload launchd service + remove binary + plist # --------------------------------------------------------------------------- remove_native_runner() { + # Native runners use launchctl + macOS-specific paths — must be macOS + require_local_os "Darwin" "Native runner removal requires macOS (uses launchctl)" + log_info "Removing native runner '${RUNNER_NAME}' from local machine..." # Resolve ~ to actual home directory diff --git a/phase5_migrate_pipelines.sh b/phase5_migrate_pipelines.sh index fa4fd5a..0c9f92f 100755 --- a/phase5_migrate_pipelines.sh +++ b/phase5_migrate_pipelines.sh @@ -114,13 +114,16 @@ for repo in "${REPOS[@]}"; do mv "$tmpwf" "$dest" # Replace GitHub-specific context variables with Gitea equivalents - # Using sed -i '' for macOS compatibility (GNU sed uses -i without arg) - sed -i '' \ + # Using sed with a temp file for portability (macOS sed -i requires '', + # GNU sed -i requires no arg — avoiding both by writing to a temp file) + tmpwf=$(mktemp) + sed \ -e 's/github\.repository/gitea.repository/g' \ -e 's/github\.event/gitea.event/g' \ -e 's/github\.token/gitea.token/g' \ -e 's/github\.server_url/gitea.server_url/g' \ - "$dest" + "$dest" > "$tmpwf" + mv "$tmpwf" "$dest" log_info " Migrated: ${local_name}" done diff --git a/preflight.sh b/preflight.sh index be7462a..090ee38 100755 --- a/preflight.sh +++ b/preflight.sh @@ -16,7 +16,7 @@ FAIL_COUNT=0 # --------------------------------------------------------------------------- # Check helper — runs a check function, tracks pass/fail count. -# Intentionally does NOT exit on failure — we want to run ALL 16 checks +# Intentionally does NOT exit on failure — we want to run ALL 19 checks # so the user sees every issue at once, not one at a time. # --------------------------------------------------------------------------- check() { @@ -33,12 +33,41 @@ check() { } # --------------------------------------------------------------------------- -# Check 1: .env exists +# Check 1: Local machine is macOS (control plane uses brew, launchctl, macOS sed) +# --------------------------------------------------------------------------- +check_local_os() { + [[ "$(uname -s)" == "Darwin" ]] +} +check 1 "Local machine is macOS (control plane)" check_local_os +if ! check_local_os 2>/dev/null; then + log_error " → This toolkit is designed to run from macOS. Detected: $(uname -s)" +fi + +# --------------------------------------------------------------------------- +# Check 2: Unraid is Linux (via SSH) +# --------------------------------------------------------------------------- +check_unraid_os() { + local remote_os + remote_os="$(ssh_exec UNRAID "uname -s" 2>/dev/null)" || return 1 + [[ "$remote_os" == "Linux" ]] +} + +# --------------------------------------------------------------------------- +# Check 3: Fedora is Linux with dnf (RPM-based) +# --------------------------------------------------------------------------- +check_fedora_os() { + local remote_os + remote_os="$(ssh_exec FEDORA "uname -s" 2>/dev/null)" || return 1 + [[ "$remote_os" == "Linux" ]] && ssh_exec FEDORA "command -v dnf" &>/dev/null +} + +# --------------------------------------------------------------------------- +# Check 4: .env exists # --------------------------------------------------------------------------- check_env_exists() { [[ -f "${SCRIPT_DIR}/.env" ]] } -check 1 ".env file exists" check_env_exists +check 4 ".env file exists" check_env_exists if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then log_error " → .env not found. Copy .env.example to .env and fill in values." log_error " → Or run: setup/configure_env.sh" @@ -46,12 +75,12 @@ if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then fi # --------------------------------------------------------------------------- -# Check 2: runners.conf exists +# Check 5: runners.conf exists # --------------------------------------------------------------------------- check_runners_conf() { [[ -f "${SCRIPT_DIR}/runners.conf" ]] } -check 2 "runners.conf file exists" check_runners_conf +check 5 "runners.conf file exists" check_runners_conf if [[ ! -f "${SCRIPT_DIR}/runners.conf" ]]; then log_error " → runners.conf not found. Copy runners.conf.example to runners.conf." fi @@ -64,7 +93,7 @@ if [[ -f "${SCRIPT_DIR}/.env" ]]; then fi # --------------------------------------------------------------------------- -# Check 3: Required .env vars +# Check 6: Required .env vars # --------------------------------------------------------------------------- REQUIRED_VARS=( UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_DATA_PATH @@ -89,76 +118,90 @@ check_required_vars() { done return $missing } -check 3 "All required .env vars are set" check_required_vars +check 6 "All required .env vars are set" check_required_vars # --------------------------------------------------------------------------- -# Check 4: SSH to Unraid +# Check 7: SSH to Unraid # --------------------------------------------------------------------------- check_ssh_unraid() { ssh_check UNRAID } -check 4 "SSH to Unraid (${UNRAID_IP:-})" check_ssh_unraid -if [[ $FAIL_COUNT -gt 0 ]] && ! ssh_check UNRAID 2>/dev/null; then +check 7 "SSH to Unraid (${UNRAID_IP:-})" check_ssh_unraid +if ! ssh_check UNRAID 2>/dev/null; then log_error " → Cannot SSH to Unraid. Run setup/unraid.sh or check SSH config." fi # --------------------------------------------------------------------------- -# Check 5: SSH to Fedora +# Check 8: SSH to Fedora # --------------------------------------------------------------------------- check_ssh_fedora() { ssh_check FEDORA } -check 5 "SSH to Fedora (${FEDORA_IP:-})" check_ssh_fedora +check 8 "SSH to Fedora (${FEDORA_IP:-})" check_ssh_fedora if ! ssh_check FEDORA 2>/dev/null; then log_error " → Cannot SSH to Fedora. Run setup/fedora.sh or check SSH config." fi # --------------------------------------------------------------------------- -# Check 6: Docker on Unraid +# Checks 2-3: Remote OS checks (deferred until after SSH is confirmed) +# These are numbered 2-3 in the output but run after SSH checks because +# they require SSH connectivity to `uname -s` on the remote machines. +# --------------------------------------------------------------------------- +check 2 "Unraid is Linux" check_unraid_os +if ! check_unraid_os 2>/dev/null; then + log_error " → UNRAID_IP points to a non-Linux machine. Check your .env." +fi +check 3 "Fedora is Linux with dnf (RPM-based)" check_fedora_os +if ! check_fedora_os 2>/dev/null; then + log_error " → FEDORA_IP points to a machine that isn't RPM-based Linux. Check your .env." +fi + +# --------------------------------------------------------------------------- +# Check 9: Docker on Unraid # --------------------------------------------------------------------------- check_docker_unraid() { ssh_exec UNRAID "docker --version" &>/dev/null } -check 6 "Docker available on Unraid" check_docker_unraid +check 9 "Docker available on Unraid" check_docker_unraid if ! check_docker_unraid 2>/dev/null; then log_error " → Docker not found on Unraid. Run setup/unraid.sh." fi # --------------------------------------------------------------------------- -# Check 7: Docker on Fedora +# Check 10: Docker on Fedora # --------------------------------------------------------------------------- check_docker_fedora() { ssh_exec FEDORA "docker --version" &>/dev/null } -check 7 "Docker available on Fedora" check_docker_fedora +check 10 "Docker available on Fedora" check_docker_fedora if ! check_docker_fedora 2>/dev/null; then log_error " → Docker not found on Fedora. Run setup/fedora.sh." fi # --------------------------------------------------------------------------- -# Check 8: docker-compose on Unraid +# Check 11: docker-compose on Unraid # --------------------------------------------------------------------------- check_compose_unraid() { ssh_exec UNRAID "docker compose version" &>/dev/null || ssh_exec UNRAID "docker-compose --version" &>/dev/null } -check 8 "docker-compose available on Unraid" check_compose_unraid +check 11 "docker-compose available on Unraid" check_compose_unraid if ! check_compose_unraid 2>/dev/null; then log_error " → docker-compose not found on Unraid. Run setup/unraid.sh." fi # --------------------------------------------------------------------------- -# Check 9: docker-compose on Fedora +# Check 12: docker-compose on Fedora # --------------------------------------------------------------------------- check_compose_fedora() { ssh_exec FEDORA "docker compose version" &>/dev/null || ssh_exec FEDORA "docker-compose --version" &>/dev/null } -check 9 "docker-compose available on Fedora" check_compose_fedora +check 12 "docker-compose available on Fedora" check_compose_fedora if ! check_compose_fedora 2>/dev/null; then log_error " → docker-compose not found on Fedora. Run setup/fedora.sh." fi # --------------------------------------------------------------------------- -# Check 10: Port free on Unraid +# Check 13: Port free on Unraid # Uses ss (socket statistics) to check if any process is listening on the port. # The ! negates the grep — we PASS if the port is NOT found in use. # --------------------------------------------------------------------------- @@ -166,49 +209,49 @@ check_port_unraid() { local port="${UNRAID_GITEA_PORT:-3000}" ! ssh_exec UNRAID "ss -tlnp | grep -q ':${port} '" 2>/dev/null } -check 10 "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 log_error " → Port ${UNRAID_GITEA_PORT:-3000} already in use on Unraid." fi # --------------------------------------------------------------------------- -# Check 11: Port free on Fedora +# Check 14: Port free on Fedora # --------------------------------------------------------------------------- check_port_fedora() { local port="${FEDORA_GITEA_PORT:-3000}" ! ssh_exec FEDORA "ss -tlnp | grep -q ':${port} '" 2>/dev/null } -check 11 "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 log_error " → Port ${FEDORA_GITEA_PORT:-3000} already in use on Fedora." fi # --------------------------------------------------------------------------- -# Check 12: DNS resolves +# Check 15: DNS resolves # --------------------------------------------------------------------------- check_dns() { local resolved resolved=$(dig +short "${GITEA_DOMAIN:-}" 2>/dev/null | head -1) [[ "$resolved" == "${UNRAID_IP:-}" ]] } -check 12 "DNS: ${GITEA_DOMAIN:-} resolves to ${UNRAID_IP:-}" check_dns +check 15 "DNS: ${GITEA_DOMAIN:-} resolves to ${UNRAID_IP:-}" check_dns if ! check_dns 2>/dev/null; then log_error " → ${GITEA_DOMAIN:-GITEA_DOMAIN} does not resolve to ${UNRAID_IP:-UNRAID_IP}." fi # --------------------------------------------------------------------------- -# Check 13: GitHub token valid +# Check 16: GitHub token valid # --------------------------------------------------------------------------- check_github_token() { [[ -n "${GITHUB_TOKEN:-}" ]] && curl -sf -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/user -o /dev/null } -check 13 "GitHub token valid" check_github_token +check 16 "GitHub token valid" check_github_token if ! check_github_token 2>/dev/null; then log_error " → GitHub token invalid. Check GITHUB_TOKEN in .env." fi # --------------------------------------------------------------------------- -# Check 14: GitHub repos exist +# Check 17: GitHub repos exist # --------------------------------------------------------------------------- check_github_repos() { local all_ok=0 @@ -224,28 +267,28 @@ check_github_repos() { done return $all_ok } -check 14 "All GitHub repos exist" check_github_repos +check 17 "All GitHub repos exist" check_github_repos # --------------------------------------------------------------------------- -# Check 15: Nginx running on Unraid +# Check 18: Nginx running on Unraid # --------------------------------------------------------------------------- check_nginx() { local status status=$(ssh_exec UNRAID "docker ps --filter name=${NGINX_CONTAINER_NAME:-nginx} --format '{{.Status}}'" 2>/dev/null) [[ "$status" == *"Up"* ]] } -check 15 "Nginx container '${NGINX_CONTAINER_NAME:-}' running on Unraid" check_nginx +check 18 "Nginx container '${NGINX_CONTAINER_NAME:-}' running on Unraid" check_nginx if ! check_nginx 2>/dev/null; then log_error " → Nginx container '${NGINX_CONTAINER_NAME:-}' not running on Unraid." fi # --------------------------------------------------------------------------- -# Check 16: Nginx conf dir writable +# Check 19: Nginx conf dir writable # --------------------------------------------------------------------------- check_nginx_conf() { ssh_exec UNRAID "test -w '${NGINX_CONF_PATH:-/nonexistent}'" 2>/dev/null } -check 16 "Nginx config path writable (${NGINX_CONF_PATH:-})" check_nginx_conf +check 19 "Nginx config path writable (${NGINX_CONF_PATH:-})" check_nginx_conf if ! check_nginx_conf 2>/dev/null; then log_error " → Nginx config path ${NGINX_CONF_PATH:-} not writable on Unraid." fi @@ -254,7 +297,7 @@ fi # Summary # --------------------------------------------------------------------------- printf '\n' -log_info "Results: ${PASS_COUNT} passed, ${FAIL_COUNT} failed (out of 16 checks)" +log_info "Results: ${PASS_COUNT} passed, ${FAIL_COUNT} failed (out of 19 checks)" if [[ $FAIL_COUNT -gt 0 ]]; then log_error "Preflight FAILED — fix the issues above before proceeding." diff --git a/run_all.sh b/run_all.sh index caa78f8..8ec31db 100755 --- a/run_all.sh +++ b/run_all.sh @@ -16,6 +16,11 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/lib/common.sh" +# --------------------------------------------------------------------------- +# OS check — the control plane must be macOS (uses brew, launchctl, macOS sed) +# --------------------------------------------------------------------------- +require_local_os "Darwin" "run_all.sh must run from macOS (the control plane)" + # --------------------------------------------------------------------------- # Parse arguments # --------------------------------------------------------------------------- diff --git a/setup/fedora.sh b/setup/fedora.sh index 5c17c61..70426b3 100755 --- a/setup/fedora.sh +++ b/setup/fedora.sh @@ -13,6 +13,15 @@ require_vars FEDORA_IP FEDORA_SSH_USER FEDORA_GITEA_DATA_PATH log_info "=== Fedora Setup ===" +# -------------------------------------------------------------------------- +# OS check — must be Linux with dnf (RPM-based distro like Fedora/RHEL/CentOS) +# This script uses `dnf` for all package installs. Running it against a +# Debian/Ubuntu/Arch host would fail with confusing errors. +# -------------------------------------------------------------------------- +log_info "Verifying Fedora OS..." +require_remote_os "FEDORA" "Linux" "Fedora target must be a Linux machine — check FEDORA_IP in .env" +require_remote_pkg_manager "FEDORA" "dnf" "Fedora target must have dnf (RPM-based distro) — this script won't work on Debian/Ubuntu" + # -------------------------------------------------------------------------- # SSH connectivity # -------------------------------------------------------------------------- diff --git a/setup/macbook.sh b/setup/macbook.sh index a3eab11..47a7ea1 100755 --- a/setup/macbook.sh +++ b/setup/macbook.sh @@ -10,6 +10,11 @@ source "${SCRIPT_DIR}/../lib/common.sh" log_info "=== MacBook Setup ===" +# -------------------------------------------------------------------------- +# OS check — this script uses Homebrew, launchctl, Xcode tools (macOS only) +# -------------------------------------------------------------------------- +require_local_os "Darwin" "macbook.sh must run on macOS — detected a non-macOS system" + # -------------------------------------------------------------------------- # Homebrew # -------------------------------------------------------------------------- diff --git a/setup/unraid.sh b/setup/unraid.sh index 6478ef5..9406b56 100755 --- a/setup/unraid.sh +++ b/setup/unraid.sh @@ -13,6 +13,12 @@ require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_DATA_PATH log_info "=== Unraid Setup ===" +# -------------------------------------------------------------------------- +# OS check — Unraid must be Linux (Docker, static binaries, x86_64) +# -------------------------------------------------------------------------- +log_info "Verifying Unraid OS..." +require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine — check UNRAID_IP in .env" + # -------------------------------------------------------------------------- # SSH connectivity # --------------------------------------------------------------------------