#!/bin/bash # Desk-Moloni v3.0 Deployment Script # # Automated deployment and update script for production and staging environments. # Handles version updates, migrations, rollbacks, and environment management. set -euo pipefail # Script configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODULE_DIR="$(dirname "$SCRIPT_DIR")" PROJECT_ROOT="$(dirname "$(dirname "$MODULE_DIR")")" # Deployment configuration VERSION_FILE="$MODULE_DIR/VERSION" DEPLOYMENT_LOG="$MODULE_DIR/logs/deployment.log" BACKUP_DIR="$MODULE_DIR/backups/deployments" STAGING_DIR="$MODULE_DIR/.staging" # Default settings DEFAULT_ENVIRONMENT="production" DEFAULT_BACKUP_RETENTION=10 # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # Logging functions log_info() { local timestamp=$(date '+%Y-%m-%d %H:%M:%S') local message="[$timestamp] [INFO] $1" echo -e "${BLUE}$message${NC}" echo "$message" >> "$DEPLOYMENT_LOG" 2>/dev/null || true } log_success() { local timestamp=$(date '+%Y-%m-%d %H:%M:%S') local message="[$timestamp] [SUCCESS] $1" echo -e "${GREEN}$message${NC}" echo "$message" >> "$DEPLOYMENT_LOG" 2>/dev/null || true } log_warning() { local timestamp=$(date '+%Y-%m-%d %H:%M:%S') local message="[$timestamp] [WARNING] $1" echo -e "${YELLOW}$message${NC}" echo "$message" >> "$DEPLOYMENT_LOG" 2>/dev/null || true } log_error() { local timestamp=$(date '+%Y-%m-%d %H:%M:%S') local message="[$timestamp] [ERROR] $1" echo -e "${RED}$message${NC}" echo "$message" >> "$DEPLOYMENT_LOG" 2>/dev/null || true } log_step() { local message="$1" echo -e "\n${CYAN}${BOLD}=== $message ===${NC}\n" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [STEP] $message" >> "$DEPLOYMENT_LOG" 2>/dev/null || true } # Help function show_help() { cat << EOF Desk-Moloni v3.0 Deployment Script Usage: $0 [OPTIONS] Commands: deploy Deploy specific version update Update to latest version rollback [version] Rollback to previous or specific version status Show deployment status list-versions List available versions test-deployment Test deployment readiness maintenance-mode Enable/disable maintenance mode Options: -h, --help Show this help message -e, --environment ENV Target environment (production|staging|development) -b, --backup Create backup before deployment --no-migrate Skip database migrations --no-restart Skip service restart --force Force deployment without checks --dry-run Show what would be done without changes --retention COUNT Number of backups to retain (default: $DEFAULT_BACKUP_RETENTION) Deployment Process: 1. Pre-deployment checks 2. Create backup (if enabled) 3. Download/prepare new version 4. Run database migrations 5. Update configuration 6. Restart services 7. Post-deployment verification 8. Cleanup old backups Examples: $0 deploy 3.0.1 # Deploy specific version $0 update --backup # Update with backup $0 rollback # Rollback to previous version $0 rollback 3.0.0 # Rollback to specific version $0 maintenance-mode on # Enable maintenance mode $0 test-deployment --dry-run # Test deployment process EOF } # Parse command line arguments COMMAND="" TARGET_VERSION="" ENVIRONMENT="$DEFAULT_ENVIRONMENT" CREATE_BACKUP=false SKIP_MIGRATE=false SKIP_RESTART=false FORCE_DEPLOY=false DRY_RUN=false BACKUP_RETENTION="$DEFAULT_BACKUP_RETENTION" while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; -e|--environment) ENVIRONMENT="$2" shift 2 ;; -b|--backup) CREATE_BACKUP=true shift ;; --no-migrate) SKIP_MIGRATE=true shift ;; --no-restart) SKIP_RESTART=true shift ;; --force) FORCE_DEPLOY=true shift ;; --dry-run) DRY_RUN=true shift ;; --retention) BACKUP_RETENTION="$2" shift 2 ;; deploy|update|rollback|status|list-versions|test-deployment|maintenance-mode) COMMAND="$1" shift # Get version/mode parameter for commands that need it if [[ $# -gt 0 ]] && [[ ! "$1" =~ ^- ]]; then TARGET_VERSION="$1" shift fi ;; *) log_error "Unknown option: $1" show_help exit 1 ;; esac done # Validate command if [[ -z "$COMMAND" ]]; then log_error "No command specified" show_help exit 1 fi # Validate environment if [[ ! "$ENVIRONMENT" =~ ^(production|staging|development)$ ]]; then log_error "Invalid environment: $ENVIRONMENT" exit 1 fi # Initialize deployment environment initialize() { log_info "Initializing deployment environment: $ENVIRONMENT" # Create required directories local dirs=("$(dirname "$DEPLOYMENT_LOG")" "$BACKUP_DIR" "$STAGING_DIR") for dir in "${dirs[@]}"; do if [[ ! -d "$dir" ]]; then mkdir -p "$dir" fi done # Set environment-specific configurations case "$ENVIRONMENT" in production) log_info "Production environment - all checks enabled" ;; staging) log_info "Staging environment - relaxed checks" ;; development) log_info "Development environment - minimal checks" FORCE_DEPLOY=true # Skip most checks in development ;; esac } # Get current version get_current_version() { if [[ -f "$VERSION_FILE" ]]; then cat "$VERSION_FILE" | tr -d '\n\r' else echo "unknown" fi } # Pre-deployment checks pre_deployment_checks() { log_step "Pre-deployment Checks" local errors=0 # Check if we're running as appropriate user if [[ "$ENVIRONMENT" == "production" ]] && [[ $EUID -eq 0 ]]; then log_warning "Running as root in production (not recommended)" fi # Check disk space local disk_usage disk_usage=$(df "$MODULE_DIR" | awk 'NR==2 {print $5}' | sed 's/%//') if [[ "$disk_usage" -gt 90 ]]; then log_error "Insufficient disk space: ${disk_usage}% used" ((errors++)) else log_success "Disk space OK: ${disk_usage}% used" fi # Check if services are running check_services # Check database connectivity if [[ "$SKIP_MIGRATE" == false ]]; then if test_database_connection; then log_success "Database connection OK" else log_error "Database connection failed" ((errors++)) fi fi # Check for active queue processes if pgrep -f "queue_processor" > /dev/null; then log_warning "Queue processor is running - will be restarted after deployment" fi # Check file permissions if [[ ! -w "$MODULE_DIR" ]]; then log_error "Module directory not writable: $MODULE_DIR" ((errors++)) fi if [[ $errors -gt 0 ]] && [[ "$FORCE_DEPLOY" == false ]]; then log_error "Pre-deployment checks failed with $errors errors" log_info "Use --force to override these checks" exit 1 fi log_success "Pre-deployment checks completed" } # Test database connection test_database_connection() { php -r " require_once '$MODULE_DIR/config/bootstrap.php'; try { \$config = include '$MODULE_DIR/config/config.php'; \$pdo = new PDO( 'mysql:host=' . \$config['database']['host'] . ';dbname=' . \$config['database']['database'], \$config['database']['username'], '' // Password would be loaded from Perfex config ); echo 'SUCCESS'; } catch (Exception \$e) { echo 'FAILED'; } " 2>/dev/null | grep -q "SUCCESS" } # Check service status check_services() { # Check web server if systemctl is-active --quiet apache2 nginx 2>/dev/null; then log_success "Web server is running" else log_warning "Web server status unclear" fi # Check cron if systemctl is-active --quiet cron crond 2>/dev/null; then log_success "Cron service is running" else log_warning "Cron service may not be running" fi } # Create deployment backup create_deployment_backup() { if [[ "$CREATE_BACKUP" == false ]]; then return fi log_step "Creating Deployment Backup" local backup_name="deployment_$(date +%Y%m%d_%H%M%S)_v$(get_current_version)" local backup_path="$BACKUP_DIR/$backup_name" if [[ "$DRY_RUN" == true ]]; then log_info "Would create backup: $backup_path" return fi mkdir -p "$backup_path" # Backup database log_info "Backing up database..." if ! "$MODULE_DIR/scripts/maintenance.sh" backup-database > "$backup_path/database_backup.log" 2>&1; then log_warning "Database backup may have issues (check log)" fi # Backup module files log_info "Backing up module files..." tar -czf "$backup_path/module_files.tar.gz" -C "$(dirname "$MODULE_DIR")" "$(basename "$MODULE_DIR")" \ --exclude="logs" --exclude="cache" --exclude="temp" --exclude="backups" # Backup configuration if [[ -d "$MODULE_DIR/config" ]]; then cp -r "$MODULE_DIR/config" "$backup_path/" fi # Create manifest cat > "$backup_path/MANIFEST" << EOF Desk-Moloni Deployment Backup Created: $(date) Version: $(get_current_version) Environment: $ENVIRONMENT Command: $COMMAND $TARGET_VERSION Host: $(hostname) User: $(whoami) EOF log_success "Backup created: $backup_path" echo "$backup_path" > "$MODULE_DIR/.last_backup" } # Download and prepare new version prepare_new_version() { local version="$1" log_step "Preparing Version $version" if [[ "$DRY_RUN" == true ]]; then log_info "Would prepare version $version" return fi # Clean staging area rm -rf "$STAGING_DIR" mkdir -p "$STAGING_DIR" # In a real implementation, this would download from a repository # For now, we'll simulate version preparation log_info "Downloading version $version..." # Copy current files to staging (simulating download) cp -r "$MODULE_DIR"/* "$STAGING_DIR/" 2>/dev/null || true # Update version file in staging echo "$version" > "$STAGING_DIR/VERSION" # Set appropriate permissions find "$STAGING_DIR" -type f -name "*.php" -exec chmod 644 {} \; find "$STAGING_DIR" -type f -name "*.sh" -exec chmod +x {} \; log_success "Version $version prepared in staging" } # Run database migrations run_migrations() { if [[ "$SKIP_MIGRATE" == true ]]; then log_warning "Skipping database migrations" return fi log_step "Running Database Migrations" if [[ "$DRY_RUN" == true ]]; then log_info "Would run database migrations" return fi # Check for migration files local migration_dir="$STAGING_DIR/migrations" if [[ ! -d "$migration_dir" ]]; then log_info "No migrations directory found, skipping" return fi # Run migrations local migration_count=0 for migration_file in "$migration_dir"/*.sql; do if [[ -f "$migration_file" ]]; then local migration_name=$(basename "$migration_file") log_info "Running migration: $migration_name" if php "$CLI_DIR/sync_commands.php" migrate "$migration_file"; then log_success "Migration completed: $migration_name" ((migration_count++)) else log_error "Migration failed: $migration_name" exit 1 fi fi done if [[ $migration_count -eq 0 ]]; then log_info "No migrations to run" else log_success "$migration_count migrations completed" fi } # Deploy staged version deploy_staged_version() { local version="$1" log_step "Deploying Version $version" if [[ "$DRY_RUN" == true ]]; then log_info "Would deploy version $version from staging" return fi # Stop queue processor log_info "Stopping queue processor..." pkill -f "queue_processor" || true sleep 2 # Enable maintenance mode enable_maintenance_mode # Copy files from staging to live log_info "Copying files from staging to live..." rsync -av --delete \ --exclude="logs" \ --exclude="cache" \ --exclude="temp" \ --exclude="locks" \ --exclude="backups" \ --exclude=".staging" \ "$STAGING_DIR/" "$MODULE_DIR/" # Update permissions set_file_permissions # Clear cache clear_cache # Restart services if needed if [[ "$SKIP_RESTART" == false ]]; then restart_services fi # Disable maintenance mode disable_maintenance_mode log_success "Version $version deployed successfully" } # Set file permissions set_file_permissions() { log_info "Setting file permissions..." # Set appropriate permissions based on environment local web_user case "$ENVIRONMENT" in production) web_user="www-data" ;; *) web_user=$(whoami) ;; esac # Set ownership if [[ "$web_user" != "$(whoami)" ]] && [[ $EUID -eq 0 ]]; then chown -R "$web_user:$web_user" "$MODULE_DIR" 2>/dev/null || true fi # Set permissions find "$MODULE_DIR" -type d -exec chmod 755 {} \; find "$MODULE_DIR" -type f -name "*.php" -exec chmod 644 {} \; find "$MODULE_DIR" -type f -name "*.sh" -exec chmod +x {} \; # Ensure writable directories local writable_dirs=("logs" "cache" "temp" "locks") for dir in "${writable_dirs[@]}"; do if [[ -d "$MODULE_DIR/$dir" ]]; then chmod 775 "$MODULE_DIR/$dir" fi done } # Clear cache clear_cache() { log_info "Clearing cache..." if [[ -d "$MODULE_DIR/cache" ]]; then rm -rf "$MODULE_DIR/cache"/* fi # Clear PHP opcache if available if command -v php &> /dev/null; then php -r "if (function_exists('opcache_reset')) opcache_reset();" 2>/dev/null || true fi } # Restart services restart_services() { log_info "Restarting services..." # Restart web server (if we have permission) if [[ "$ENVIRONMENT" == "production" ]]; then if systemctl is-active --quiet apache2; then systemctl reload apache2 2>/dev/null || log_warning "Could not reload Apache" elif systemctl is-active --quiet nginx; then systemctl reload nginx 2>/dev/null || log_warning "Could not reload Nginx" fi fi # Restart queue processor log_info "Starting queue processor..." nohup php "$MODULE_DIR/cli/queue_processor.php" > "$MODULE_DIR/logs/queue_processor.log" 2>&1 & sleep 2 if pgrep -f "queue_processor" > /dev/null; then log_success "Queue processor started" else log_warning "Queue processor may not have started properly" fi } # Maintenance mode functions enable_maintenance_mode() { log_info "Enabling maintenance mode..." touch "$MODULE_DIR/.maintenance" } disable_maintenance_mode() { log_info "Disabling maintenance mode..." rm -f "$MODULE_DIR/.maintenance" } # Post-deployment verification post_deployment_verification() { log_step "Post-deployment Verification" local errors=0 # Check version was updated local deployed_version deployed_version=$(get_current_version) if [[ "$deployed_version" == "$TARGET_VERSION" ]]; then log_success "✓ Version updated: $deployed_version" else log_error "✗ Version mismatch: expected $TARGET_VERSION, got $deployed_version" ((errors++)) fi # Check file permissions if [[ -r "$MODULE_DIR/cli/queue_processor.php" ]]; then log_success "✓ Files readable" else log_error "✗ Files not readable" ((errors++)) fi # Check database connectivity if test_database_connection; then log_success "✓ Database connection OK" else log_error "✗ Database connection failed" ((errors++)) fi # Check queue processor if pgrep -f "queue_processor" > /dev/null; then log_success "✓ Queue processor running" else log_warning "⚠ Queue processor not running" fi # Run basic health check if [[ "$DRY_RUN" == false ]]; then if php "$MODULE_DIR/cli/sync_commands.php" health &>/dev/null; then log_success "✓ Health check passed" else log_warning "⚠ Health check failed (may be expected after deployment)" fi fi if [[ $errors -eq 0 ]]; then log_success "Post-deployment verification completed successfully" else log_error "Post-deployment verification found $errors errors" return 1 fi } # Cleanup old backups cleanup_old_backups() { log_step "Cleaning Up Old Backups" if [[ ! -d "$BACKUP_DIR" ]]; then return fi local backup_count backup_count=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "deployment_*" | wc -l) if [[ $backup_count -le $BACKUP_RETENTION ]]; then log_info "Backup retention OK: $backup_count backups (limit: $BACKUP_RETENTION)" return fi local to_remove=$((backup_count - BACKUP_RETENTION)) log_info "Removing $to_remove old backups (keeping $BACKUP_RETENTION most recent)" if [[ "$DRY_RUN" == true ]]; then log_info "Would remove $to_remove old backup directories" return fi # Remove oldest backups find "$BACKUP_DIR" -maxdepth 1 -type d -name "deployment_*" -printf '%T@ %p\n' | \ sort -n | head -n $to_remove | cut -d' ' -f2- | \ while read -r backup; do rm -rf "$backup" log_info "Removed old backup: $(basename "$backup")" done } # Command implementations cmd_deploy() { if [[ -z "$TARGET_VERSION" ]]; then log_error "Version required for deploy command" exit 1 fi log_step "Deploying Desk-Moloni v$TARGET_VERSION" local current_version current_version=$(get_current_version) log_info "Current version: $current_version" log_info "Target version: $TARGET_VERSION" pre_deployment_checks create_deployment_backup prepare_new_version "$TARGET_VERSION" run_migrations deploy_staged_version "$TARGET_VERSION" post_deployment_verification cleanup_old_backups log_success "🎉 Deployment of v$TARGET_VERSION completed successfully!" } cmd_update() { # In a real implementation, this would check for the latest version local latest_version="3.0.1" # This would be fetched from a repository log_info "Updating to latest version: $latest_version" TARGET_VERSION="$latest_version" cmd_deploy } cmd_rollback() { local rollback_version="$TARGET_VERSION" if [[ -z "$rollback_version" ]]; then # Get previous version from backup if [[ -f "$MODULE_DIR/.last_backup" ]]; then local last_backup last_backup=$(cat "$MODULE_DIR/.last_backup") if [[ -f "$last_backup/MANIFEST" ]]; then rollback_version=$(grep "^Version:" "$last_backup/MANIFEST" | cut -d' ' -f2) fi fi fi if [[ -z "$rollback_version" ]]; then log_error "No rollback version specified and no previous backup found" exit 1 fi log_step "Rolling Back to v$rollback_version" # Implement rollback logic (restore from backup) log_warning "Rollback functionality would be implemented here" log_info "Would rollback to version: $rollback_version" } cmd_status() { log_step "Deployment Status" local current_version current_version=$(get_current_version) echo "Current Version: $current_version" echo "Environment: $ENVIRONMENT" echo "Module Path: $MODULE_DIR" echo "Last Deployment: $(stat -c %y "$VERSION_FILE" 2>/dev/null || echo "Unknown")" # Check if maintenance mode is enabled if [[ -f "$MODULE_DIR/.maintenance" ]]; then echo "Maintenance Mode: ENABLED" else echo "Maintenance Mode: DISABLED" fi # Check service status if pgrep -f "queue_processor" > /dev/null; then echo "Queue Processor: RUNNING" else echo "Queue Processor: STOPPED" fi # Recent backups if [[ -d "$BACKUP_DIR" ]]; then local backup_count backup_count=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "deployment_*" | wc -l) echo "Available Backups: $backup_count" fi } cmd_list_versions() { log_info "Available versions (simulated):" echo "3.0.0 - Initial release" echo "3.0.1 - Bug fixes and improvements" echo "3.1.0 - New features (upcoming)" } cmd_test_deployment() { log_step "Testing Deployment Readiness" pre_deployment_checks log_info "Testing backup creation..." CREATE_BACKUP=true DRY_RUN=true create_deployment_backup log_info "Testing version preparation..." prepare_new_version "test" log_success "Deployment readiness test completed" } cmd_maintenance_mode() { local mode="$TARGET_VERSION" # Reusing TARGET_VERSION for mode parameter case "$mode" in on|enable) enable_maintenance_mode log_success "Maintenance mode enabled" ;; off|disable) disable_maintenance_mode log_success "Maintenance mode disabled" ;; *) log_error "Invalid maintenance mode: $mode (use 'on' or 'off')" exit 1 ;; esac } # Main execution main() { initialize case "$COMMAND" in deploy) cmd_deploy ;; update) cmd_update ;; rollback) cmd_rollback ;; status) cmd_status ;; list-versions) cmd_list_versions ;; test-deployment) cmd_test_deployment ;; maintenance-mode) cmd_maintenance_mode ;; *) log_error "Unknown command: $COMMAND" exit 1 ;; esac } # Error handling trap 'log_error "Deployment script failed on line $LINENO"' ERR # Execute if called directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi