<# .SYNOPSIS ROA2WEB - Quick Deployment/Update Script for Windows Server .DESCRIPTION This script performs rapid deployment or updates of ROA2WEB application: - Auto-detects source path (use from scripts/ directory) - Creates backup of current deployment - Stops backend service - Updates backend and/or frontend files - Installs new Python dependencies if changed - Restarts backend service - Validates deployment health .PARAMETER InstallPath Target installation path (default: C:\inetpub\wwwroot\roa2web) .PARAMETER BackupEnabled Create backup before deployment (default: true) .PARAMETER RestartService Restart backend service after deployment (default: true) .PARAMETER UpdateBackend Update backend files (default: true) .PARAMETER UpdateFrontend Update frontend files (default: true) .EXAMPLE cd C:\Deploy\ROA2WEB\scripts .\Deploy-ROA2WEB.ps1 Deploy from current deployment package (auto-detected) .EXAMPLE .\Deploy-ROA2WEB.ps1 -UpdateBackend -UpdateFrontend:$false Update only backend files .NOTES Author: ROA2WEB Team Requires: PowerShell 5.1+, Administrator privileges Must be run from deployment package's scripts/ directory #> [CmdletBinding()] param( [string]$InstallPath = "C:\inetpub\wwwroot\roa2web", [bool]$BackupEnabled = $true, [bool]$RestartService = $true, [bool]$UpdateBackend = $true, [bool]$UpdateFrontend = $true ) $ErrorActionPreference = "Stop" # ============================================================================= # CONFIGURATION # ============================================================================= # Auto-detect source path: if running from scripts/ subdirectory, use parent $detectedSourcePath = $PSScriptRoot if ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") { $detectedSourcePath = Split-Path $PSScriptRoot -Parent } $script:Config = @{ AppName = "ROA2WEB" ServiceName = "ROA2WEB-Backend" InstallPath = $InstallPath BackendPath = Join-Path $InstallPath "backend" FrontendPath = Join-Path $InstallPath "frontend" BackupPath = Join-Path $InstallPath "backups" LogsPath = Join-Path $InstallPath "logs" SourcePath = $detectedSourcePath } # ============================================================================= # HELPER FUNCTIONS # ============================================================================= function Write-Step { param([string]$Message) Write-Host "`n[*] $Message" -ForegroundColor Cyan } function Write-Success { param([string]$Message) Write-Host " [OK] $Message" -ForegroundColor Green } function Write-Error { param([string]$Message) Write-Host " [ERROR] $Message" -ForegroundColor Red } function Write-Warning { param([string]$Message) Write-Host " [WARN] $Message" -ForegroundColor Yellow } function Test-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]$identity return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function New-BackupDirectory { if (-not (Test-Path $Config.BackupPath)) { New-Item -ItemType Directory -Path $Config.BackupPath -Force | Out-Null } } function Backup-CurrentDeployment { if (-not $BackupEnabled) { Write-Warning "Backup disabled, skipping..." return $null } Write-Step "Creating backup of current deployment..." New-BackupDirectory $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $backupName = "backup-$timestamp" $backupFullPath = Join-Path $Config.BackupPath $backupName try { # Create backup directory New-Item -ItemType Directory -Path $backupFullPath -Force | Out-Null # Backup backend if ((Test-Path $Config.BackendPath) -and $UpdateBackend) { $backupBackendPath = Join-Path $backupFullPath "backend" Copy-Item -Path $Config.BackendPath -Destination $backupBackendPath -Recurse -Force -ErrorAction SilentlyContinue Write-Success "Backend backed up" } # Backup frontend if ((Test-Path $Config.FrontendPath) -and $UpdateFrontend) { $backupFrontendPath = Join-Path $backupFullPath "frontend" Copy-Item -Path $Config.FrontendPath -Destination $backupFrontendPath -Recurse -Force -ErrorAction SilentlyContinue Write-Success "Frontend backed up" } Write-Success "Backup created at: $backupFullPath" # Clean old backups (keep last 10) $allBackups = Get-ChildItem -Path $Config.BackupPath -Directory | Sort-Object Name -Descending if ($allBackups.Count -gt 10) { $oldBackups = $allBackups | Select-Object -Skip 10 foreach ($oldBackup in $oldBackups) { Remove-Item -Path $oldBackup.FullName -Recurse -Force Write-Success "Cleaned old backup: $($oldBackup.Name)" } } return $backupFullPath } catch { Write-Error "Backup failed: $_" throw } } function Stop-BackendService { Write-Step "Stopping backend service..." try { $service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue if (-not $service) { Write-Warning "Service $($Config.ServiceName) not found" return } if ($service.Status -eq "Running") { Stop-Service -Name $Config.ServiceName -Force Start-Sleep -Seconds 2 # Wait for service to stop $timeout = 30 $elapsed = 0 while ($service.Status -ne "Stopped" -and $elapsed -lt $timeout) { Start-Sleep -Seconds 1 $service.Refresh() $elapsed++ } if ($service.Status -eq "Stopped") { Write-Success "Service stopped successfully" } else { Write-Warning "Service did not stop within timeout" } } else { Write-Success "Service already stopped" } } catch { Write-Error "Failed to stop service: $_" throw } } function Update-BackendFiles { if (-not $UpdateBackend) { Write-Warning "Backend update disabled, skipping..." return } Write-Step "Updating backend files..." $sourceBackend = Join-Path $Config.SourcePath "backend" if (-not (Test-Path $sourceBackend)) { Write-Warning "Source backend path not found: $sourceBackend" return } try { # Copy all files except .env (preserve existing config) $excludeFiles = @("*.env", "*.log", "*.pyc", "__pycache__") Get-ChildItem -Path $sourceBackend -Recurse -File | ForEach-Object { $relativePath = $_.FullName.Substring($sourceBackend.Length) $destPath = Join-Path $Config.BackendPath $relativePath # Skip excluded files $skip = $false foreach ($pattern in $excludeFiles) { if ($_.Name -like $pattern -or $_.Directory.Name -eq "__pycache__") { $skip = $true break } } if (-not $skip) { $destDir = Split-Path $destPath -Parent if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } Copy-Item -Path $_.FullName -Destination $destPath -Force } } Write-Success "Backend files updated" # Check if requirements.txt changed $sourceReq = Join-Path $sourceBackend "requirements.txt" $destReq = Join-Path $Config.BackendPath "requirements.txt" if (Test-Path $sourceReq) { $sourceHash = (Get-FileHash $sourceReq).Hash $destHash = if (Test-Path $destReq) { (Get-FileHash $destReq).Hash } else { "" } if ($sourceHash -ne $destHash) { Write-Step "Requirements changed, updating Python dependencies..." Copy-Item -Path $sourceReq -Destination $destReq -Force try { & python -m pip install -r $destReq --upgrade Write-Success "Python dependencies updated" } catch { Write-Error "Failed to update Python dependencies: $_" } } else { Write-Success "Python dependencies unchanged" } } } catch { Write-Error "Failed to update backend files: $_" throw } } function Update-FrontendFiles { if (-not $UpdateFrontend) { Write-Warning "Frontend update disabled, skipping..." return } Write-Step "Updating frontend files..." $sourceFrontend = Join-Path $Config.SourcePath "frontend" if (-not (Test-Path $sourceFrontend)) { Write-Warning "Source frontend path not found: $sourceFrontend" Write-Warning "Expected path: $sourceFrontend" return } try { # Remove old frontend files (except web.config) if (Test-Path $Config.FrontendPath) { Get-ChildItem -Path $Config.FrontendPath -Exclude "web.config" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } # Copy new frontend files Copy-Item -Path "$sourceFrontend\*" -Destination $Config.FrontendPath -Recurse -Force # Ensure web.config exists $webConfigPath = Join-Path $Config.FrontendPath "web.config" $webConfigTemplate = Join-Path (Split-Path $PSScriptRoot -Parent) "config\web.config" if (-not (Test-Path $webConfigPath) -and (Test-Path $webConfigTemplate)) { Copy-Item -Path $webConfigTemplate -Destination $webConfigPath -Force Write-Success "web.config restored from template" } Write-Success "Frontend files updated" } catch { Write-Error "Failed to update frontend files: $_" throw } } function Start-BackendService { if (-not $RestartService) { Write-Warning "Service restart disabled, skipping..." return } Write-Step "Starting backend service..." try { $service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue if (-not $service) { Write-Error "Service $($Config.ServiceName) not found" return } Start-Service -Name $Config.ServiceName Start-Sleep -Seconds 3 # Wait for service to start $timeout = 30 $elapsed = 0 while ($service.Status -ne "Running" -and $elapsed -lt $timeout) { Start-Sleep -Seconds 1 $service.Refresh() $elapsed++ } if ($service.Status -eq "Running") { Write-Success "Service started successfully" } else { Write-Error "Service failed to start (Status: $($service.Status))" Write-Warning "Check logs at: $($Config.LogsPath)\backend-stderr.log" } } catch { Write-Error "Failed to start service: $_" throw } } function Test-DeploymentHealth { Write-Step "Testing deployment health..." Start-Sleep -Seconds 5 try { # Get service port from .env or use default $envFile = Join-Path $Config.BackendPath ".env" $port = 8000 if (Test-Path $envFile) { $portLine = Get-Content $envFile | Where-Object { $_ -match "^PORT=(\d+)" } if ($portLine) { $port = [int]$matches[1] } } # Test backend health endpoint $healthUrl = "http://localhost:$port/health" $response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10 if ($response.StatusCode -eq 200) { Write-Success "Backend health check passed" Write-Success "Response: $($response.Content)" } else { Write-Warning "Health check returned status: $($response.StatusCode)" } # Test frontend (IIS) try { $frontendResponse = Invoke-WebRequest -Uri "http://localhost/" -UseBasicParsing -TimeoutSec 5 if ($frontendResponse.StatusCode -eq 200) { Write-Success "Frontend health check passed" } } catch { Write-Warning "Frontend health check failed: $_" } } catch { Write-Warning "Health check failed: $_" Write-Warning "The service may need more time to start" Write-Warning "Check logs: $($Config.LogsPath)\backend-stderr.log" } } function Show-DeploymentSummary { param([string]$BackupPath, [datetime]$StartTime) $duration = (Get-Date) - $StartTime Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan Write-Host " DEPLOYMENT COMPLETED" -ForegroundColor Green Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host "`nDeployment Summary:" -ForegroundColor Yellow Write-Host " Duration: $($duration.TotalSeconds) seconds" Write-Host " Backend Updated: $UpdateBackend" Write-Host " Frontend Updated: $UpdateFrontend" if ($BackupPath) { Write-Host " Backup Location: $BackupPath" } Write-Host "`nApplication Status:" -ForegroundColor Yellow try { $service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue if ($service) { Write-Host " Backend Service: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" }) } } catch { Write-Host " Backend Service: Unknown" -ForegroundColor Yellow } Write-Host "`nAccess Points:" -ForegroundColor Yellow Write-Host " Web Application: http://localhost" Write-Host " API Documentation: http://localhost/api/docs" Write-Host "`nManagement:" -ForegroundColor Yellow Write-Host " View Logs: Get-Content $($Config.LogsPath)\backend-stdout.log -Tail 50 -Wait" Write-Host " Restart Service: .\Restart-ROA2WEB.ps1" Write-Host " Rollback: Use backup at $BackupPath" Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan } # ============================================================================= # MAIN DEPLOYMENT FLOW # ============================================================================= function Main { Write-Host @" ==================================================================== ROA2WEB - Deployment Script Fast deployment and updates for Windows Server + IIS ==================================================================== "@ -ForegroundColor Cyan $startTime = Get-Date # Check prerequisites Write-Step "Checking prerequisites..." if (-not (Test-Administrator)) { Write-Error "This script must be run as Administrator" exit 1 } Write-Success "Running as Administrator" if (-not (Test-Path $Config.InstallPath)) { Write-Error "Installation path not found: $($Config.InstallPath)" Write-Host " Run Install-ROA2WEB.ps1 first" -ForegroundColor Yellow exit 1 } Write-Success "Installation path exists" try { # Deployment steps $backupPath = Backup-CurrentDeployment Stop-BackendService Update-BackendFiles Update-FrontendFiles Start-BackendService Test-DeploymentHealth Show-DeploymentSummary -BackupPath $backupPath -StartTime $startTime Write-Host "`nDeployment completed successfully!" -ForegroundColor Green } catch { Write-Host "`n[FATAL ERROR] Deployment failed: $_" -ForegroundColor Red Write-Host $_.ScriptStackTrace -ForegroundColor Red # Attempt rollback if backup exists if ($backupPath -and (Test-Path $backupPath)) { Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow # TODO: Implement rollback logic } exit 1 } } # Run main deployment Main