<# .SYNOPSIS ROA2WEB Telegram Bot - Quick Deployment/Update Script for Windows Server .DESCRIPTION This script performs rapid deployment or updates of ROA2WEB Telegram Bot: - Auto-detects source path (use from scripts/ directory) - Creates backup of current deployment (app files + database) - Stops bot service - Updates application files - Installs new Python dependencies if changed - Preserves .env configuration - Restarts bot service - Validates deployment health - Rollback support on failure .PARAMETER InstallPath Target installation path (default: C:\inetpub\wwwroot\roa2web\telegram-bot) .PARAMETER SourcePath Source path for deployment package (auto-detected if run from scripts/) .PARAMETER BackupEnabled Create backup before deployment (default: true) .PARAMETER RestartService Restart bot service after deployment (default: true) .PARAMETER RollbackOnFailure Automatically rollback if deployment fails (default: true) .EXAMPLE cd C:\Deploy\TelegramBot\scripts .\Deploy-TelegramBot.ps1 Deploy from current deployment package (auto-detected) .EXAMPLE .\Deploy-TelegramBot.ps1 -SourcePath "C:\Deploy\new-version" Deploy from specific source path .EXAMPLE .\Deploy-TelegramBot.ps1 -BackupEnabled $false -RestartService $false Update files without backup or restart (manual testing) .NOTES Author: ROA2WEB Team Requires: PowerShell 5.1+, Administrator privileges Recommended to run from deployment package's scripts/ directory #> [CmdletBinding()] param( [string]$InstallPath = "C:\inetpub\wwwroot\roa2web\telegram-bot", [string]$SourcePath = "", [bool]$BackupEnabled = $true, [bool]$RestartService = $true, [bool]$RollbackOnFailure = $true ) $ErrorActionPreference = "Stop" # ============================================================================= # CONFIGURATION # ============================================================================= # Auto-detect source path: if running from scripts/ subdirectory, use parent $detectedSourcePath = if ($SourcePath) { $SourcePath } elseif ((Split-Path $PSScriptRoot -Leaf) -eq "scripts") { Split-Path $PSScriptRoot -Parent } else { $PSScriptRoot } $script:Config = @{ AppName = "ROA2WEB-TelegramBot" ServiceName = "ROA2WEB-TelegramBot" InstallPath = $InstallPath DataPath = Join-Path $InstallPath "data" BackupPath = Join-Path $InstallPath "backups" LogsPath = Join-Path $InstallPath "logs" SourcePath = $detectedSourcePath } $script:DeploymentState = @{ BackupPath = $null ServiceWasRunning = $false DeploymentSuccess = $false } # ============================================================================= # 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 app directory if (Test-Path (Join-Path $Config.InstallPath "app")) { $backupAppPath = Join-Path $backupFullPath "app" Copy-Item -Path (Join-Path $Config.InstallPath "app") -Destination $backupAppPath -Recurse -Force Write-Success "App files backed up" } # Backup requirements.txt $reqFile = Join-Path $Config.InstallPath "requirements.txt" if (Test-Path $reqFile) { Copy-Item -Path $reqFile -Destination (Join-Path $backupFullPath "requirements.txt") -Force Write-Success "Requirements file backed up" } # Backup .env file $envFile = Join-Path $Config.InstallPath ".env" if (Test-Path $envFile) { Copy-Item -Path $envFile -Destination (Join-Path $backupFullPath ".env") -Force Write-Success ".env file backed up" } # Backup database $dbFile = Join-Path $Config.DataPath "telegram_bot.db" if (Test-Path $dbFile) { $backupDataPath = Join-Path $backupFullPath "data" New-Item -ItemType Directory -Path $backupDataPath -Force | Out-Null Copy-Item -Path $dbFile -Destination (Join-Path $backupDataPath "telegram_bot.db") -Force Write-Success "Database backed up" } Write-Success "Backup created at: $backupFullPath" # Clean old backups (keep last 10) $allBackups = Get-ChildItem -Path $Config.BackupPath -Directory | Where-Object { $_.Name -like "backup-*" } | 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-BotService { Write-Step "Stopping Telegram bot service..." try { $service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue if (-not $service) { Write-Warning "Service $($Config.ServiceName) not found" $DeploymentState.ServiceWasRunning = $false return } if ($service.Status -eq "Running") { $DeploymentState.ServiceWasRunning = $true 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" $DeploymentState.ServiceWasRunning = $false } } catch { Write-Error "Failed to stop service: $_" throw } } function Update-ApplicationFiles { Write-Step "Updating application files..." $sourceApp = Join-Path $Config.SourcePath "app" if (-not (Test-Path $sourceApp)) { throw "Source app directory not found: $sourceApp" } try { # Remove old app directory $destApp = Join-Path $Config.InstallPath "app" if (Test-Path $destApp) { Remove-Item -Path $destApp -Recurse -Force Write-Success "Removed old app directory" } # Copy new app files Copy-Item -Path $sourceApp -Destination $destApp -Recurse -Force Write-Success "Application files updated" # Update requirements.txt if present $sourceReq = Join-Path $Config.SourcePath "requirements.txt" $destReq = Join-Path $Config.InstallPath "requirements.txt" if (Test-Path $sourceReq) { $sourceHash = (Get-FileHash $sourceReq -Algorithm SHA256).Hash $destHash = if (Test-Path $destReq) { (Get-FileHash $destReq -Algorithm SHA256).Hash } else { "" } if ($sourceHash -ne $destHash) { Write-Step "Requirements changed, updating Python dependencies..." Copy-Item -Path $sourceReq -Destination $destReq -Force # Use virtual environment pip $venvPath = Join-Path $Config.InstallPath "venv" $pipPath = Join-Path $venvPath "Scripts\pip.exe" if (Test-Path $pipPath) { try { & $pipPath install -r $destReq --upgrade Write-Success "Python dependencies updated" } catch { Write-Error "Failed to update Python dependencies: $_" throw } } else { Write-Warning "Virtual environment not found, skipping dependency update" } } else { Write-Success "Python dependencies unchanged" } } # Preserve .env file (never overwrite) $envFile = Join-Path $Config.InstallPath ".env" if (-not (Test-Path $envFile)) { $sourceEnv = Join-Path $Config.SourcePath ".env.example" if (Test-Path $sourceEnv) { Copy-Item -Path $sourceEnv -Destination $envFile -Force Write-Warning "Created .env from .env.example - PLEASE CONFIGURE" } } else { Write-Success ".env file preserved (not overwritten)" } # Update management scripts $sourceScripts = Join-Path $Config.SourcePath "scripts" if (Test-Path $sourceScripts) { $destScripts = Join-Path $Config.InstallPath "scripts" if (-not (Test-Path $destScripts)) { New-Item -ItemType Directory -Path $destScripts -Force | Out-Null } # List of management scripts to update $managementScripts = @( "Start-TelegramBot.ps1", "Stop-TelegramBot.ps1", "Restart-TelegramBot.ps1", "Backup-TelegramDB.ps1", "Setup-DailyBackup.ps1", "Setup-ClaudeAuth.ps1" ) $updatedScriptsCount = 0 foreach ($script in $managementScripts) { $sourcePath = Join-Path $sourceScripts $script if (Test-Path $sourcePath) { $destPath = Join-Path $destScripts $script Copy-Item -Path $sourcePath -Destination $destPath -Force $updatedScriptsCount++ } } if ($updatedScriptsCount -gt 0) { Write-Success "Updated $updatedScriptsCount management scripts" } } } catch { Write-Error "Failed to update application files: $_" throw } } function Start-BotService { if (-not $RestartService) { Write-Warning "Service restart disabled, skipping..." return } Write-Step "Starting Telegram bot service..." try { $service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue if (-not $service) { Write-Warning "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 { throw "Service failed to start (Status: $($service.Status))" } } 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.InstallPath ".env" $port = 8002 if (Test-Path $envFile) { $envContent = Get-Content $envFile $portLine = $envContent | Where-Object { $_ -match "^INTERNAL_API_PORT=(\d+)" } if ($portLine -and $matches[1]) { $port = [int]$matches[1] } } $healthUrl = "http://localhost:$port/internal/health" $response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 10 if ($response.StatusCode -eq 200) { $content = $response.Content | ConvertFrom-Json Write-Success "Health check passed: $($content.status)" Write-Success "Database: $($content.database.status)" return $true } else { throw "Health check returned status code: $($response.StatusCode)" } } catch { Write-Error "Health check failed: $_" return $false } } function Restore-FromBackup { param([string]$BackupPath) if (-not $BackupPath -or -not (Test-Path $BackupPath)) { Write-Error "Cannot rollback: backup path not found ($BackupPath)" return $false } Write-Step "Rolling back to backup: $BackupPath" try { # Stop service Stop-BotService # Restore app directory $backupApp = Join-Path $BackupPath "app" $destApp = Join-Path $Config.InstallPath "app" if (Test-Path $backupApp) { if (Test-Path $destApp) { Remove-Item -Path $destApp -Recurse -Force } Copy-Item -Path $backupApp -Destination $destApp -Recurse -Force Write-Success "App files restored" } # Restore requirements.txt $backupReq = Join-Path $BackupPath "requirements.txt" if (Test-Path $backupReq) { Copy-Item -Path $backupReq -Destination (Join-Path $Config.InstallPath "requirements.txt") -Force Write-Success "Requirements file restored" } # Restore database $backupDb = Join-Path $BackupPath "data\telegram_bot.db" if (Test-Path $backupDb) { Copy-Item -Path $backupDb -Destination (Join-Path $Config.DataPath "telegram_bot.db") -Force Write-Success "Database restored" } # Restart service if ($DeploymentState.ServiceWasRunning) { Start-BotService } Write-Success "Rollback completed successfully" return $true } catch { Write-Error "Rollback failed: $_" return $false } } function Show-DeploymentSummary { Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan if ($DeploymentState.DeploymentSuccess) { Write-Host " DEPLOYMENT COMPLETED SUCCESSFULLY" -ForegroundColor Green } else { Write-Host " DEPLOYMENT FAILED" -ForegroundColor Red } Write-Host ("=" * 80) -ForegroundColor Cyan Write-Host "`nDeployment Details:" -ForegroundColor Yellow Write-Host " Install Path: $($Config.InstallPath)" Write-Host " Source Path: $($Config.SourcePath)" Write-Host " Backup Created: $(if ($DeploymentState.BackupPath) { 'Yes' } else { 'No' })" if ($DeploymentState.BackupPath) { Write-Host " Backup Location: $($DeploymentState.BackupPath)" } $service = Get-Service -Name $Config.ServiceName -ErrorAction SilentlyContinue if ($service) { Write-Host " Service Status: $($service.Status)" -ForegroundColor $(if ($service.Status -eq "Running") { "Green" } else { "Red" }) } if ($DeploymentState.DeploymentSuccess) { Write-Host "`nNext Steps:" -ForegroundColor Yellow Write-Host " - Monitor logs: Get-Content $($Config.LogsPath)\stdout.log -Tail 50 -Wait" Write-Host " - Check health: Invoke-WebRequest http://localhost:8002/internal/health" Write-Host " - Test bot on Telegram" } else { Write-Host "`nTroubleshooting:" -ForegroundColor Yellow Write-Host " - Check logs: Get-Content $($Config.LogsPath)\stderr.log -Tail 100" Write-Host " - Verify .env configuration" if ($DeploymentState.BackupPath -and $RollbackOnFailure) { Write-Host " - Rollback completed automatically to: $($DeploymentState.BackupPath)" } elseif ($DeploymentState.BackupPath) { Write-Host " - Manual rollback available at: $($DeploymentState.BackupPath)" } } Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan } # ============================================================================= # MAIN DEPLOYMENT FLOW # ============================================================================= function Main { Write-Host @" ==================================================================== ROA2WEB Telegram Bot - Deployment Script Quick deployment and update automation ==================================================================== "@ -ForegroundColor Cyan # Check prerequisites Write-Step "Checking prerequisites..." if (-not (Test-Administrator)) { Write-Error "This script must be run as Administrator" Write-Host " Right-click PowerShell and select 'Run as Administrator'" -ForegroundColor Yellow 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-TelegramBot.ps1 first" -ForegroundColor Yellow exit 1 } Write-Success "Installation path verified" if (-not (Test-Path $Config.SourcePath)) { Write-Error "Source path not found: $($Config.SourcePath)" exit 1 } Write-Success "Source path verified: $($Config.SourcePath)" try { # Deployment steps $DeploymentState.BackupPath = Backup-CurrentDeployment Stop-BotService Update-ApplicationFiles Start-BotService $healthOk = Test-DeploymentHealth if ($healthOk) { $DeploymentState.DeploymentSuccess = $true Write-Host "`nDeployment completed successfully!" -ForegroundColor Green } else { throw "Health check failed after deployment" } } catch { Write-Host "`n[DEPLOYMENT FAILED] $_" -ForegroundColor Red if ($RollbackOnFailure -and $DeploymentState.BackupPath) { Write-Host "`nAttempting automatic rollback..." -ForegroundColor Yellow $rollbackOk = Restore-FromBackup -BackupPath $DeploymentState.BackupPath if ($rollbackOk) { Write-Host "Rollback completed successfully" -ForegroundColor Yellow } else { Write-Host "Rollback failed - manual intervention required" -ForegroundColor Red } } else { Write-Host "Automatic rollback disabled or no backup available" -ForegroundColor Yellow } $DeploymentState.DeploymentSuccess = $false } finally { Show-DeploymentSummary } if (-not $DeploymentState.DeploymentSuccess) { exit 1 } } # Run main deployment Main