Files
managing-apps/scripts/apply-migrations.sh

757 lines
37 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Apply Migrations Script (No Build, No Migration Creation)
# Usage: ./apply-migrations.sh [environment]
# Environments: Development, Sandbox, Production, Oda
set -e # Exit on any error
ENVIRONMENT=${1:-"Development"} # Default to Development for safer initial testing
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR_NAME="backups" # Just the directory name
LOGS_DIR_NAME="logs" # Just the directory name
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Create logs directory first (before LOG_FILE is used)
LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME"
mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; }
LOG_FILE="$SCRIPT_DIR/logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log"
# 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 function
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE"
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" | tee -a "$LOG_FILE"
exit 1
}
info() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" | tee -a "$LOG_FILE"
}
# --- Determine Base Paths ---
# Get the directory where the script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
log "Script is located in: $SCRIPT_DIR"
# Define absolute paths for projects and common directories relative to the script
# Assuming the project structure is:
# your_repo/
# ├── scripts/apply-migrations.sh
# └── src/
# ├── Managing.Api/
# ├── Managing.Infrastructure.Database/
# └── Managing.Docker/
PROJECT_ROOT_DIR="$(dirname "$SCRIPT_DIR")" # One level up from scripts/
SRC_DIR="$PROJECT_ROOT_DIR/src"
DB_PROJECT_PATH="$SRC_DIR/Managing.Infrastructure.Database"
API_PROJECT_PATH="$SRC_DIR/Managing.Api"
WORKERS_PROJECT_PATH="$SRC_DIR/Managing.Workers"
DOCKER_DIR="$SRC_DIR/Managing.Docker" # Adjust if your docker-compose files are elsewhere
# Define absolute path for backup directory with environment subfolder
BACKUP_DIR="$SCRIPT_DIR/$BACKUP_DIR_NAME/$ENVIRONMENT"
# --- Pre-checks and Setup ---
info "Pre-flight checks..."
command -v dotnet >/dev/null 2>&1 || error ".NET SDK is not installed. Please install .NET SDK to run this script."
command -v docker >/dev/null 2>&1 || warn "Docker is not installed. This is fine if not running Development or Oda environment with Docker."
command -v psql >/dev/null 2>&1 || warn "PostgreSQL CLI (psql) is not installed. Database connectivity checks will be skipped."
command -v pg_dump >/dev/null 2>&1 || warn "PostgreSQL pg_dump is not installed. Will use EF Core migration script for backup instead."
# Create backup directory (with environment subfolder)
mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory: $BACKUP_DIR"
log "Backup directory created/verified: $BACKUP_DIR"
log "🚀 Starting migration application for environment: $ENVIRONMENT"
# Validate environment
case $ENVIRONMENT in
"Development"|"SandboxRemote"|"ProductionRemote"|"Oda")
log "✅ Environment '$ENVIRONMENT' is valid"
;;
*)
error "❌ Invalid environment '$ENVIRONMENT'. Use: Development, SandboxRemote, ProductionRemote, or Oda"
;;
esac
# Helper function to start PostgreSQL for Development (if still using Docker Compose)
start_postgres_if_needed() {
if [ "$ENVIRONMENT" = "Development" ] || [ "$ENVIRONMENT" = "Oda" ]; then # Assuming Oda also uses local Docker
log "🔍 Checking if PostgreSQL is running for $ENVIRONMENT..."
if ! docker ps --filter "name=postgres" --format "{{.Names}}" | grep -q "postgres"; then
log "🐳 Starting PostgreSQL container for $ENVIRONMENT from $DOCKER_DIR..."
# Execute docker-compose from the DOCKER_DIR
(cd "$DOCKER_DIR" && docker-compose -f docker-compose.yml -f docker-compose.local.yml up -d postgres) || error "Failed to start PostgreSQL container."
log "⏳ Waiting for PostgreSQL to be ready (15 seconds)..."
sleep 15
else
log "✅ PostgreSQL container is already running."
fi
fi
}
# Helper function to extract connection details from appsettings
extract_connection_details() {
local appsettings_file=""
local default_appsettings=""
# For SandboxRemote and ProductionRemote, check Managing.Workers first
if [ "$ENVIRONMENT" = "SandboxRemote" ] || [ "$ENVIRONMENT" = "ProductionRemote" ]; then
appsettings_file="$WORKERS_PROJECT_PATH/appsettings.$ENVIRONMENT.json"
default_appsettings="$WORKERS_PROJECT_PATH/appsettings.json"
log "📋 Checking Managing.Workers for environment: $ENVIRONMENT"
else
appsettings_file="$API_PROJECT_PATH/appsettings.$ENVIRONMENT.json"
default_appsettings="$API_PROJECT_PATH/appsettings.json"
fi
# Try environment-specific file first, then default
if [ -f "$appsettings_file" ]; then
log "📋 Reading connection string from: $(basename "$appsettings_file")"
# Look for PostgreSql.ConnectionString first, then fallback to ConnectionString
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$appsettings_file" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$CONNECTION_STRING" ]; then
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$appsettings_file" | cut -d'"' -f4)
fi
elif [ -f "$default_appsettings" ]; then
log "📋 Reading connection string from: $(basename "$default_appsettings") (default)"
# Look for PostgreSql.ConnectionString first, then fallback to ConnectionString
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$default_appsettings" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$CONNECTION_STRING" ]; then
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$default_appsettings" | cut -d'"' -f4)
fi
else
# If Workers file not found for SandboxRemote/ProductionRemote, fallback to API
if [ "$ENVIRONMENT" = "SandboxRemote" ] || [ "$ENVIRONMENT" = "ProductionRemote" ]; then
warn "⚠️ Could not find appsettings file in Managing.Workers, trying Managing.Api..."
appsettings_file="$API_PROJECT_PATH/appsettings.$ENVIRONMENT.json"
default_appsettings="$API_PROJECT_PATH/appsettings.json"
if [ -f "$appsettings_file" ]; then
log "📋 Reading connection string from: $(basename "$appsettings_file") (fallback to API)"
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$appsettings_file" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$CONNECTION_STRING" ]; then
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$appsettings_file" | cut -d'"' -f4)
fi
elif [ -f "$default_appsettings" ]; then
log "📋 Reading connection string from: $(basename "$default_appsettings") (default, fallback to API)"
CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$default_appsettings" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4)
if [ -z "$CONNECTION_STRING" ]; then
CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$default_appsettings" | cut -d'"' -f4)
fi
else
warn "⚠️ Could not find appsettings file for environment $ENVIRONMENT"
return 1
fi
else
warn "⚠️ Could not find appsettings file for environment $ENVIRONMENT"
return 1
fi
fi
if [ -z "$CONNECTION_STRING" ]; then
error "❌ Could not extract connection string from appsettings file"
return 1
fi
log "📋 Found connection string: $CONNECTION_STRING"
# Parse connection string
DB_HOST=$(echo "$CONNECTION_STRING" | grep -o 'Host=[^;]*' | cut -d'=' -f2)
DB_PORT=$(echo "$CONNECTION_STRING" | grep -o 'Port=[^;]*' | cut -d'=' -f2)
DB_NAME=$(echo "$CONNECTION_STRING" | grep -o 'Database=[^;]*' | cut -d'=' -f2)
DB_USER=$(echo "$CONNECTION_STRING" | grep -o 'Username=[^;]*' | cut -d'=' -f2)
DB_PASSWORD=$(echo "$CONNECTION_STRING" | grep -o 'Password=[^;]*' | cut -d'=' -f2)
# Set defaults if not found
DB_HOST=${DB_HOST:-"localhost"}
DB_PORT=${DB_PORT:-"5432"}
DB_NAME=${DB_NAME:-"postgres"}
DB_USER=${DB_USER:-"postgres"}
DB_PASSWORD=${DB_PASSWORD:-"postgres"}
log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)"
}
# Helper function to get the first migration name
get_first_migration() {
local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}')
echo "$first_migration"
}
# Helper function to test PostgreSQL connectivity
test_postgres_connectivity() {
if ! command -v psql >/dev/null 2>&1; then
warn "⚠️ psql not available, skipping PostgreSQL connectivity test"
return 0
fi
log "🔍 Testing PostgreSQL connectivity with psql..."
# For remote servers or when target database might not exist, test with postgres database first
local test_database="$DB_NAME"
if [ "$TARGET_DB_EXISTS" = "false" ]; then
test_database="postgres"
log "🔍 Target database doesn't exist, testing connectivity with 'postgres' database..."
fi
# Test basic connectivity
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$test_database" -c "SELECT version();" >/dev/null 2>&1; then
log "✅ PostgreSQL connectivity test passed"
# Get database info
log "📊 Database Information:"
DB_INFO=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$test_database" -t -c "
SELECT
'Database: ' || current_database() || ' (Size: ' || pg_size_pretty(pg_database_size(current_database())) || ')',
'PostgreSQL Version: ' || version(),
'Connection: ' || inet_server_addr() || ':' || inet_server_port()
" 2>/dev/null | tr '\n' ' ')
log " $DB_INFO"
# Only check migrations if we're testing the actual target database
if [ "$test_database" = "$DB_NAME" ]; then
# Check if __EFMigrationsHistory table exists
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\dt __EFMigrationsHistory" >/dev/null 2>&1; then
log "✅ EF Core migrations history table exists"
# Count applied migrations
MIGRATION_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM \"__EFMigrationsHistory\";" 2>/dev/null | tr -d ' ')
log "📋 Applied migrations count: $MIGRATION_COUNT"
# Show recent migrations
if [ "$MIGRATION_COUNT" -gt 0 ]; then
log "📋 Recent migrations:"
RECENT_MIGRATIONS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "
SELECT \"MigrationId\" FROM \"__EFMigrationsHistory\"
ORDER BY \"MigrationId\" DESC
LIMIT 5;
" 2>/dev/null | sed 's/^/ /')
echo "$RECENT_MIGRATIONS"
fi
else
warn "⚠️ EF Core migrations history table not found - database may be empty"
fi
else
log "📋 Connectivity test completed using 'postgres' database (target database will be created)"
fi
return 0
else
error "❌ PostgreSQL connectivity test failed"
error " Host: $DB_HOST, Port: $DB_PORT, Database: $test_database, User: $DB_USER"
return 1
fi
}
# --- Core Logic ---
# No global 'cd' needed here. All paths are now absolute.
# This makes the script much more robust to where it's executed from.
# Set ASPNETCORE_ENVIRONMENT to load the correct appsettings
export ASPNETCORE_ENVIRONMENT="$ENVIRONMENT"
log "ASPNETCORE_ENVIRONMENT set to: $ASPNETCORE_ENVIRONMENT"
# If Development or Oda, start local PostgreSQL
start_postgres_if_needed
# Extract connection details from appsettings
extract_connection_details
# Step 1: Check Database Connection and Create if Needed
log "🔧 Step 1: Checking database connection and creating database if needed..."
# Log the environment and expected connection details (for user info, still relies on appsettings)
log "🔧 Using environment: $ENVIRONMENT"
log "📋 Connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER)"
# Initial connectivity check - test if we can reach the database server
log "🔍 Step 1a: Testing basic database server connectivity..."
if command -v psql >/dev/null 2>&1; then
# Test if we can connect to the postgres database (which should always exist)
log "🔍 Connecting to default 'postgres' database to verify server connectivity..."
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT 1;" >/dev/null 2>&1; then
log "✅ Database server connectivity test passed"
# Check if our target database exists
log "🔍 Checking if target database '$DB_NAME' exists..."
DB_EXISTS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME';" 2>/dev/null | tr -d ' ')
if [ "$DB_EXISTS" = "1" ]; then
log "✅ Target database '$DB_NAME' exists"
TARGET_DB_EXISTS=true
else
log "⚠️ Target database '$DB_NAME' does not exist - will be created"
TARGET_DB_EXISTS=false
fi
else
error "❌ Database server connectivity test failed"
error " Cannot reach PostgreSQL server at $DB_HOST:$DB_PORT with database 'postgres'"
error " Please verify:"
error " - Database server is running"
error " - Network connectivity to $DB_HOST:$DB_PORT"
error " - Credentials are correct (user: $DB_USER)"
error " - Firewall settings allow connections"
error " - The 'postgres' database exists (default PostgreSQL database)"
fi
else
# Fallback: try to connect using EF Core to test basic connectivity
log "🔄 psql not available, testing connectivity via EF Core..."
if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then
log "✅ Database server connectivity test passed (via EF Core)"
TARGET_DB_EXISTS=true # Assume it exists if EF Core can connect
else
warn "⚠️ Could not verify database server connectivity (psql not available)"
warn " Proceeding with caution - connectivity will be tested during migration"
TARGET_DB_EXISTS=false # Assume it doesn't exist if EF Core can't connect
fi
fi
log "🔍 Step 1b: Testing database connection and checking if database exists via EF CLI..."
# Test connection by listing migrations. If it fails, the database likely doesn't exist or is inaccessible.
# Execute dotnet ef from DB_PROJECT_PATH for correct context, but pass API_PROJECT_PATH as startup.
# Since we assume projects are already built, we can safely use --no-build flag for faster execution
if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then
log "✅ EF Core database connection successful and database appears to exist."
# Now test with psql for additional verification (this will use postgres db if target doesn't exist)
test_postgres_connectivity
# If psql connectivity test fails, stop the migration
if [ $? -ne 0 ]; then
error "❌ PostgreSQL connectivity test failed. Migration aborted for safety."
error " Please verify your database connection and try again."
fi
else
# Database doesn't exist or connection failed
if [ "$TARGET_DB_EXISTS" = "false" ]; then
log "📝 Database '$DB_NAME' does not exist. Creating database and applying migrations..."
# Test connectivity with postgres database first (since target doesn't exist)
test_postgres_connectivity
# If connectivity test fails, stop the migration
if [ $? -ne 0 ]; then
error "❌ PostgreSQL connectivity test failed. Cannot proceed with database creation."
error " Please verify your connection settings and try again."
fi
# Step 1: Create the database first
log "🔧 Step 1: Creating database '$DB_NAME'..."
if command -v psql >/dev/null 2>&1; then
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "CREATE DATABASE \"$DB_NAME\";" >/dev/null 2>&1; then
log "✅ Database '$DB_NAME' created successfully"
else
error "❌ Failed to create database '$DB_NAME'"
error " Please verify you have sufficient privileges to create databases."
fi
else
warn "⚠️ psql not available, attempting to create database via EF Core..."
# EF Core will attempt to create the database during update
fi
# Step 2: Generate migration script for the new database
log "📝 Step 2: Generating migration script for new database..."
TEMP_MIGRATION_SCRIPT="$BACKUP_DIR/temp_migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$TEMP_MIGRATION_SCRIPT"); then
log "✅ Migration script generated successfully: $(basename "$TEMP_MIGRATION_SCRIPT")"
# Step 3: Apply the migration script to the new database
log "🔧 Step 3: Applying migration script to new database..."
if command -v psql >/dev/null 2>&1; then
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$TEMP_MIGRATION_SCRIPT" >/dev/null 2>&1; then
log "✅ Migration script applied successfully to new database"
else
error "❌ Failed to apply migration script to newly created database"
fi
else
# Fallback to EF Core database update
log "🔄 psql not available, using EF Core to apply migrations..."
if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then
log "✅ Database created and initialized successfully using EF Core"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true )
error "❌ Failed to create and initialize database using EF Core."
error " EF CLI Output: $ERROR_OUTPUT"
fi
fi
# Clean up temporary migration script
rm -f "$TEMP_MIGRATION_SCRIPT"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$TEMP_MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate migration script."
error " EF CLI Output: $ERROR_OUTPUT"
fi
else
warn "⚠️ Database connection failed but database may exist. Attempting to update existing database..."
# Try to update the existing database
if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then
log "✅ Database updated successfully"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true )
error "❌ Failed to update database."
error " EF CLI Output: $ERROR_OUTPUT"
error " This usually means the connection string in your .NET project's appsettings.$ENVIRONMENT.json is incorrect,"
error " or the database server is not running/accessible for environment '$ENVIRONMENT'."
fi
fi
# Test connectivity after creation/update
test_postgres_connectivity
# If connectivity test fails after creation, stop the migration
if [ $? -ne 0 ]; then
error "❌ PostgreSQL connectivity test failed after database creation. Migration aborted for safety."
error " Database may have been created but is not accessible. Please verify your connection settings."
fi
fi
# Final verification of connection
log "🔍 Verifying database connection post-creation/update..."
if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then
log "✅ Database connectivity verification passed."
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true )
error "❌ Final database connectivity verification failed."
error " EF CLI Output: $ERROR_OUTPUT"
error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'."
fi
# Step 2: Create database backup (only if database exists)
log "📦 Step 2: Checking if database backup is needed..."
# Check if the target database exists
DB_EXISTS=false
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then
DB_EXISTS=true
log "✅ Target database '$DB_NAME' exists"
else
log " Target database '$DB_NAME' does not exist - skipping backup"
fi
# Ask user if they want to create a backup
CREATE_BACKUP=false
if [ "$DB_EXISTS" = "true" ]; then
echo ""
echo "=========================================="
echo "📦 DATABASE BACKUP"
echo "=========================================="
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo "Environment: $ENVIRONMENT"
echo ""
echo "Would you like to create a backup before proceeding?"
echo "⚠️ It is highly recommended to create a backup for safety."
echo "=========================================="
echo ""
read -p "🔧 Create database backup? (y/n, default: y): " create_backup
create_backup=${create_backup:-y} # Default to 'y' if user just presses Enter
if [[ "$create_backup" =~ ^[Yy]$ ]]; then
log "✅ User chose to create backup - proceeding with backup"
CREATE_BACKUP=true
else
warn "⚠️ User chose to skip backup - proceeding without backup"
warn " This is not recommended. Proceed at your own risk!"
CREATE_BACKUP=false
fi
fi
if [ "$DB_EXISTS" = "true" ] && [ "$CREATE_BACKUP" = "true" ]; then
# Define the actual backup file path (absolute)
BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
# Backup file display path (relative to script execution)
BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql"
# Create backup with retry logic
BACKUP_SUCCESS=false
for attempt in 1 2 3; do
log "Backup attempt $attempt/3..."
# Create real database backup using pg_dump
if command -v pg_dump >/dev/null 2>&1; then
if PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" --no-password --verbose --clean --if-exists --create --format=plain > "$BACKUP_FILE" 2>/dev/null; then
log "✅ Database backup created using pg_dump: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
# If pg_dump fails, fall back to EF Core migration script
warn "⚠️ pg_dump failed, falling back to EF Core migration script..."
# Generate complete backup script (all migrations from beginning)
log "📋 Generating complete backup script (all migrations)..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
warn " EF CLI Output: $ERROR_OUTPUT"
sleep 5
else
error "❌ Database backup failed after 3 attempts."
error " EF CLI Output: $ERROR_OUTPUT"
error " Migration aborted for safety reasons."
fi
fi
fi
else
# If pg_dump is not available, use EF Core migration script
warn "⚠️ pg_dump not available, using EF Core migration script for backup..."
# Generate complete backup script (all migrations from beginning)
log "📋 Generating complete backup script (all migrations)..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then
log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY"
BACKUP_SUCCESS=true
break
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true)
if [ $attempt -lt 3 ]; then
warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..."
warn " EF CLI Output: $ERROR_OUTPUT"
sleep 5
else
error "❌ Database backup failed after 3 attempts."
error " EF CLI Output: $ERROR_OUTPUT"
error " Migration aborted for safety reasons."
fi
fi
fi
done
# Check if backup was successful before proceeding
if [ "$BACKUP_SUCCESS" != "true" ]; then
error "❌ Database backup failed. Migration aborted for safety."
error " Cannot proceed with migration without a valid backup."
error " Please resolve backup issues and try again."
fi
fi
# Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply)
log "🔄 Step 3: Running database migration (final application of pending migrations)..."
# Check if database exists and create it if needed before applying migrations
log "🔍 Step 3a: Ensuring target database exists..."
if [ "$TARGET_DB_EXISTS" = "false" ]; then
log "🔧 Creating database '$DB_NAME'..."
if command -v psql >/dev/null 2>&1; then
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "CREATE DATABASE \"$DB_NAME\";" >/dev/null 2>&1; then
log "✅ Database '$DB_NAME' created successfully"
else
error "❌ Failed to create database '$DB_NAME'"
error " Please verify you have sufficient privileges to create databases."
fi
else
error "❌ psql not available, cannot create database. Please create database '$DB_NAME' manually."
fi
fi
# Generate migration script first (Microsoft recommended approach)
MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql"
log "📝 Step 3b: Generating migration script for pending migrations..."
# Check if database is empty (no tables) to determine the best approach
log "🔍 Checking if database has existing tables..."
DB_HAS_TABLES=false
if command -v psql >/dev/null 2>&1; then
TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0")
if [ "$TABLE_COUNT" -gt 0 ]; then
DB_HAS_TABLES=true
log "✅ Database has $TABLE_COUNT existing tables - using idempotent script generation"
else
log "⚠️ Database appears to be empty - using full migration script generation"
fi
else
log "⚠️ psql not available - assuming database has tables and using idempotent script generation"
DB_HAS_TABLES=true
fi
# Generate migration script based on database state
if [ "$DB_HAS_TABLES" = "true" ]; then
# For databases with existing tables, generate a complete idempotent script
log "📝 Generating complete migration script (idempotent) for database with existing tables..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate complete migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
fi
else
# Use full script generation for empty databases (generate script from the very beginning)
log "📝 Generating full migration script for empty database..."
if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then
log "✅ Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")"
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to generate complete migration script."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
fi
fi
# Show the migration script path to the user for review
echo ""
echo "=========================================="
echo "📋 MIGRATION SCRIPT READY FOR REVIEW"
echo "=========================================="
echo "Generated script: $MIGRATION_SCRIPT"
echo "Environment: $ENVIRONMENT"
echo "Database: $DB_HOST:$DB_PORT/$DB_NAME"
echo ""
# Show a preview of the migration script content
if [ -f "$MIGRATION_SCRIPT" ]; then
SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT")
echo "📄 Migration script contains $SCRIPT_SIZE lines"
# Show last 20 lines as preview
echo ""
echo "📋 PREVIEW (last 20 lines):"
echo "----------------------------------------"
tail -20 "$MIGRATION_SCRIPT" | sed 's/^/ /'
if [ "$SCRIPT_SIZE" -gt 20 ]; then
echo " ... (showing last 20 lines of $SCRIPT_SIZE total)"
fi
echo "----------------------------------------"
echo ""
fi
echo "⚠️ IMPORTANT: Please review the migration script before proceeding!"
echo " You can examine the full script with: cat $MIGRATION_SCRIPT"
echo " Or open it in your editor to review the changes."
echo ""
echo "=========================================="
echo ""
# Ask for user confirmation
read -p "🔍 Have you reviewed the migration script and want to proceed? Type 'yes' to continue: " user_confirmation
if [ "$user_confirmation" != "yes" ]; then
log "❌ Migration cancelled by user."
log " Migration script is available at: $(basename "$MIGRATION_SCRIPT")"
log " You can apply it manually later with:"
log " PGPASSWORD=\"$DB_PASSWORD\" psql -h \"$DB_HOST\" -p \"$DB_PORT\" -U \"$DB_USER\" -d \"$DB_NAME\" -f \"$MIGRATION_SCRIPT\""
exit 0
fi
log "✅ User confirmed migration. Proceeding with database update..."
# Apply the migration script using psql (recommended approach)
log "🔧 Step 3c: Applying migration script to database..."
if command -v psql >/dev/null 2>&1; then
if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$MIGRATION_SCRIPT" >/dev/null 2>&1; then
log "✅ Migration script applied successfully to database"
else
ERROR_OUTPUT=$( (PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$MIGRATION_SCRIPT") 2>&1 || true )
error "❌ Failed to apply migration script to database"
error " PSQL Output: $ERROR_OUTPUT"
error " Migration script available at: $(basename "$MIGRATION_SCRIPT")"
fi
else
# Fallback to EF Core database update if psql is not available
log "🔄 psql not available, falling back to EF Core database update..."
if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then
log "✅ Database migration completed successfully using EF Core."
else
ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true )
error "❌ Database migration failed during final update."
error " EF CLI Output: $ERROR_OUTPUT"
error " Check the .NET project logs for detailed errors."
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
error " Backup script available at: $BACKUP_FILE_DISPLAY"
fi
fi
fi
# Save a copy of the migration script for reference before cleaning up
MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql"
if [ -f "$MIGRATION_SCRIPT" ]; then
cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY"
log "📝 Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")"
fi
# Clean up temporary migration script after successful application
rm -f "$MIGRATION_SCRIPT"
# Step 4: Verify Migration
log "🔍 Step 4: Verifying migration status..."
# List migrations to check applied status
MIGRATION_LIST_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 )
log "📋 Current migration status:\n$MIGRATION_LIST_OUTPUT"
# Check if there are any pending migrations after update
PENDING_MIGRATIONS=$(echo "$MIGRATION_LIST_OUTPUT" | grep -c "\[ \]" || echo "0")
PENDING_MIGRATIONS=$(echo "$PENDING_MIGRATIONS" | tr -d '\n') # Remove any newlines
if [ "$PENDING_MIGRATIONS" -gt 0 ]; then
warn "⚠️ WARNING: $PENDING_MIGRATIONS pending migration(s) found after update."
warn " This indicates the 'dotnet ef database update' command may not have fully completed."
else
log "✅ All migrations appear to be applied successfully."
fi
# --- Step 5: Cleanup Backups (keep only 5 dumps max) ---
log "🧹 Step 5: Cleaning up old backups..."
# Keep only the last 5 backups for this environment (in the environment-specific subfolder)
ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_backup_*.sql 2>/dev/null | tail -n +6 | xargs -r rm -f || true # Added -f for force removal
log "✅ Kept last 5 backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$ENVIRONMENT/"
log "🎉 Migration application completed successfully for environment: $ENVIRONMENT!"
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
log "📁 EF Core Migration SQL Script: $BACKUP_FILE_DISPLAY"
fi
log "📝 Full Log file: $LOG_FILE"
echo ""
echo "=========================================="
echo "📋 MIGRATION SUMMARY"
echo "=========================================="
echo "Environment: $ENVIRONMENT"
echo "Timestamp: $TIMESTAMP"
echo "Status: ✅ SUCCESS"
if [ "$CREATE_BACKUP" = "true" ] && [ -n "$BACKUP_FILE_DISPLAY" ]; then
echo "EF Core SQL Backup: $BACKUP_FILE_DISPLAY"
else
echo "Database Backup: Skipped by user"
fi
echo "Log: $LOG_FILE"
echo "=========================================="