Add deploy script with dry-run, retry, rollback and health checks
This commit is contained in:
Executable
+241
@@ -0,0 +1,241 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user