param( [string]$ArtifactRoot = "", [switch]$SkipFrontendBuild, [switch]$SkipFrontendLint, [switch]$SkipBackendTests, [switch]$SkipDiffCheck ) $ErrorActionPreference = "Stop" $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss" if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { $ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\deployment-preflight" } if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) { $ArtifactRoot = Join-Path $repoRoot $ArtifactRoot } $artifactDir = Join-Path $ArtifactRoot $timestamp $summaryJson = Join-Path $artifactDir "deployment-preflight-summary.json" $summaryMd = Join-Path $artifactDir "deployment-preflight-summary.md" New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null $checks = New-Object System.Collections.Generic.List[object] function Add-Check { param( [string]$Key, [string]$Label, [ValidateSet("pass", "fail", "skip")] [string]$Status, [string]$Detail, [bool]$Required = $true, [hashtable]$Meta = @{} ) $checks.Add([ordered]@{ key = $Key label = $Label status = $Status required = $Required detail = $Detail meta = $Meta }) | Out-Null } function Save-Json { param( [string]$Path, [object]$Value ) $Value | ConvertTo-Json -Depth 12 | Set-Content -Encoding UTF8 -Path $Path } function Invoke-PreflightStep { param( [string]$Key, [string]$Label, [string]$Command, [string[]]$Arguments, [string]$WorkingDirectory ) $logPath = Join-Path $artifactDir "$Key.log" $previousErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { Push-Location -LiteralPath $WorkingDirectory try { $output = & $Command @Arguments 2>&1 $exitCode = $LASTEXITCODE } finally { Pop-Location } } finally { $ErrorActionPreference = $previousErrorActionPreference } $outputText = ($output -join [Environment]::NewLine) $outputText | Set-Content -Encoding UTF8 -Path $logPath if ($exitCode -eq 0) { Add-Check -Key $Key -Label $Label -Status "pass" -Detail "$Label passed" -Meta @{ log = $logPath } } else { Add-Check -Key $Key -Label $Label -Status "fail" -Detail "$Label failed with exit code $exitCode" -Meta @{ exit_code = $exitCode log = $logPath working_directory = $WorkingDirectory } } } $branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim() $commit = (git -C $repoRoot rev-parse HEAD).Trim() if ($SkipBackendTests) { Add-Check -Key "backend_tests" -Label "backend deployment tests" -Status "skip" -Required $false -Detail "Skipped by -SkipBackendTests" } else { Invoke-PreflightStep ` -Key "backend_tests" ` -Label "backend deployment tests" ` -Command "python" ` -Arguments @("-m", "pytest", "backend/tests/test_system_api.py", "backend/tests/test_contracts_api.py", "backend/tests/test_search_service.py", "backend/tests/test_search_api.py") ` -WorkingDirectory $repoRoot } if ($SkipFrontendLint) { Add-Check -Key "frontend_lint" -Label "frontend lint" -Status "skip" -Required $false -Detail "Skipped by -SkipFrontendLint" } else { Invoke-PreflightStep ` -Key "frontend_lint" ` -Label "frontend lint" ` -Command "npm" ` -Arguments @("--prefix", "frontend", "run", "lint") ` -WorkingDirectory $repoRoot } if ($SkipFrontendBuild) { Add-Check -Key "frontend_build" -Label "frontend production build" -Status "skip" -Required $false -Detail "Skipped by -SkipFrontendBuild" } else { Invoke-PreflightStep ` -Key "frontend_build" ` -Label "frontend production build" ` -Command "npm" ` -Arguments @("--prefix", "frontend", "run", "build") ` -WorkingDirectory $repoRoot } if ($SkipDiffCheck) { Add-Check -Key "diff_check" -Label "git diff whitespace check" -Status "skip" -Required $false -Detail "Skipped by -SkipDiffCheck" } else { Invoke-PreflightStep ` -Key "diff_check" ` -Label "git diff whitespace check" ` -Command "git" ` -Arguments @("-C", $repoRoot, "diff", "--check") ` -WorkingDirectory $repoRoot } $failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" }) $skippedList = @($checks | Where-Object { $_.status -eq "skip" }) $overallStatus = if ($failedRequired.Count -gt 0) { "FAIL" } else { "PASS" } $summary = [ordered]@{ name = "deployment-preflight" status = $overallStatus generated_at = (Get-Date).ToUniversalTime().ToString("o") artifact_dir = $artifactDir branch = $branch commit = $commit checks = $checks totals = [ordered]@{ total = $checks.Count failed_required = $failedRequired.Count skipped = $skippedList.Count } } Save-Json -Path $summaryJson -Value $summary $markdown = New-Object System.Collections.Generic.List[string] $markdown.Add("# Deployment Preflight") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("- Status: $overallStatus") | Out-Null $markdown.Add("- Generated at: $($summary.generated_at)") | Out-Null $markdown.Add("- Branch: $branch") | Out-Null $markdown.Add("- Commit: $commit") | Out-Null $markdown.Add("- Artifact dir: $artifactDir") | Out-Null $markdown.Add("") | Out-Null $markdown.Add("| Check | Required | Status | Detail |") | Out-Null $markdown.Add("|---|---:|---|---|") | Out-Null foreach ($check in $checks) { $markdown.Add("| $($check.label) | $($check.required) | $($check.status.ToUpperInvariant()) | $($check.detail) |") | Out-Null } $markdown.Add("") | Out-Null $markdown.Add("Logs:") | Out-Null $markdown.Add("") | Out-Null foreach ($check in $checks) { if ($check.meta.log) { $markdown.Add(('- {0}: `{1}`' -f $check.label, $check.meta.log)) | Out-Null } } $markdown | Set-Content -Encoding UTF8 -Path $summaryMd Write-Host "DEPLOYMENT_PREFLIGHT_$overallStatus $artifactDir" if ($overallStatus -eq "FAIL") { throw "Deployment preflight failed. See $summaryMd" }