Spaces:
Running
Running
| param( | |
| [string]$PythonPath = "", | |
| [string]$ImageTag = "openenv-rl:predeploy", | |
| [int]$ContainerPort = 8786, | |
| [int]$StartupTimeoutSec = 120, | |
| [switch]$Quick, | |
| [switch]$SkipFrontendBuild, | |
| [switch]$SkipDockerBuild, | |
| [switch]$SkipDockerRuntime, | |
| [switch]$SkipOpenEnvCli | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = "Stop" | |
| $script:StepResults = New-Object System.Collections.Generic.List[object] | |
| function Add-StepResult { | |
| param( | |
| [string]$Name, | |
| [string]$Status, | |
| [double]$DurationSec, | |
| [string]$Detail = "" | |
| ) | |
| $script:StepResults.Add([pscustomobject]@{ | |
| Step = $Name | |
| Status = $Status | |
| DurationSec = [Math]::Round($DurationSec, 2) | |
| Detail = $Detail | |
| }) | Out-Null | |
| } | |
| function Show-Summary { | |
| Write-Host "" | |
| Write-Host "==============================================" | |
| Write-Host "Pre-Deploy E2E Summary" | |
| Write-Host "==============================================" | |
| $table = $script:StepResults | Select-Object Step, Status, DurationSec, Detail | |
| if ($table.Count -gt 0) { | |
| $table | Format-Table -AutoSize | Out-String | Write-Host | |
| } | |
| $failed = @($script:StepResults | Where-Object { $_.Status -eq "FAILED" }) | |
| if ($failed.Count -gt 0) { | |
| Write-Host "Result: FAILED ($($failed.Count) step(s) failed)" -ForegroundColor Red | |
| } | |
| else { | |
| Write-Host "Result: PASSED (all checks succeeded)" -ForegroundColor Green | |
| } | |
| } | |
| function Ensure-CommandExists { | |
| param([string[]]$Candidates) | |
| foreach ($candidate in $Candidates) { | |
| $cmd = Get-Command $candidate -ErrorAction SilentlyContinue | |
| if ($null -ne $cmd) { | |
| return $cmd.Source | |
| } | |
| } | |
| throw "Required command not found. Tried: $($Candidates -join ', ')" | |
| } | |
| function Resolve-PythonExe { | |
| param([string]$RequestedPath) | |
| if ($RequestedPath) { | |
| if (Test-Path $RequestedPath) { | |
| return (Resolve-Path $RequestedPath).Path | |
| } | |
| throw "PythonPath was provided but not found: $RequestedPath" | |
| } | |
| $candidatePaths = @( | |
| ".venv313\\Scripts\\python.exe", | |
| ".venv\\Scripts\\python.exe" | |
| ) | |
| foreach ($candidate in $candidatePaths) { | |
| if (Test-Path $candidate) { | |
| return (Resolve-Path $candidate).Path | |
| } | |
| } | |
| $pythonCmd = Get-Command python.exe -ErrorAction SilentlyContinue | |
| if ($null -ne $pythonCmd) { | |
| return $pythonCmd.Source | |
| } | |
| throw "Could not resolve Python interpreter. Provide -PythonPath explicitly." | |
| } | |
| function Invoke-CheckedCommand { | |
| param( | |
| [string]$Executable, | |
| [string[]]$Arguments | |
| ) | |
| Write-Host "-> $Executable $($Arguments -join ' ')" | |
| & $Executable @Arguments | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Command failed with exit code $LASTEXITCODE: $Executable $($Arguments -join ' ')" | |
| } | |
| } | |
| function Invoke-Step { | |
| param( | |
| [string]$Name, | |
| [scriptblock]$Action | |
| ) | |
| Write-Host "" | |
| Write-Host "=== $Name ===" -ForegroundColor Cyan | |
| $sw = [System.Diagnostics.Stopwatch]::StartNew() | |
| try { | |
| & $Action | |
| $sw.Stop() | |
| Add-StepResult -Name $Name -Status "PASSED" -DurationSec $sw.Elapsed.TotalSeconds | |
| Write-Host "[PASS] $Name" -ForegroundColor Green | |
| } | |
| catch { | |
| $sw.Stop() | |
| Add-StepResult -Name $Name -Status "FAILED" -DurationSec $sw.Elapsed.TotalSeconds -Detail $_.Exception.Message | |
| Write-Host "[FAIL] $Name" -ForegroundColor Red | |
| Write-Host "Reason: $($_.Exception.Message)" -ForegroundColor Red | |
| Show-Summary | |
| throw | |
| } | |
| } | |
| function Wait-ForHealth { | |
| param( | |
| [string]$HealthUrl, | |
| [int]$TimeoutSec | |
| ) | |
| $deadline = (Get-Date).AddSeconds($TimeoutSec) | |
| $lastError = "No response yet" | |
| while ((Get-Date) -lt $deadline) { | |
| try { | |
| $response = Invoke-RestMethod -Method Get -Uri $HealthUrl -TimeoutSec 5 | |
| return $response | |
| } | |
| catch { | |
| $lastError = $_.Exception.Message | |
| Start-Sleep -Seconds 2 | |
| } | |
| } | |
| throw "Timed out waiting for container health endpoint at $HealthUrl. Last error: $lastError" | |
| } | |
| $repoRoot = Split-Path -Parent $PSScriptRoot | |
| Set-Location $repoRoot | |
| Write-Host "Repo root: $repoRoot" | |
| $resolvedPython = $null | |
| $npmExecutable = $null | |
| $dockerExecutable = $null | |
| Invoke-Step -Name "Resolve toolchain" -Action { | |
| $resolvedPython = Resolve-PythonExe -RequestedPath $PythonPath | |
| Write-Host "Python: $resolvedPython" | |
| if (-not $SkipFrontendBuild) { | |
| $npmExecutable = Ensure-CommandExists -Candidates @("npm.cmd", "npm") | |
| Write-Host "NPM: $npmExecutable" | |
| } | |
| if (-not $SkipDockerBuild -or -not $SkipDockerRuntime) { | |
| $dockerExecutable = Ensure-CommandExists -Candidates @("docker") | |
| Write-Host "Docker: $dockerExecutable" | |
| } | |
| } | |
| Invoke-Step -Name "Python syntax and import sanity" -Action { | |
| Invoke-CheckedCommand -Executable $resolvedPython -Arguments @("-m", "compileall", "app", "rl", "scripts", "tests") | |
| Invoke-CheckedCommand -Executable $resolvedPython -Arguments @("-c", "import fastapi, uvicorn; print('python runtime ok')") | |
| } | |
| Invoke-Step -Name "OpenEnv manifest and import validation" -Action { | |
| $args = @("scripts/validate_env.py", "--repo", ".") | |
| if ($SkipOpenEnvCli) { | |
| $args += "--skip-openenv-cli" | |
| } | |
| Invoke-CheckedCommand -Executable $resolvedPython -Arguments $args | |
| } | |
| Invoke-Step -Name "Deterministic smoke baseline" -Action { | |
| Invoke-CheckedCommand -Executable $resolvedPython -Arguments @("scripts/smoke_test.py") | |
| } | |
| Invoke-Step -Name "API contract E2E suite" -Action { | |
| Invoke-CheckedCommand -Executable $resolvedPython -Arguments @("-m", "pytest", "tests/test_api_end_to_end_suite.py", "-v", "--tb=short") | |
| } | |
| if (-not $Quick) { | |
| Invoke-Step -Name "Core API and environment regression tests" -Action { | |
| Invoke-CheckedCommand -Executable $resolvedPython -Arguments @( | |
| "-m", "pytest", | |
| "tests/test_phase1_models.py", | |
| "tests/test_phase1_sector_and_tasks.py", | |
| "tests/test_phase1_event_engine.py", | |
| "tests/test_phase1_signal_computer.py", | |
| "tests/test_phase2_env_integration.py", | |
| "tests/test_phase2_simulator.py", | |
| "tests/test_phase2_api.py", | |
| "tests/test_live_simulation_e2e.py", | |
| "tests/test_action_mask.py", | |
| "-v", | |
| "--tb=short" | |
| ) | |
| } | |
| } | |
| if (-not $SkipFrontendBuild) { | |
| Invoke-Step -Name "Frontend install and production build" -Action { | |
| Invoke-CheckedCommand -Executable $npmExecutable -Arguments @("--prefix", "frontend/react", "ci", "--no-audit", "--no-fund") | |
| Invoke-CheckedCommand -Executable $npmExecutable -Arguments @("--prefix", "frontend/react", "run", "build") | |
| } | |
| } | |
| if (-not $SkipDockerBuild) { | |
| Invoke-Step -Name "Docker image build" -Action { | |
| Invoke-CheckedCommand -Executable $dockerExecutable -Arguments @("build", "-t", $ImageTag, ".") | |
| } | |
| } | |
| if (-not $SkipDockerRuntime) { | |
| Invoke-Step -Name "Docker runtime endpoint sanity" -Action { | |
| $containerName = "openenv-preflight-" + [Guid]::NewGuid().ToString("N").Substring(0, 8) | |
| $healthUrl = "http://127.0.0.1:$ContainerPort/health" | |
| $baseUrl = "http://127.0.0.1:$ContainerPort" | |
| $containerStarted = $false | |
| try { | |
| $runOutput = & $dockerExecutable run -d --rm --name $containerName -p "$ContainerPort`:7860" $ImageTag | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Failed to start Docker container $containerName" | |
| } | |
| $containerStarted = $true | |
| Write-Host "Container: $containerName" | |
| Write-Host "Container ID: $($runOutput | Select-Object -Last 1)" | |
| $health = Wait-ForHealth -HealthUrl $healthUrl -TimeoutSec $StartupTimeoutSec | |
| if ($health.status -notin @("ok", "degraded")) { | |
| throw "Unexpected health status: $($health.status)" | |
| } | |
| $resetBody = @{ task_id = "district_backlog_easy"; seed = 42 } | ConvertTo-Json | |
| $reset = Invoke-RestMethod -Method Post -Uri "$baseUrl/reset" -ContentType "application/json" -Body $resetBody -TimeoutSec 20 | |
| if (-not $reset.session_id) { | |
| throw "Reset response missing session_id" | |
| } | |
| $stepBody = @{ | |
| session_id = $reset.session_id | |
| action = @{ action_type = "advance_time" } | |
| } | ConvertTo-Json -Depth 5 | |
| $step = Invoke-RestMethod -Method Post -Uri "$baseUrl/step" -ContentType "application/json" -Body $stepBody -TimeoutSec 20 | |
| if (-not $step.observation) { | |
| throw "Step response missing observation" | |
| } | |
| $gradeBody = @{ session_id = $reset.session_id } | ConvertTo-Json | |
| $grade = Invoke-RestMethod -Method Post -Uri "$baseUrl/grade" -ContentType "application/json" -Body $gradeBody -TimeoutSec 20 | |
| $score = [double]$grade.score | |
| if ($score -lt 0.0 -or $score -gt 1.0) { | |
| throw "Grade score out of range: $score" | |
| } | |
| Write-Host "Health status: $($health.status)" | |
| Write-Host "Session ID: $($reset.session_id)" | |
| Write-Host "Grade score: $score" | |
| } | |
| finally { | |
| if ($containerStarted) { | |
| try { | |
| & $dockerExecutable stop $containerName | Out-Null | |
| } | |
| catch { | |
| Write-Warning "Failed to stop container $containerName: $($_.Exception.Message)" | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Show-Summary | |
| Write-Host "Pre-deployment E2E checks completed successfully." -ForegroundColor Green | |
| exit 0 | |