#!/bin/bash ############################################################################### # Pokedex.Online Deployment Automation Script # # This script automates the deployment process with pre-deployment checks, # build verification, and rollback capability. # # Usage: # ./deploy.sh [options] # # Options: # --target Deployment target (default: local) # --port Frontend HTTP port (default: 8099) # --backend-port Backend port (default: 3099) # --skip-tests Skip test execution # --skip-build Skip build step (use existing dist/) # --no-backup Skip backup creation # --dry-run Show what would be deployed without deploying # # Deployment Strategies: # local - Docker deployment on localhost:8099 for production testing # production - Deploy to Synology at https://app.pokedex.online # # Examples: # ./deploy.sh --target local # ./deploy.sh --target production # ./deploy.sh --dry-run ############################################################################### set -e # Exit on error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Default configuration TARGET="local" PORT=8099 BACKEND_PORT=3099 SKIP_TESTS=false SKIP_BUILD=false NO_BACKUP=false DRY_RUN=false # Script directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT_ROOT="$SCRIPT_DIR" ############################################################################### # Helper Functions ############################################################################### log_info() { echo -e "${BLUE}ℹ${NC} $1" } log_success() { echo -e "${GREEN}✅${NC} $1" } log_warning() { echo -e "${YELLOW}⚠${NC} $1" } log_error() { echo -e "${RED}❌${NC} $1" } log_step() { echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" } ############################################################################### # Parse Arguments ############################################################################### parse_args() { while [[ $# -gt 0 ]]; do case $1 in --target) TARGET="$2" shift 2 ;; --port) PORT="$2" shift 2 ;; --backend-port) BACKEND_PORT="$2" shift 2 ;; --skip-tests) SKIP_TESTS=true shift ;; --skip-build) SKIP_BUILD=true shift ;; --no-backup) NO_BACKUP=true shift ;; --dry-run) DRY_RUN=true shift ;; --help) head -n 30 "$0" | tail -n 25 exit 0 ;; *) log_error "Unknown option: $1" echo "Use --help for usage information" exit 1 ;; esac done } ############################################################################### # Pre-Deployment Checks ############################################################################### check_prerequisites() { log_step "🔍 Checking Prerequisites" # Check Node.js if ! command -v node &> /dev/null; then log_error "Node.js is not installed" exit 1 fi log_success "Node.js $(node --version)" # Check npm if ! command -v npm &> /dev/null; then log_error "npm is not installed" exit 1 fi log_success "npm $(npm --version)" # Check if dependencies are installed if [ ! -d "$PROJECT_ROOT/node_modules" ]; then log_error "Dependencies not installed. Run 'npm install' first" exit 1 fi log_success "Dependencies installed" # Check if server dependencies are installed (workspaces hoist to root node_modules) # Check for key server dependencies in root node_modules if [ ! -d "$PROJECT_ROOT/node_modules/express" ] || [ ! -d "$PROJECT_ROOT/node_modules/cors" ]; then log_error "Server dependencies not installed. Run 'npm install' first" exit 1 fi log_success "Server dependencies installed" } check_environment() { log_step "🔧 Checking Environment Configuration" # Validate target if [ "$TARGET" != "local" ] && [ "$TARGET" != "production" ]; then log_error "Invalid target: $TARGET" log_info "Valid targets: local, production" exit 1 fi # Check for mode-specific env files local mode if [ "$TARGET" = "local" ]; then mode="docker-local" else mode="production" fi if [ ! -f "$PROJECT_ROOT/.env.$mode" ]; then log_error "Environment file .env.$mode not found" exit 1 fi log_success "Frontend environment configuration found (.env.$mode)" if [ ! -f "$PROJECT_ROOT/server/.env.$mode" ]; then log_error "Environment file server/.env.$mode not found" exit 1 fi log_success "Backend environment configuration found (server/.env.$mode)" } run_tests() { if [ "$SKIP_TESTS" = true ]; then log_warning "Skipping tests (--skip-tests flag set)" return fi log_step "🧪 Running Tests" log_info "Running frontend tests..." npm run test:run || { log_error "Frontend tests failed" exit 1 } log_success "Frontend tests passed" log_info "Running backend tests..." npm run test:run --workspace=server || { log_warning "Backend tests failed or not found - continuing anyway" } log_success "Backend checks completed" } ############################################################################### # Build ############################################################################### prepare_build_mode() { # Determine Vite build mode based on deployment target if [ "$TARGET" = "local" ]; then BUILD_MODE="docker-local" else BUILD_MODE="production" fi log_info "Preparing build for ${TARGET} deployment..." log_info "Vite build mode: ${BUILD_MODE}" log_info "Environment file: .env.${BUILD_MODE}" } build_application() { if [ "$SKIP_BUILD" = true ]; then log_warning "Skipping build (--skip-build flag set)" # Check if dist exists if [ ! -d "$PROJECT_ROOT/dist" ]; then log_error "dist/ directory not found and --skip-build is set" log_info "Remove --skip-build or run 'npm run build' first" exit 1 fi return fi log_step "🔨 Building Application" # Prepare build mode prepare_build_mode log_info "Building frontend with mode: $BUILD_MODE..." npx vite build --mode "$BUILD_MODE" || { log_error "Frontend build failed" exit 1 } log_success "Frontend built successfully" log_info "Verifying build..." BUILD_TARGET="$TARGET" npm run build:verify || { log_error "Build verification failed" exit 1 } log_success "Build verified" } ############################################################################### # Backup ############################################################################### create_backup() { if [ "$NO_BACKUP" = true ]; then log_warning "Skipping backup (--no-backup flag set)" return fi log_step "💾 Creating Backup" BACKUP_DIR="$PROJECT_ROOT/backups" TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="$BACKUP_DIR/backup_${TIMESTAMP}.tar.gz" mkdir -p "$BACKUP_DIR" log_info "Creating backup of current deployment..." tar -czf "$BACKUP_FILE" \ --exclude='node_modules' \ --exclude='dist' \ --exclude='.git' \ --exclude='backups' \ -C "$PROJECT_ROOT" . || { log_error "Backup creation failed" exit 1 } log_success "Backup created: $BACKUP_FILE" # Keep only last 5 backups BACKUP_COUNT=$(ls -1 "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l) if [ "$BACKUP_COUNT" -gt 5 ]; then log_info "Cleaning old backups (keeping last 5)..." ls -1t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +6 | xargs rm -f fi } ############################################################################### # Deployment ############################################################################### deploy_to_server() { log_step "🚀 Deploying to Server" if [ "$DRY_RUN" = true ]; then log_info "DRY RUN - Would deploy with following configuration:" log_info " Target: $TARGET" log_info " Frontend Port: $PORT" log_info " Backend Port: $BACKEND_PORT" log_success "Dry run completed" return fi if [ "$TARGET" = "local" ]; then log_info "Deploying to local Docker..." # Copy server env file cp "$PROJECT_ROOT/server/.env.docker-local" "$PROJECT_ROOT/server/.env" || { log_error "Failed to copy server environment file" exit 1 } # Use docker-compose.docker-local.yml log_info "Starting Docker containers..." docker compose -f docker-compose.docker-local.yml down || true docker compose -f docker-compose.docker-local.yml up -d --build || { log_error "Docker deployment failed" exit 1 } else log_info "Deploying to production (Synology)..." deploy_to_synology fi log_success "Deployment completed successfully" } deploy_to_synology() { # SSH Configuration local SSH_HOST="10.0.0.81" local SSH_PORT="2323" local SSH_USER="GregRJacobs" local SSH_KEY="$HOME/.ssh/ds3627xs_gregrjacobs" local REMOTE_PATH="/volume1/docker/pokedex-online" log_info "SSH Config: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}" # Verify SSH key exists if [ ! -f "$SSH_KEY" ]; then log_error "SSH key not found: $SSH_KEY" exit 1 fi # Test SSH connection log_info "Testing SSH connection..." if ! ssh -i "$SSH_KEY" -p "$SSH_PORT" -o ConnectTimeout=10 -o BatchMode=yes "$SSH_USER@$SSH_HOST" "echo 'Connection successful'" > /dev/null 2>&1; then log_error "SSH connection failed" log_info "Please verify:" log_info " 1. SSH key exists: $SSH_KEY" log_info " 2. Key has correct permissions: chmod 600 $SSH_KEY" log_info " 3. Public key is in authorized_keys on server" log_info " 4. You can connect manually: ssh -i $SSH_KEY -p $SSH_PORT $SSH_USER@$SSH_HOST" exit 1 fi log_success "SSH connection verified" # Create remote directory log_info "Creating remote directory..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_PATH/dist $REMOTE_PATH/server" || { log_error "Failed to create remote directory" exit 1 } # Transfer dist directory using tar over SSH log_info "Transferring frontend build (dist/)..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "rm -rf $REMOTE_PATH/dist && mkdir -p $REMOTE_PATH/dist" tar -czf - -C "$PROJECT_ROOT" dist 2>/dev/null | \ ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "tar -xzf - -C $REMOTE_PATH --strip-components=0 2>/dev/null" || { log_error "Failed to transfer dist directory" exit 1 } log_success "Frontend build transferred" # Transfer server directory (excluding node_modules) using tar over SSH log_info "Transferring backend code (server/)..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "rm -rf $REMOTE_PATH/server && mkdir -p $REMOTE_PATH/server" tar -czf - -C "$PROJECT_ROOT" \ --exclude='node_modules' \ --exclude='.env*' \ --exclude='data' \ --exclude='logs' \ server 2>/dev/null | \ ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "tar -xzf - -C $REMOTE_PATH --strip-components=0 2>/dev/null" || { log_error "Failed to transfer server directory" exit 1 } log_success "Backend code transferred" # Copy production environment file to server (after server code transfer) log_info "Copying server environment file..." cat "$PROJECT_ROOT/server/.env.production" | \ ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "cat > $REMOTE_PATH/server/.env" || { log_error "Failed to copy server .env file" exit 1 } log_success "Server environment configured" # Create required directories for volume mounts log_info "Creating volume mount directories..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "mkdir -p $REMOTE_PATH/server/data $REMOTE_PATH/server/logs" || { log_error "Failed to create volume mount directories" exit 1 } # Transfer Docker configuration files log_info "Transferring Docker configuration..." for file in docker-compose.production.yml nginx.conf Dockerfile.frontend; do cat "$PROJECT_ROOT/$file" | \ ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "cat > $REMOTE_PATH/$file" || { log_error "Failed to transfer $file" exit 1 } done cat "$PROJECT_ROOT/server/Dockerfile" | \ ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "cat > $REMOTE_PATH/server/Dockerfile" || { log_error "Failed to transfer server Dockerfile" exit 1 } log_success "Docker configuration transferred" # Find Docker on remote system log_info "Locating Docker on remote system..." DOCKER_PATH=$(ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "which docker 2>/dev/null || echo /usr/local/bin/docker") log_info "Using Docker at: $DOCKER_PATH" # Stop and remove existing containers (force cleanup) log_info "Stopping and removing existing containers..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "cd $REMOTE_PATH && $DOCKER_PATH compose -f docker-compose.production.yml down --remove-orphans 2>/dev/null || true" # Force remove any lingering containers with matching names ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "$DOCKER_PATH rm -f pokedex-frontend pokedex-backend 2>/dev/null || true" # Start containers log_info "Starting Docker containers..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "cd $REMOTE_PATH && $DOCKER_PATH compose -f docker-compose.production.yml up -d --build" || { log_error "Failed to start Docker containers" log_info "Rolling back..." ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \ "cd $REMOTE_PATH && $DOCKER_PATH compose -f docker-compose.production.yml down --remove-orphans" exit 1 } log_success "Containers started" } ############################################################################### # Post-Deployment ############################################################################### verify_deployment() { log_step "🏥 Verifying Deployment" if [ "$DRY_RUN" = true ]; then log_info "DRY RUN - Skipping verification" return fi # Determine host based on target if [ "$TARGET" = "production" ]; then HOST="10.0.0.81" else HOST="localhost" fi log_info "Checking frontend health..." sleep 3 # Give containers time to start if curl -f -s "http://$HOST:$PORT/health" > /dev/null; then log_success "Frontend is responding" else log_warning "Frontend health check failed" fi log_info "Checking backend health..." if curl -f -s "http://$HOST:$BACKEND_PORT/health" > /dev/null; then log_success "Backend is responding" else log_warning "Backend health check failed" fi } print_summary() { log_step "📊 Deployment Summary" if [ "$DRY_RUN" = true ]; then log_info "DRY RUN completed - no changes made" return fi # Determine URLs based on target if [ "$TARGET" = "production" ]; then FRONTEND_URL="https://app.pokedex.online" BACKEND_URL="http://10.0.0.81:$BACKEND_PORT" else FRONTEND_URL="http://localhost:$PORT" BACKEND_URL="http://localhost:$BACKEND_PORT" fi echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${GREEN} 🎉 Deployment Successful!${NC}" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e " ${BLUE}Frontend:${NC} $FRONTEND_URL" echo -e " ${BLUE}Backend:${NC} $BACKEND_URL" echo -e " ${BLUE}Target:${NC} $TARGET" echo "" echo -e " ${YELLOW}Next Steps:${NC}" echo -e " • Test the application manually" if [ "$TARGET" = "local" ]; then echo -e " • Check logs: docker compose -f docker-compose.local.yml logs -f" echo -e " • Stop containers: docker compose -f docker-compose.local.yml down" else echo -e " • Check logs via SSH or Docker commands on Synology" fi echo -e " • Monitor backend: curl $BACKEND_URL/health" echo "" echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } ############################################################################### # Rollback ############################################################################### rollback() { log_step "🔄 Rollback Instructions" echo "To rollback to a previous version:" echo "" echo "1. List available backups:" echo " ls -lh backups/" echo "" echo "2. Extract backup:" echo " tar -xzf backups/backup_TIMESTAMP.tar.gz -C /tmp/restore" echo "" echo "3. Copy files back:" echo " rsync -av /tmp/restore/ ./" echo "" echo "4. Redeploy:" echo " ./deploy.sh --skip-tests --target $TARGET" echo "" echo "Or use the deployment script's built-in rollback:" echo " node ../../utils/deploy-pokedex.js --target $TARGET" echo " (will auto-rollback on failure)" } ############################################################################### # Main Execution ############################################################################### main() { echo -e "${BLUE}" echo "╔════════════════════════════════════════════════════════════╗" echo "║ ║" echo "║ Pokedex.Online Deployment Automation ║" echo "║ ║" echo "╚════════════════════════════════════════════════════════════╝" echo -e "${NC}\n" # Parse command line arguments parse_args "$@" # Run deployment pipeline check_prerequisites check_environment run_tests build_application create_backup deploy_to_server verify_deployment print_summary log_success "All done! 🚀" } # Run main function main "$@"