feat: add version checking and install manifest tracking

Add minimum version validation for all dependencies across local and
remote machines (jq>=1.6, curl>=7.70, git>=2.30, docker>=20.0,
compose>=2.0, shellcheck>=0.8, gh>=2.0). Setup scripts now record
every install action to .manifests/<host>.manifest files, enabling
full rollback via setup/cleanup.sh. teardown_all.sh gains --cleanup
flag to chain prerequisite removal after phase teardowns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
S
2026-02-26 19:35:09 -06:00
parent 720197bb10
commit 07d27f7a9c
9 changed files with 605 additions and 3 deletions

299
setup/cleanup.sh Executable file
View File

@@ -0,0 +1,299 @@
#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# setup/cleanup.sh — Reverse everything installed by setup scripts
# Reads .manifests/<host>.manifest files and undoes each recorded action.
# Use this to clean up machines after testing or after migrating to new servers.
#
# Usage:
# ./setup/cleanup.sh # Clean up all hosts with manifests
# ./setup/cleanup.sh --host=macbook # Clean up macOS only
# ./setup/cleanup.sh --host=unraid # Clean up Unraid only
# ./setup/cleanup.sh --host=fedora # Clean up Fedora only
# ./setup/cleanup.sh --dry-run # Show what would be removed
# ./setup/cleanup.sh --yes # Skip confirmation prompts
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/../lib/common.sh"
load_env || true # Best effort — .env may already be gone during full cleanup
# ---------------------------------------------------------------------------
# Parse arguments
# ---------------------------------------------------------------------------
TARGET_HOST=""
DRY_RUN=false
AUTO_YES=false
for arg in "$@"; do
case "$arg" in
--host=*) TARGET_HOST="${arg#*=}" ;;
--dry-run) DRY_RUN=true ;;
--yes|-y) AUTO_YES=true ;;
--help|-h)
cat <<EOF
Usage: $(basename "$0") [options]
Reverses actions recorded in .manifests/ by setup scripts.
Options:
--host=NAME Clean up a specific host: macbook, unraid, fedora
--dry-run Show what would be removed without doing it
--yes, -y Skip confirmation prompts
--help Show this help
Examples:
$(basename "$0") Clean up all hosts
$(basename "$0") --host=fedora Clean up Fedora only
$(basename "$0") --dry-run Preview cleanup actions
EOF
exit 0 ;;
*) log_error "Unknown argument: $arg"; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# Discover which hosts have manifests
# ---------------------------------------------------------------------------
MANIFEST_DIR="$(_project_root)/.manifests"
if [[ ! -d "$MANIFEST_DIR" ]]; then
log_info "No .manifests/ directory found — nothing to clean up."
exit 0
fi
HOSTS=()
if [[ -n "$TARGET_HOST" ]]; then
if [[ ! -f "${MANIFEST_DIR}/${TARGET_HOST}.manifest" ]]; then
log_error "No manifest found for host '${TARGET_HOST}'"
exit 1
fi
HOSTS=("$TARGET_HOST")
else
for f in "${MANIFEST_DIR}"/*.manifest; do
[[ -f "$f" ]] || continue
host=$(basename "$f" .manifest)
HOSTS+=("$host")
done
fi
if [[ ${#HOSTS[@]} -eq 0 ]]; then
log_info "No manifest files found — nothing to clean up."
exit 0
fi
log_info "Hosts with install manifests: ${HOSTS[*]}"
# ---------------------------------------------------------------------------
# Confirmation (unless --yes or --dry-run)
# ---------------------------------------------------------------------------
if [[ "$DRY_RUN" == "false" ]] && [[ "$AUTO_YES" == "false" ]]; then
log_warn "This will attempt to uninstall everything recorded in the manifests."
printf 'Continue? [y/N] '
read -r confirm
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
log_info "Cleanup cancelled"
exit 0
fi
fi
# ---------------------------------------------------------------------------
# Cleanup functions — one per action type
# ---------------------------------------------------------------------------
cleanup_brew_pkg() {
local pkg="$1"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would uninstall brew package: $pkg"
return 0
fi
if brew list "$pkg" &>/dev/null; then
log_info "Uninstalling brew package: $pkg"
brew uninstall "$pkg" || log_warn "Failed to uninstall $pkg (may have dependents)"
else
log_info "Brew package $pkg not installed — skipping"
fi
}
cleanup_dnf_pkg() {
local host_key="$1" pkg="$2"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would remove dnf package on ${host_key}: $pkg"
return 0
fi
log_info "Removing dnf package on ${host_key}: $pkg"
ssh_exec "$host_key" "sudo dnf -y remove $pkg" 2>/dev/null || log_warn "Failed to remove $pkg on ${host_key}"
}
cleanup_static_bin() {
local host_key="$1" path="$2"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would remove static binary on ${host_key}: $path"
return 0
fi
log_info "Removing static binary on ${host_key}: $path"
ssh_exec "$host_key" "rm -f '$path'" 2>/dev/null || log_warn "Failed to remove $path on ${host_key}"
}
cleanup_docker_group() {
local host_key="$1" username="$2"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would remove ${username} from docker group on ${host_key}"
return 0
fi
log_info "Removing ${username} from docker group on ${host_key}"
ssh_exec "$host_key" "sudo gpasswd -d $username docker" 2>/dev/null || log_warn "Failed to remove $username from docker group on ${host_key}"
}
cleanup_systemd_svc() {
local host_key="$1" service="$2"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would disable systemd service on ${host_key}: $service"
return 0
fi
log_info "Disabling systemd service on ${host_key}: $service"
ssh_exec "$host_key" "sudo systemctl disable --now $service" 2>/dev/null || log_warn "Failed to disable $service on ${host_key}"
}
cleanup_xcode_cli() {
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would remove Xcode CLI Tools"
return 0
fi
log_info "Removing Xcode Command Line Tools..."
sudo rm -rf /Library/Developer/CommandLineTools 2>/dev/null || log_warn "Failed to remove Xcode CLI Tools (may need sudo)"
}
# ---------------------------------------------------------------------------
# Map host names to SSH host keys for remote operations
# ---------------------------------------------------------------------------
host_to_ssh_key() {
local host="$1"
case "$host" in
unraid) echo "UNRAID" ;;
fedora) echo "FEDORA" ;;
*) echo "" ;;
esac
}
# ---------------------------------------------------------------------------
# Process each host's manifest in reverse order
# Reverse order ensures dependent packages are removed before their providers
# (e.g., docker-compose-plugin before docker-ce).
# ---------------------------------------------------------------------------
TOTAL_ACTIONS=0
CLEANED=0
FAILED=0
for host in "${HOSTS[@]}"; do
log_info "=== Cleaning up: ${host} ==="
ssh_key=$(host_to_ssh_key "$host")
# Read entries into array, then reverse
mapfile -t entries < <(manifest_entries "$host")
if [[ ${#entries[@]} -eq 0 ]]; then
log_info "No entries in ${host} manifest — skipping"
continue
fi
# Process in reverse order
for ((i = ${#entries[@]} - 1; i >= 0; i--)); do
entry="${entries[$i]}"
IFS='|' read -r action_type target _details <<< "$entry"
TOTAL_ACTIONS=$((TOTAL_ACTIONS + 1))
case "$action_type" in
brew_pkg)
if cleanup_brew_pkg "$target"; then
CLEANED=$((CLEANED + 1))
else
FAILED=$((FAILED + 1))
fi
;;
dnf_pkg)
if [[ -z "$ssh_key" ]]; then
log_warn "Cannot clean up dnf_pkg '$target' — no SSH key for host '$host'"
FAILED=$((FAILED + 1))
continue
fi
if cleanup_dnf_pkg "$ssh_key" "$target"; then
CLEANED=$((CLEANED + 1))
else
FAILED=$((FAILED + 1))
fi
;;
static_bin)
if [[ -z "$ssh_key" ]]; then
log_warn "Cannot clean up static_bin '$target' — no SSH key for host '$host'"
FAILED=$((FAILED + 1))
continue
fi
if cleanup_static_bin "$ssh_key" "$target"; then
CLEANED=$((CLEANED + 1))
else
FAILED=$((FAILED + 1))
fi
;;
docker_group)
if [[ -z "$ssh_key" ]]; then
log_warn "Cannot clean up docker_group '$target' — no SSH key for host '$host'"
FAILED=$((FAILED + 1))
continue
fi
if cleanup_docker_group "$ssh_key" "$target"; then
CLEANED=$((CLEANED + 1))
else
FAILED=$((FAILED + 1))
fi
;;
systemd_svc)
if [[ -z "$ssh_key" ]]; then
log_warn "Cannot clean up systemd_svc '$target' — no SSH key for host '$host'"
FAILED=$((FAILED + 1))
continue
fi
if cleanup_systemd_svc "$ssh_key" "$target"; then
CLEANED=$((CLEANED + 1))
else
FAILED=$((FAILED + 1))
fi
;;
xcode_cli)
if cleanup_xcode_cli; then
CLEANED=$((CLEANED + 1))
else
FAILED=$((FAILED + 1))
fi
;;
*)
log_warn "Unknown action type '${action_type}' for target '${target}' — skipping"
FAILED=$((FAILED + 1))
;;
esac
done
# Clear the manifest after successful cleanup (unless dry run)
if [[ "$DRY_RUN" == "false" ]]; then
manifest_clear "$host"
log_success "Manifest cleared for ${host}"
fi
done
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
printf '\n'
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[dry-run] Would process ${TOTAL_ACTIONS} actions across ${#HOSTS[@]} host(s)"
else
log_info "Cleanup summary: ${CLEANED} cleaned, ${FAILED} failed (out of ${TOTAL_ACTIONS} actions)"
if [[ $FAILED -gt 0 ]]; then
log_warn "Some cleanup actions failed — check logs above"
else
log_success "All cleanup actions completed successfully"
fi
fi

View File

@@ -22,6 +22,11 @@ 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"
# --------------------------------------------------------------------------
# Manifest — track what we install for rollback/cleanup
# --------------------------------------------------------------------------
manifest_init "fedora"
# --------------------------------------------------------------------------
# SSH connectivity
# --------------------------------------------------------------------------
@@ -40,12 +45,19 @@ if ssh_exec FEDORA "docker --version" &>/dev/null; then
else
log_info "Installing Docker CE on Fedora..."
ssh_exec FEDORA "sudo dnf -y install dnf-plugins-core"
manifest_record "fedora" "dnf_pkg" "dnf-plugins-core"
ssh_exec FEDORA "sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
ssh_exec FEDORA "sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin"
manifest_record "fedora" "dnf_pkg" "docker-ce"
manifest_record "fedora" "dnf_pkg" "docker-ce-cli"
manifest_record "fedora" "dnf_pkg" "containerd.io"
manifest_record "fedora" "dnf_pkg" "docker-compose-plugin"
ssh_exec FEDORA "sudo systemctl enable --now docker"
manifest_record "fedora" "systemd_svc" "docker"
# Add user to docker group
ssh_exec FEDORA "sudo usermod -aG docker $FEDORA_SSH_USER"
manifest_record "fedora" "docker_group" "$FEDORA_SSH_USER"
log_warn "User $FEDORA_SSH_USER added to docker group. You may need to re-login for this to take effect."
if ssh_exec FEDORA "docker --version" &>/dev/null; then
@@ -74,6 +86,7 @@ if ssh_exec FEDORA "jq --version" &>/dev/null; then
else
log_info "Installing jq on Fedora..."
ssh_exec FEDORA "sudo dnf -y install jq"
manifest_record "fedora" "dnf_pkg" "jq"
if ssh_exec FEDORA "jq --version" &>/dev/null; then
log_success "jq installed on Fedora"
else
@@ -82,6 +95,14 @@ else
fi
fi
# --------------------------------------------------------------------------
# Minimum version checks for remote tools
# --------------------------------------------------------------------------
log_info "Checking minimum versions on Fedora..."
check_remote_min_version "FEDORA" "docker" "docker --version" "20.0"
check_remote_min_version "FEDORA" "docker-compose" "docker compose version" "2.0"
check_remote_min_version "FEDORA" "jq" "jq --version" "1.6"
# --------------------------------------------------------------------------
# Verify Docker works without sudo
# --------------------------------------------------------------------------

View File

@@ -15,6 +15,11 @@ log_info "=== MacBook Setup ==="
# --------------------------------------------------------------------------
require_local_os "Darwin" "macbook.sh must run on macOS — detected a non-macOS system"
# --------------------------------------------------------------------------
# Manifest — track what we install for rollback/cleanup
# --------------------------------------------------------------------------
manifest_init "macbook"
# --------------------------------------------------------------------------
# Homebrew
# --------------------------------------------------------------------------
@@ -37,10 +42,21 @@ for pkg in "${BREW_PACKAGES[@]}"; do
else
log_info "Installing $pkg..."
brew install "$pkg"
manifest_record "macbook" "brew_pkg" "$pkg"
log_success "$pkg installed"
fi
done
# --------------------------------------------------------------------------
# Minimum version checks for local tools
# --------------------------------------------------------------------------
log_info "Checking minimum versions..."
check_min_version "jq" "jq --version" "1.6"
check_min_version "curl" "curl --version" "7.70"
check_min_version "git" "git --version" "2.30"
check_min_version "shellcheck" "shellcheck --version" "0.8"
check_min_version "gh" "gh --version" "2.0"
# --------------------------------------------------------------------------
# Verify built-in tools
# --------------------------------------------------------------------------
@@ -70,6 +86,7 @@ if xcode-select -p &>/dev/null; then
else
log_info "Installing Xcode Command Line Tools..."
xcode-select --install
manifest_record "macbook" "xcode_cli" "xcode-select"
log_warn "Xcode CLI Tools installation started. Wait for it to finish, then re-run this script."
exit 0
fi

View File

@@ -19,6 +19,11 @@ log_info "=== Unraid Setup ==="
log_info "Verifying Unraid OS..."
require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine — check UNRAID_IP in .env"
# --------------------------------------------------------------------------
# Manifest — track what we install for rollback/cleanup
# --------------------------------------------------------------------------
manifest_init "unraid"
# --------------------------------------------------------------------------
# SSH connectivity
# --------------------------------------------------------------------------
@@ -52,6 +57,7 @@ else
log_info "Installing docker-compose standalone binary on Unraid..."
COMPOSE_VERSION="v2.29.1"
ssh_exec UNRAID "curl -SL https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose"
manifest_record "unraid" "static_bin" "/usr/local/bin/docker-compose"
if ssh_exec UNRAID "docker-compose --version" &>/dev/null; then
log_success "docker-compose installed on Unraid"
else
@@ -69,6 +75,7 @@ else
log_info "Installing jq static binary on Unraid..."
JQ_VERSION="1.7.1"
ssh_exec UNRAID "curl -SL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64 -o /usr/local/bin/jq && chmod +x /usr/local/bin/jq"
manifest_record "unraid" "static_bin" "/usr/local/bin/jq"
if ssh_exec UNRAID "jq --version" &>/dev/null; then
log_success "jq installed on Unraid"
else
@@ -77,6 +84,14 @@ else
fi
fi
# --------------------------------------------------------------------------
# Minimum version checks for remote tools
# --------------------------------------------------------------------------
log_info "Checking minimum versions on Unraid..."
check_remote_min_version "UNRAID" "docker" "docker --version" "20.0"
check_remote_min_version "UNRAID" "docker-compose" "docker compose version 2>/dev/null || docker-compose --version" "2.0"
check_remote_min_version "UNRAID" "jq" "jq --version" "1.6"
# --------------------------------------------------------------------------
# Data path
# --------------------------------------------------------------------------