2026-03-25 15:52:26 +00:00
|
|
|
#!/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 (preserve aiui and claude-login.html)
|
|
|
|
|
BUILT_WEB="$REPO_DIR/web/dist/neode-ui"
|
|
|
|
|
if [ -d "$BUILT_WEB" ]; then
|
|
|
|
|
# Sync new files, preserving aiui/ and claude-login.html
|
|
|
|
|
sudo rsync -a --delete \
|
|
|
|
|
--exclude 'aiui' \
|
|
|
|
|
--exclude 'claude-login.html' \
|
|
|
|
|
"$BUILT_WEB/" "$INSTALL_WEB/"
|
|
|
|
|
ok "Frontend installed"
|
|
|
|
|
else
|
|
|
|
|
warn "Frontend build output not found at $BUILT_WEB — skipping"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-04-23 10:07:53 -04:00
|
|
|
# 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)
|
2026-03-25 15:52:26 +00:00
|
|
|
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
|
|
|
|
|
|
2026-04-23 12:02:46 -04:00
|
|
|
# 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
|
|
|
|
|
|
2026-03-25 15:52:26 +00:00
|
|
|
# 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"
|