#!/bin/bash # Application Deployment Script for Custom PHP Framework # Integrates Docker Compose with Ansible infrastructure deployment # Usage: ./deploy-app.sh [staging|production] [options] set -euo pipefail # Script directory and project root SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../../" && pwd)" DEPLOYMENT_DIR="${PROJECT_ROOT}/deployment" APPLICATIONS_DIR="${DEPLOYMENT_DIR}/applications" # Default configuration DEFAULT_ENV="staging" DRY_RUN=false SKIP_TESTS=false SKIP_BACKUP=false FORCE_DEPLOY=false VERBOSE=false # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging functions log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" } warn() { echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $1${NC}" } error() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" } debug() { if [ "$VERBOSE" = true ]; then echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] DEBUG: $1${NC}" fi } # Usage information show_usage() { cat << EOF Usage: $0 [environment] [options] Environment: staging Deploy to staging environment (default) production Deploy to production environment Options: --dry-run Show what would be done without making changes --skip-tests Skip running tests before deployment --skip-backup Skip database backup (not recommended for production) --force Force deployment even if validation fails --verbose Enable verbose output -h, --help Show this help message Examples: $0 staging # Deploy to staging $0 production --skip-tests # Deploy to production without tests $0 staging --dry-run --verbose # Dry run with detailed output EOF } # Parse command line arguments parse_arguments() { local environment="" while [[ $# -gt 0 ]]; do case $1 in staging|production) environment="$1" shift ;; --dry-run) DRY_RUN=true shift ;; --skip-tests) SKIP_TESTS=true shift ;; --skip-backup) SKIP_BACKUP=true shift ;; --force) FORCE_DEPLOY=true shift ;; --verbose) VERBOSE=true shift ;; -h|--help) show_usage exit 0 ;; *) error "Unknown argument: $1" show_usage exit 1 ;; esac done # Set environment, defaulting to staging DEPLOY_ENV="${environment:-$DEFAULT_ENV}" } # Validate environment and prerequisites validate_environment() { log "Validating deployment environment: $DEPLOY_ENV" # Check if we're in the project root if [[ ! -f "${PROJECT_ROOT}/docker-compose.yml" ]]; then error "Project root not found. Please run from the correct directory." exit 1 fi # Validate environment-specific files exist local compose_file="${APPLICATIONS_DIR}/docker-compose.${DEPLOY_ENV}.yml" local env_file="${APPLICATIONS_DIR}/environments/.env.${DEPLOY_ENV}" if [[ ! -f "$compose_file" ]]; then error "Docker Compose overlay not found: $compose_file" exit 1 fi if [[ ! -f "$env_file" ]]; then error "Environment file not found: $env_file" error "Copy from template: cp ${env_file}.template $env_file" exit 1 fi # Check for required tools local required_tools=("docker" "docker-compose" "ansible-playbook") for tool in "${required_tools[@]}"; do if ! command -v "$tool" &> /dev/null; then error "Required tool not found: $tool" exit 1 fi done debug "Environment validation completed" } # Validate environment configuration validate_configuration() { log "Validating environment configuration" local env_file="${APPLICATIONS_DIR}/environments/.env.${DEPLOY_ENV}" # Check for required placeholder values local required_placeholders=( "*** REQUIRED" ) for placeholder in "${required_placeholders[@]}"; do if grep -q "$placeholder" "$env_file"; then error "Environment file contains unfilled templates:" grep "$placeholder" "$env_file" || true if [ "$FORCE_DEPLOY" != true ]; then error "Fix configuration or use --force to proceed" exit 1 else warn "Proceeding with incomplete configuration due to --force flag" fi fi done debug "Configuration validation completed" } # Run pre-deployment tests run_tests() { if [ "$SKIP_TESTS" = true ]; then warn "Skipping tests as requested" return 0 fi log "Running pre-deployment tests" cd "$PROJECT_ROOT" # PHP tests if [[ -f "vendor/bin/pest" ]]; then log "Running PHP tests with Pest" ./vendor/bin/pest --bail elif [[ -f "vendor/bin/phpunit" ]]; then log "Running PHP tests with PHPUnit" ./vendor/bin/phpunit --stop-on-failure else warn "No PHP test framework found" fi # JavaScript tests if [[ -f "package.json" ]] && command -v npm &> /dev/null; then log "Running JavaScript tests" npm test fi # Code quality checks if [[ -f "composer.json" ]]; then log "Running code style checks" composer cs || { error "Code style checks failed" if [ "$FORCE_DEPLOY" != true ]; then exit 1 else warn "Proceeding despite code style issues due to --force flag" fi } fi debug "Tests completed successfully" } # Create database backup create_backup() { if [ "$SKIP_BACKUP" = true ]; then warn "Skipping database backup as requested" return 0 fi log "Creating database backup for $DEPLOY_ENV" local backup_dir="${PROJECT_ROOT}/storage/backups" local timestamp=$(date +%Y%m%d_%H%M%S) local backup_file="${backup_dir}/db_backup_${DEPLOY_ENV}_${timestamp}.sql" mkdir -p "$backup_dir" # Load environment variables set -a source "${APPLICATIONS_DIR}/environments/.env.${DEPLOY_ENV}" set +a if [ "$DRY_RUN" != true ]; then # Create backup using the running database container docker-compose -f "${PROJECT_ROOT}/docker-compose.yml" \ -f "${APPLICATIONS_DIR}/docker-compose.${DEPLOY_ENV}.yml" \ exec -T db \ mariadb-dump -u root -p"${DB_ROOT_PASSWORD}" --all-databases > "$backup_file" log "Database backup created: $backup_file" else debug "DRY RUN: Would create backup at $backup_file" fi } # Build application assets build_assets() { log "Building application assets" cd "$PROJECT_ROOT" if [[ -f "package.json" ]] && command -v npm &> /dev/null; then log "Installing Node.js dependencies" if [ "$DRY_RUN" != true ]; then npm ci else debug "DRY RUN: Would run npm ci" fi log "Building production assets" if [ "$DRY_RUN" != true ]; then npm run build else debug "DRY RUN: Would run npm run build" fi else warn "No package.json found, skipping asset build" fi debug "Asset building completed" } # Deploy infrastructure using Ansible deploy_infrastructure() { log "Deploying infrastructure with Ansible" local ansible_dir="${DEPLOYMENT_DIR}/infrastructure" local inventory="${ansible_dir}/inventories/${DEPLOY_ENV}/hosts.yml" local site_playbook="${ansible_dir}/site.yml" if [[ ! -f "$inventory" ]]; then warn "Ansible inventory not found: $inventory" warn "Skipping infrastructure deployment" return 0 fi cd "$ansible_dir" # First run the main site playbook for infrastructure setup local ansible_cmd="ansible-playbook -i $inventory $site_playbook" if [ "$DRY_RUN" = true ]; then ansible_cmd="$ansible_cmd --check" fi if [ "$VERBOSE" = true ]; then ansible_cmd="$ansible_cmd -v" fi debug "Running infrastructure setup: $ansible_cmd" if [ "$DRY_RUN" != true ]; then $ansible_cmd log "Infrastructure setup completed" else debug "DRY RUN: Would run Ansible infrastructure setup" fi } # Deploy application using Ansible deploy_application() { log "Deploying application with Ansible" local ansible_dir="${DEPLOYMENT_DIR}/infrastructure" local inventory="${ansible_dir}/inventories/${DEPLOY_ENV}/hosts.yml" local app_playbook="${ansible_dir}/playbooks/deploy-application.yml" if [[ ! -f "$inventory" ]]; then warn "Ansible inventory not found: $inventory" warn "Skipping application deployment" return 0 fi if [[ ! -f "$app_playbook" ]]; then error "Application deployment playbook not found: $app_playbook" exit 1 fi cd "$ansible_dir" # Run the application deployment playbook with proper environment variable local ansible_cmd="ansible-playbook -i $inventory $app_playbook -e environment=$DEPLOY_ENV" if [ "$DRY_RUN" = true ]; then ansible_cmd="$ansible_cmd --check" fi if [ "$VERBOSE" = true ]; then ansible_cmd="$ansible_cmd -v" fi debug "Running application deployment: $ansible_cmd" if [ "$DRY_RUN" != true ]; then $ansible_cmd log "Application deployment completed" else debug "DRY RUN: Would run Ansible application deployment" fi } # Run post-deployment tasks post_deployment() { log "Running post-deployment tasks" cd "$PROJECT_ROOT" local compose_files="-f docker-compose.yml -f ${APPLICATIONS_DIR}/docker-compose.${DEPLOY_ENV}.yml" local env_file="--env-file ${APPLICATIONS_DIR}/environments/.env.${DEPLOY_ENV}" if [ "$DRY_RUN" != true ]; then # Run database migrations log "Running database migrations" docker-compose $compose_files $env_file exec -T php php console.php db:migrate # Clear application cache log "Clearing application cache" docker-compose $compose_files $env_file exec -T php php console.php cache:clear || true # Warm up application log "Warming up application" sleep 5 # Run health checks "${SCRIPT_DIR}/health-check.sh" "$DEPLOY_ENV" else debug "DRY RUN: Would run post-deployment tasks" fi debug "Post-deployment tasks completed" } # Main deployment function main() { log "Starting deployment to $DEPLOY_ENV environment" if [ "$DRY_RUN" = true ]; then log "DRY RUN MODE - No actual changes will be made" fi # Deployment steps validate_environment validate_configuration run_tests create_backup build_assets deploy_infrastructure deploy_application post_deployment log "Deployment to $DEPLOY_ENV completed successfully!" if [ "$DEPLOY_ENV" = "production" ]; then log "Production deployment complete. Monitor application health and performance." else log "Staging deployment complete. Ready for testing." fi } # Parse arguments and run main function parse_arguments "$@" main