#!/bin/bash # Self-update: pull latest code from git.tx1138.com and apply # Designed to run on installed Archipelago nodes (as archipelago user) # # Usage: # ./self-update.sh # Check + apply if available # ./self-update.sh --check # Check only, don't apply # ./self-update.sh --force # Apply even if already up to date # # The script: # 1. Pulls latest code from origin (git.tx1138.com) # 2. Builds the Rust backend (release mode) # 3. Builds the Vue frontend (production mode) # 4. Installs the new binary and web UI # 5. Restarts the archipelago service # 6. Verifies health after restart set -euo pipefail REPO_DIR="$HOME/archy" BACKEND_DIR="$REPO_DIR/core" FRONTEND_DIR="$REPO_DIR/neode-ui" INSTALL_BIN="/usr/local/bin/archipelago" INSTALL_WEB="/opt/archipelago/web-ui" STATE_FILE="/var/lib/archipelago/update_state.json" LOG_FILE="/var/lib/archipelago/update.log" LOCK_FILE="/tmp/archipelago-update.lock" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log() { echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $*" | tee -a "$LOG_FILE"; } ok() { echo -e "${GREEN}[$(date '+%H:%M:%S')] OK${NC} $*" | tee -a "$LOG_FILE"; } err() { echo -e "${RED}[$(date '+%H:%M:%S')] ERROR${NC} $*" | tee -a "$LOG_FILE"; } warn(){ echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARN${NC} $*" | tee -a "$LOG_FILE"; } cleanup() { rm -f "$LOCK_FILE" } trap cleanup EXIT # Prevent concurrent updates if [ -f "$LOCK_FILE" ]; then pid=$(cat "$LOCK_FILE" 2>/dev/null) if kill -0 "$pid" 2>/dev/null; then err "Update already in progress (PID $pid)" exit 1 fi warn "Stale lock file found, removing" rm -f "$LOCK_FILE" fi echo $$ > "$LOCK_FILE" # Parse args CHECK_ONLY=false FORCE=false while [[ $# -gt 0 ]]; do case "$1" in --check) CHECK_ONLY=true; shift ;; --force) FORCE=true; shift ;; *) shift ;; esac done # Ensure repo exists if [ ! -d "$REPO_DIR/.git" ]; then err "Repo not found at $REPO_DIR" err "Clone it first: git clone https://git.tx1138.com/lfg2025/archy ~/archy" exit 1 fi cd "$REPO_DIR" # Fetch latest log "Fetching from origin..." git fetch origin main --quiet 2>>"$LOG_FILE" # Check if there are updates LOCAL=$(git rev-parse HEAD) REMOTE=$(git rev-parse origin/main) if [ "$LOCAL" = "$REMOTE" ] && [ "$FORCE" = "false" ]; then ok "Already up to date ($LOCAL)" if [ "$CHECK_ONLY" = "true" ]; then echo '{"update_available": false, "current": "'"$LOCAL"'"}' fi exit 0 fi # Calculate what changed COMMITS_BEHIND=$(git rev-list HEAD..origin/main --count) log "Update available: $COMMITS_BEHIND commits behind" log " Local: $LOCAL" log " Remote: $REMOTE" if [ "$CHECK_ONLY" = "true" ]; then CHANGELOG=$(git log HEAD..origin/main --oneline --no-merges | head -20) echo '{"update_available": true, "current": "'"$LOCAL"'", "latest": "'"$REMOTE"'", "commits_behind": '"$COMMITS_BEHIND"'}' echo "" echo "Changes:" echo "$CHANGELOG" exit 0 fi # Backup current binary BACKUP_DIR="/var/lib/archipelago/update-backup" mkdir -p "$BACKUP_DIR" if [ -f "$INSTALL_BIN" ]; then cp "$INSTALL_BIN" "$BACKUP_DIR/archipelago.bak" log "Backed up current binary" fi # Pull latest code log "Pulling latest code..." git pull origin main --ff-only 2>>"$LOG_FILE" || { err "Git pull failed — local changes? Run: git reset --hard origin/main" exit 1 } NEW_VERSION=$(git rev-parse --short HEAD) log "Now at: $NEW_VERSION" # Build backend log "Building Rust backend (release)..." cd "$BACKEND_DIR" if cargo build --release --workspace 2>>"$LOG_FILE"; then ok "Backend built successfully" else err "Backend build failed — rolling back" cd "$REPO_DIR" git reset --hard "$LOCAL" 2>>"$LOG_FILE" exit 1 fi # Install binary BUILT_BIN="$BACKEND_DIR/target/release/archipelago" if [ ! -f "$BUILT_BIN" ]; then err "Built binary not found at $BUILT_BIN" exit 1 fi sudo cp "$BUILT_BIN" "$INSTALL_BIN" sudo chmod +x "$INSTALL_BIN" ok "Backend installed" # Build frontend log "Building Vue frontend (production)..." cd "$FRONTEND_DIR" npm ci --silent 2>>"$LOG_FILE" || npm install --silent 2>>"$LOG_FILE" if npm run build 2>>"$LOG_FILE"; then ok "Frontend built successfully" else err "Frontend build failed — backend already updated, service may need manual fix" exit 1 fi # Install frontend (always ship fresh AIUI from demo/aiui; preserve claude-login.html) BUILT_WEB="$REPO_DIR/web/dist/neode-ui" if [ -d "$BUILT_WEB" ]; then # Bake AIUI into the built tree so rsync --delete does not wipe it. # demo/aiui is the canonical AIUI bundle checked into the repo; copying # it here means every self-update ships a matching AIUI version instead # of preserving whatever stale copy happened to be on disk (which is # empty on nodes where an earlier ad-hoc deploy blew it away). if [ -d "$REPO_DIR/demo/aiui" ] && [ -f "$REPO_DIR/demo/aiui/index.html" ]; then log "Staging AIUI bundle from demo/aiui into frontend dist..." rm -rf "$BUILT_WEB/aiui" cp -r "$REPO_DIR/demo/aiui" "$BUILT_WEB/aiui" else warn "demo/aiui not found in repo; existing /opt/archipelago/web-ui/aiui will be wiped by rsync --delete" fi # Sync new files, preserving claude-login.html (per-node admin bookmark) sudo rsync -a --delete \ --exclude 'claude-login.html' \ "$BUILT_WEB/" "$INSTALL_WEB/" ok "Frontend installed" else warn "Frontend build output not found at $BUILT_WEB — skipping" fi # Update helper scripts in /opt/archipelago/scripts/ # These are canonical home; keep a copy at /opt/archipelago/image-versions.sh # for backward compatibility with older binaries that still look there. SCRIPTS_DEST="/opt/archipelago/scripts" sudo mkdir -p "$SCRIPTS_DEST" for script in image-versions.sh reconcile-containers.sh container-specs.sh; do src="$REPO_DIR/scripts/$script" if [ -f "$src" ]; then sudo install -m 755 "$src" "$SCRIPTS_DEST/$script" ok "Updated $script" else warn "Missing $src — skipping" fi done # Legacy path for image-versions.sh (older binaries looked here first) if [ -f "$REPO_DIR/scripts/image-versions.sh" ]; then sudo cp "$REPO_DIR/scripts/image-versions.sh" /opt/archipelago/image-versions.sh fi # Update systemd service if changed if [ -f "$REPO_DIR/image-recipe/configs/archipelago.service" ]; then if ! diff -q "$REPO_DIR/image-recipe/configs/archipelago.service" /etc/systemd/system/archipelago.service &>/dev/null; then sudo cp "$REPO_DIR/image-recipe/configs/archipelago.service" /etc/systemd/system/archipelago.service sudo systemctl daemon-reload ok "Systemd service updated" fi fi # Install/refresh tmpfiles.d rules. The logs rule creates # /var/log/archipelago/ + container-installs.log with archipelago:archipelago # ownership so the non-root backend can append install audit lines. # Apply immediately so existing nodes don't need a reboot. if [ -f "$REPO_DIR/image-recipe/configs/archipelago-tmpfiles.conf" ]; then sudo install -m 644 "$REPO_DIR/image-recipe/configs/archipelago-tmpfiles.conf" \ /usr/lib/tmpfiles.d/archipelago-logs.conf sudo systemd-tmpfiles --create /usr/lib/tmpfiles.d/archipelago-logs.conf 2>/dev/null || true ok "Log tmpfiles rule installed" fi # Restart service log "Restarting archipelago service..." sudo systemctl restart archipelago # Wait for health log "Waiting for backend health..." for i in $(seq 1 30); do if curl -sf http://127.0.0.1:5678/health > /dev/null 2>&1; then ok "Backend healthy after ${i}s" break fi if [ "$i" = "30" ]; then err "Backend failed to start within 30s" warn "Rolling back binary..." if [ -f "$BACKUP_DIR/archipelago.bak" ]; then sudo cp "$BACKUP_DIR/archipelago.bak" "$INSTALL_BIN" sudo systemctl restart archipelago err "Rolled back to previous binary" fi exit 1 fi sleep 1 done # Update state file for the UI python3 -c " import json, datetime state = { 'current_version': '$NEW_VERSION', 'last_check': datetime.datetime.utcnow().isoformat() + 'Z', 'available_update': None, 'update_in_progress': False, 'rollback_available': True, 'schedule': 'daily_check' } with open('$STATE_FILE', 'w') as f: json.dump(state, f, indent=2) " 2>/dev/null || true echo "" ok "Update complete: $LOCAL -> $NEW_VERSION" log "Changelog:" git log "$LOCAL".."$NEW_VERSION" --oneline --no-merges | head -10 | tee -a "$LOG_FILE"