Files
Varaverk/git_pull_execute.sh
T
Gmer4LfeandClaude Sonnet 4.6 5526bc1eca v2 post-migration bug fixes, lidarr_missing_art port, and Partnership SSH/FolderView3 enhancements
Bug fixes across the ecosystem after v1→v2 architecture migration and Unraid 7.2.5 upgrade:

- common.sh: fix _alias_array() phantom empty element (removed [@]:-} pattern), fix
  resolve_remote_ip() with Tailscale FQDN lowercase + awk fallback, add FANART/LASTFM
  key aliases in detect_hosts(), global [@]:-} sweep across 10+ scripts
- webgui_restart.sh: fix emhttp detection (pgrep emhttpd) and restart command for 7.2.5
  (/usr/local/sbin/emhttp stop && start — rc.emhttp removed in 7.2.5)
- bandwidth_monitor.sh, continuous_scripts_status.sh: fix 'local' keyword outside function
- backup_verify.sh: fix resolve_remote_ip() called before detect_hosts()
- Orchestrators: fix script display duplication bug in status output (${entry##*/})
- rsync_stop.sh, git_pull_execute.sh, partnership_manager.sh, coffee_report: lowercase all
  tailscale ip -4 call sites to match Tailscale's lowercase device names
- master_host1.conf: fix SSH key path (gmer4lfe_rsync_automation), add FANART/LASTFM keys
- master_host2.conf: add FANART/LASTFM API keys

New: Media/lidarr_missing_art.sh
- Full ecosystem port of standalone Lidarr artwork fetcher
- Fetches missing album art via fanart.tv + Last.fm APIs
- @tsv batch extraction: 1 jq call per API response vs N*albums (8050 albums in 24s)
- HOST guard (HOST1 only), --status, --dry-run, acquire_lock

New: Initial_run/ssh_setup.sh
- Generates {hostname}_rsync_automation ed25519 keypair (skip if exists, --force to regen)
- ssh-copy-id to remote via Tailscale IP, auto-updates master_host*.conf
- --validate mode: strike tracking (SSH_MAX_STRIKES, SSH_STRIKE_RESET_HRS),
  notify at limit — Tailscale-unreachable remote does NOT count as SSH strike

New: Initial_run/partnership_onboard.sh
- Orchestrator: ssh_setup.sh then partnership_manager.sh --onboard in one command

Partnership/partnership_manager.sh: FolderView3 integration
- Derive partner folder name at runtime (strip unraid- prefix case-insensitively)
- --onboard: create {Mirror}-Failover folder with failover tier containers
- --offboard (both paths): stop + rm containers in folder, remove JSON entry
- --check: calls ssh_setup.sh --validate when IP resolves but SSH state empty
- --status: shows FolderView3 folder state and containers inline

master.conf: SSH_MAX_STRIKES, SSH_STRIKE_RESET_HRS, PARTNERSHIP_FOLDERVIEW3,
PARTNERSHIP_FOLDERVIEW3_URL added to Partnership section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 18:20:23 -04:00

279 lines
13 KiB
Bash

#!/bin/bash
# ==============================================================================================
# ================================= Git Pull & Execute =========================================
# ==============================================================================================
# Pulls the latest scripts from the Gitea repository via SSH.
# Lives at the repo root — sources load_config.sh from the same directory.
#
# ── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────────────────────
# 1. Detects which server it's running on via detect_hosts() (MY_ID)
# 2. Configures sparse checkout to exclude other servers' credential files
# Each server only pulls its own master_host*.conf — never sees peer credentials
# 3. Pulls or clones latest scripts from Gitea
# 4. Sets executable permissions on all .sh files
#
# ── SPARSE CHECKOUT ───────────────────────────────────────────────────────────────────────────
# Sparse checkout ensures each server only receives its own host conf:
# HOST1 pulls: master.conf + master_host1.conf + all scripts
# HOST1 skips: master_host2.conf, master_host3.conf etc.
# HOST2 pulls: master.conf + master_host2.conf + all scripts
# HOST2 skips: master_host1.conf, master_host3.conf etc.
#
# Adding a new server:
# Create master_host3.conf in the repo
# All existing servers automatically exclude it on next pull
# New server gets only its own conf ✅
#
# ── GITEA LOCATION DETECTION ──────────────────────────────────────────────────────────────────
# Detects where Gitea is running at runtime — works through failover:
# Gitea local → connects via local IP
# Gitea remote → connects via Tailscale IP
# Both fail → falls back to GITEA_DOMAIN if configured
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# GITEA_CONTAINER — Docker container name for Gitea
# GITEA_REPO_PATH — repo path on Gitea (e.g. FailedProxy/Unraid_Scripts.git)
# GITEA_DOMAIN — public domain fallback (optional)
# TARGET_DIR — local path to clone/pull into
# GITEA_SSH_KEY — SSH key for Gitea authentication
# SSH_PORT — Gitea SSH port (often 221 or 222)
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# git_pull_execute.sh — normal pull
# git_pull_execute.sh --dry-run — preview without making changes
# git_pull_execute.sh --log — verbose output
# git_pull_execute.sh --status — show config and exit
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Root level script — load_config.sh is in the same directory
source "$SCRIPT_DIR/load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Setup ━━━"
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
success "Running as root"
acquire_lock
# detect_hosts() sets MY_ID — needed for sparse checkout configuration
detect_hosts
# ==============================================================================================
# ━━━ Locate Gitea ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_CONTAINERS Locate Gitea ━━━"
if docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then
# Gitea is running on this server — use local IP
GITEA_IP=$(hostname -I | awk '{print $1}')
log "Gitea running locally — connecting via $GITEA_IP"
else
# Gitea not running locally — find it on the remote server via Tailscale
log "Gitea not running locally — checking remote server"
GITEA_IP=$(tailscale ip -4 "${REMOTE_SERVER_NAME,,}" 2>/dev/null)
if [[ -n "$GITEA_IP" ]]; then
info "$ICON_NET Gitea on $REMOTE_SERVER_NAME — connecting via Tailscale $GITEA_IP"
elif [[ -n "${GITEA_DOMAIN:-}" ]]; then
warn "Tailscale resolution failed — falling back to $GITEA_DOMAIN"
GITEA_IP="$GITEA_DOMAIN"
else
error "Cannot find Gitea — local: not running, Tailscale: failed, domain: not configured"
notify "Git pull failed on $(hostname) — cannot locate Gitea container" "Git Sync" "alert"
exit 1
fi
fi
REPO_SSH="git@${GITEA_IP}:${GITEA_REPO_PATH}"
require_var REPO_SSH
require_var TARGET_DIR
require_var GITEA_SSH_KEY
require_var SSH_PORT
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_NET Repo: $REPO_SSH"
echo "$ICON_GEAR Target: $TARGET_DIR"
echo "$ICON_GEAR SSH Key: $GITEA_SSH_KEY"
echo "$ICON_GEAR SSH Port: $SSH_PORT"
echo "$ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_HOST Remote ID: $REMOTE_ID ($REMOTE_SERVER_NAME)"
echo "$ICON_NOTIFY Notify: unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# ==============================================================================================
# ━━━ Sparse Checkout Configuration ━━━
# ==============================================================================================
# Build the list of master_host*.conf files that belong to OTHER servers.
# This server pulls everything EXCEPT those files.
# MY_ID is set by detect_hosts() — e.g. "HOST1"
configure_sparse_checkout() {
local repo_dir="$1"
log "Configuring sparse checkout for $MY_ID..."
# Enable sparse checkout
git -C "$repo_dir" config core.sparseCheckout true 2>/dev/null
# Build exclusion list — all master_host*.conf files except MY_ID's
local sparse_file="$repo_dir/.git/info/sparse-checkout"
mkdir -p "$(dirname "$sparse_file")"
# Start with: pull everything
echo "/*" > "$sparse_file"
# Exclude each other server's conf file
# Find all master_host*.conf files present in the repo
local excluded=0
for conf_file in "$repo_dir"/master_host*.conf; do
[[ -f "$conf_file" ]] || continue
local conf_name
conf_name=$(basename "$conf_file")
# Determine which HOST ID owns this conf by grepping its hostname var
# Pattern: HOST1="unRAID-..." or HOST2="unRAID-..."
local conf_host_id
conf_host_id=$(grep -m1 -oP '^\s+HOST[0-9]+(?==)' "$conf_file" 2>/dev/null | tr -d ' ')
if [[ -z "$conf_host_id" ]]; then
log "Cannot determine HOST ID for $conf_name — including in pull (safe default)"
continue
fi
if [[ "$conf_host_id" != "$MY_ID" ]]; then
echo "!$conf_name" >> "$sparse_file"
log "Sparse checkout: excluding $conf_name (belongs to $conf_host_id)"
((excluded++))
else
log "Sparse checkout: including $conf_name (belongs to $MY_ID — this server)"
fi
done
if [[ "$excluded" -gt 0 ]]; then
info "$ICON_LOCK Sparse checkout: excluding $excluded peer conf file(s) — credentials protected"
else
log "Sparse checkout: no peer conf files to exclude (single server or first run)"
fi
}
# ==============================================================================================
# ━━━ Git Sync ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Git Sync ━━━"
echo "$ICON_NET Repo: $REPO_SSH"
echo "$ICON_GEAR Target: $TARGET_DIR"
echo ""
START=$(date +%s)
SYNC_SUCCESS=false
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would sync $REPO_SSH$TARGET_DIR"
warn "DRY RUN — would configure sparse checkout for $MY_ID"
warn "DRY RUN — would exclude peer master_host*.conf files"
SYNC_SUCCESS=true
else
mkdir -p "$TARGET_DIR"
cd "$TARGET_DIR" || { error "Cannot cd into $TARGET_DIR"; exit 1; }
if [[ -d ".git" ]]; then
# ── Existing repository ──────────────────────────────────────────────
info "$ICON_SYNC Existing repository detected — updating"
# Configure sparse checkout BEFORE pull
# Uses conf files already present from last pull to determine exclusions
configure_sparse_checkout "$TARGET_DIR"
log "git reset --hard"
git reset --hard
log "git clean -fd"
git clean -fd
info "Pulling latest changes..."
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git pull; then
success "Git pull successful"
SYNC_SUCCESS=true
else
error "Git pull failed"
notify "Git pull failed on $(hostname) — check Gitea connectivity" "Git Sync" "alert"
exit 1
fi
else
# ── Fresh clone ──────────────────────────────────────────────────────
info "$ICON_SYNC No repository found — cloning"
# Clone first — need the repo to exist before configuring sparse checkout
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" git clone "$REPO_SSH" .; then
success "Clone successful"
# Configure sparse checkout after clone
# Now all master_host*.conf files are present — can detect exclusions
configure_sparse_checkout "$TARGET_DIR"
# Apply sparse checkout — removes excluded files from working tree
info "Applying sparse checkout..."
git read-tree -mu HEAD
success "Sparse checkout applied — peer credentials removed from working tree"
SYNC_SUCCESS=true
else
error "Clone failed"
notify "Git clone failed on $(hostname) — check Gitea connectivity" "Git Sync" "alert"
exit 1
fi
fi
# ── Permissions ──────────────────────────────────────────────────────────
echo ""
echo "━━━ $ICON_GEAR Permissions ━━━"
log "Setting executable permissions on all .sh files..."
find "$TARGET_DIR" -type f -name "*.sh" -exec chmod +x {} \;
log "Permissions applied"
fi
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY GIT SYNC SUMMARY ━━━━━"
echo "$ICON_NET Repo: $REPO_SSH"
echo "$ICON_GEAR Target: $TARGET_DIR"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_LOCK Excluded: peer master_host*.conf files"
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
if [[ "$DRY_RUN" == true ]]; then
echo "$ICON_WARN Status: DRY RUN — no changes made"
elif [[ "$SYNC_SUCCESS" == true ]]; then
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
notify "Repository synced successfully on $(hostname)" "Git Sync" "normal"
else
echo "$ICON_ERROR Status: $ICON_ERROR FAILED"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"