Files
desk-moloni/scripts/deploy.sh
Emanuel Almeida c19f6fd9ee fix(perfexcrm module): align version to 3.0.1, unify entrypoint, and harden routes/views
- Bump DESK_MOLONI version to 3.0.1 across module
- Normalize hooks to after_client_* and instantiate PerfexHooks safely
- Fix OAuthController view path and API client class name
- Add missing admin views for webhook config/logs; adjust view loading
- Harden client portal routes and admin routes mapping
- Make Dashboard/Logs/Queue tolerant to optional model methods
- Align log details query with existing schema; avoid broken joins

This makes the module operational in Perfex (admin + client), reduces 404s,
and avoids fatal errors due to inconsistent tables/methods.
2025-09-11 17:38:45 +01:00

835 lines
23 KiB
Bash

#!/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 <command> [OPTIONS]
Commands:
deploy <version> 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 <on|off> 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