#!/bin/bash # scripts/deploy.sh - Automated deploy for SyncServer # # Usage: # ./scripts/deploy.sh # Full deploy # ./scripts/deploy.sh --dry-run # Simulate without making changes # ./scripts/deploy.sh --help # Show help # SKIP_BUILD=1 ./scripts/deploy.sh # Skip rebuild (use existing .deb) # LXC_HOST=user@host ./scripts/deploy.sh # Custom target # # Environment variables: # LXC_HOST - SSH target (default: root@10.5.1.30) # LXC_PORT - SSH port (default: 22) # REMOTE_DIR - Remote directory (default: ~/move-data-nas) # SKIP_BUILD - Skip make package if 1 # SKIP_VERIFY - Skip post-deploy verification if 1 # LOG_DIR - Where to write logs (default: logs/) set -euo pipefail # === Configuration === SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" LXC_HOST="${LXC_HOST:-root@10.5.1.30}" LXC_PORT="${LXC_PORT:-22}" REMOTE_DIR="${REMOTE_DIR:-~/move-data-nas}" LOG_DIR="${LOG_DIR:-$PROJECT_DIR/logs}" SKIP_BUILD="${SKIP_BUILD:-0}" SKIP_VERIFY="${SKIP_VERIFY:-0}" DRY_RUN=0 HEALTH_TIMEOUT=30 SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -p $LXC_PORT" # === Setup === mkdir -p "$LOG_DIR" LOG_FILE="$LOG_DIR/deploy_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 # === Helpers === log() { echo "→ $*"; } success() { echo "✓ $*"; } warn() { echo "⚠ $*" >&2; } die() { echo "✗ $*" >&2; exit 1; } run_ssh() { ssh $SSH_OPTS "$LXC_HOST" "$@"; } run_scp() { scp -P "$LXC_PORT" "$@"; } BACKUP_PATH="" rollback() { if [ -z "$BACKUP_PATH" ]; then warn "No backup path set, cannot rollback" return 1 fi warn "Rolling back to $BACKUP_PATH..." if run_ssh "systemctl stop syncserver 2>/dev/null; cp $BACKUP_PATH /usr/bin/syncserver; systemctl start syncserver 2>/dev/null || true"; then warn "Rollback completed" else warn "Rollback failed - manual intervention required" fi } trap 'die "Interrupted by signal"' INT TERM # === Argument parsing === while [ $# -gt 0 ]; do case "$1" in --dry-run) DRY_RUN=1; shift ;; --help|-h) sed -n '2,16p' "$0" exit 0 ;; *) die "Unknown argument: $1. Use --help for usage." ;; esac done # === Step 1: Pre-flight === log "Pre-flight checks..." cd "$PROJECT_DIR" git diff --quiet || die "Git working tree is dirty. Commit or stash first." git diff --cached --quiet || die "Git index has staged changes. Commit first." [ "$(git rev-parse --abbrev-ref HEAD)" = "main" ] || die "Must be on main branch" [ -f "$SCRIPT_DIR/bump-version.sh" ] || die "bump-version.sh not found" [ -f "Makefile" ] || die "Makefile not found" [ -d "web" ] || die "web/ directory not found" for tool in ssh scp make go dpkg-deb; do command -v "$tool" >/dev/null || die "$tool not found" done if ! run_ssh 'true' 2>/dev/null; then [ "$DRY_RUN" = "1" ] && warn "Cannot reach $LXC_HOST, but --dry-run so continuing" || die "Cannot ssh to $LXC_HOST" fi FREE_KB=$(df -k "$PROJECT_DIR" | tail -1 | awk '{print $4}') if [ "$FREE_KB" -lt 512000 ]; then warn "Less than 500MB free on local disk" fi success "Pre-flight OK" # === Step 2: Bump version === log "Bumping version..." CURRENT_VERSION=$(grep -oE 'var version = "[0-9]+\.[0-9]+\.[0-9]+"' cmd/server/main.go | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') [ -n "$CURRENT_VERSION" ] || die "Cannot parse current version" if [ "$DRY_RUN" = "1" ]; then success "Would bump from $CURRENT_VERSION (dry run)" NEW_VERSION="$CURRENT_VERSION" else "$SCRIPT_DIR/bump-version.sh" >/dev/null NEW_VERSION=$(grep -oE 'var version = "[0-9]+\.[0-9]+\.[0-9]+"' cmd/server/main.go | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') [ "$NEW_VERSION" != "$CURRENT_VERSION" ] || die "Version bump failed" git add cmd/server/main.go Makefile git commit -m "Bump version to $NEW_VERSION" >/dev/null git push || die "git push failed" success "Bumped to $NEW_VERSION and pushed" fi VERSION="$NEW_VERSION" # === Step 3: Build === if [ "$SKIP_BUILD" = "1" ]; then log "Skipping build (SKIP_BUILD=1)" DEB="dist/syncserver_${VERSION}_amd64.deb" [ -f "$DEB" ] || die "SKIP_BUILD=1 but $DEB does not exist" else log "Building..." if [ "$DRY_RUN" = "1" ]; then log "[dry-run] Would run: make clean && make package" DEB="dist/syncserver_${VERSION}_amd64.deb" if [ -f "$DEB" ]; then log "[dry-run] Found existing $DEB" else log "[dry-run] $DEB does not exist yet (would be created by build)" fi else make clean >/dev/null make package DEB="dist/syncserver_${VERSION}_amd64.deb" [ -f "$DEB" ] || die "Build failed: $DEB not found" success "Built: $DEB ($(du -h "$DEB" | cut -f1))" fi fi # === Step 4: Backup on LXC === BACKUP_PATH="/var/backups/syncserver.previous.${VERSION}" log "Backing up current binary on LXC to $BACKUP_PATH..." if [ "$DRY_RUN" = "1" ]; then log "[dry-run] Would backup /usr/bin/syncserver" else if ! run_ssh "mkdir -p /var/backups && cp /usr/bin/syncserver $BACKUP_PATH && ls -la $BACKUP_PATH" 2>/dev/null; then die "Backup failed on $LXC_HOST" fi success "Backed up to $BACKUP_PATH" fi # === Step 5: Deploy === log "Deploying to $LXC_HOST..." if [ "$DRY_RUN" = "1" ]; then log "[dry-run] Would scp $DEB and run dpkg -i" echo "" echo "Dry run complete. No changes made." echo "Log file: $LOG_FILE" exit 0 fi if ! run_scp "$DEB" "${LXC_HOST}:${REMOTE_DIR}/dist/" 2>/dev/null; then rollback die "SCP failed" fi if ! run_ssh "cd $REMOTE_DIR && dpkg -i dist/syncserver_${VERSION}_amd64.deb" 2>/dev/null; then rollback die "dpkg -i failed, rolled back" fi success "Deployed $DEB" # === Step 6: Verify === if [ "$SKIP_VERIFY" = "1" ]; then log "Skipping verify (SKIP_VERIFY=1)" else log "Verifying deployment (timeout: ${HEALTH_TIMEOUT}s)..." SERVICE_ACTIVE="" for i in $(seq 1 $HEALTH_TIMEOUT); do SERVICE_ACTIVE=$(run_ssh 'systemctl is-active syncserver' 2>/dev/null || echo "unknown") if [ "$SERVICE_ACTIVE" = "active" ]; then break fi sleep 1 done if [ "$SERVICE_ACTIVE" != "active" ]; then rollback die "Service did not become active within ${HEALTH_TIMEOUT}s (current: $SERVICE_ACTIVE)" fi success "Service active" REMOTE_VERSION=$(run_ssh '/usr/bin/syncserver --version' 2>/dev/null | tr -d '[:space:]' || echo "unknown") if [ "$REMOTE_VERSION" != "$VERSION" ]; then rollback die "Version mismatch: deployed $VERSION, remote reports $REMOTE_VERSION" fi success "Version: $REMOTE_VERSION" CACHE_HEADER=$(run_ssh 'curl -sI http://localhost:8080/' 2>/dev/null | grep -i "cache-control:" | head -1 | tr -d '\r') if ! echo "$CACHE_HEADER" | grep -qi "no-cache"; then warn "Cache-Control header missing or wrong: $CACHE_HEADER" else success "Cache headers OK" fi HTTP_CODE=$(run_ssh 'curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/' 2>/dev/null || echo "000") if [ "$HTTP_CODE" != "200" ]; then rollback die "Health check failed: HTTP $HTTP_CODE" fi success "HTTP / → 200" fi # === Summary === echo "" echo "════════════════════════════════════════════" echo " Deploy Summary" echo "════════════════════════════════════════════" echo " Version: $VERSION" echo " Host: $LXC_HOST" echo " Binary: /usr/bin/syncserver" echo " Backup: $BACKUP_PATH" echo " Status: active" echo " HTTP: 200" echo " Headers: $CACHE_HEADER" echo " Time: $(date)" echo " Log file: $LOG_FILE" echo "════════════════════════════════════════════" echo "" echo "Next: hard reload browser once (Ctrl+Shift+R) to clear stale cache"