Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
497 lines
16 KiB
PowerShell
497 lines
16 KiB
PowerShell
<#
|
|
.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
|