278 lines
9.8 KiB
PowerShell
278 lines
9.8 KiB
PowerShell
param(
|
|
[string]$ArtifactRoot = "",
|
|
[switch]$SkipBuild
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
$backendDir = Join-Path $repoRoot "backend"
|
|
$frontendDir = Join-Path $repoRoot "frontend"
|
|
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase5-dependency-governance"
|
|
}
|
|
if (-not [System.IO.Path]::IsPathRooted($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot $ArtifactRoot
|
|
}
|
|
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
|
$summaryJson = Join-Path $artifactDir "dependency-governance-summary.json"
|
|
$summaryMd = Join-Path $artifactDir "dependency-governance-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", "warn")]
|
|
[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-CommandWithLog {
|
|
param(
|
|
[string]$Name,
|
|
[string]$Command,
|
|
[string]$WorkingDirectory
|
|
)
|
|
|
|
$logPath = Join-Path $artifactDir "$Name.log"
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
Push-Location $WorkingDirectory
|
|
try {
|
|
$output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -Command $Command 2>&1
|
|
$exitCode = $LASTEXITCODE
|
|
} finally {
|
|
Pop-Location
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
($output -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $logPath
|
|
return [ordered]@{
|
|
exit_code = $exitCode
|
|
log = $logPath
|
|
}
|
|
}
|
|
|
|
function Resolve-PythonCommand {
|
|
param(
|
|
[string]$ArtifactDir
|
|
)
|
|
|
|
$candidates = @(
|
|
(Join-Path $repoRoot "backend\.venv\Scripts\python.exe"),
|
|
(Join-Path $repoRoot ".venv\Scripts\python.exe")
|
|
)
|
|
foreach ($candidate in $candidates) {
|
|
if (Test-Path -LiteralPath $candidate) {
|
|
return [ordered]@{
|
|
command = "& `"$candidate`""
|
|
detail = "using project virtual environment: $candidate"
|
|
setup_log = $null
|
|
}
|
|
}
|
|
}
|
|
|
|
$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "ai-contract-platform-dependency-governance"
|
|
$tempVenv = Join-Path $tempRoot "backend-pip-check-venv"
|
|
$tempPython = Join-Path $tempVenv "Scripts\python.exe"
|
|
$setupLog = Join-Path $ArtifactDir "backend-pip-check-venv-setup.log"
|
|
$requirements = Join-Path $backendDir "requirements.txt"
|
|
|
|
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
|
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
Push-Location $backendDir
|
|
try {
|
|
$setupOutput = @()
|
|
if (-not (Test-Path -LiteralPath $tempPython)) {
|
|
$setupOutput += & python -m venv $tempVenv 2>&1
|
|
$setupExit = $LASTEXITCODE
|
|
} else {
|
|
$setupExit = 0
|
|
}
|
|
if ($setupExit -eq 0) {
|
|
$setupOutput += & $tempPython -m pip install --upgrade pip 2>&1
|
|
$setupExit = $LASTEXITCODE
|
|
}
|
|
if ($setupExit -eq 0) {
|
|
$setupOutput += & $tempPython -m pip install -r $requirements 2>&1
|
|
$setupExit = $LASTEXITCODE
|
|
}
|
|
} finally {
|
|
Pop-Location
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
($setupOutput -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $setupLog
|
|
|
|
if ($setupExit -ne 0) {
|
|
return [ordered]@{
|
|
command = "python"
|
|
detail = "temporary backend dependency environment setup failed"
|
|
setup_log = $setupLog
|
|
setup_exit_code = $setupExit
|
|
}
|
|
}
|
|
|
|
return [ordered]@{
|
|
command = "& `"$tempPython`""
|
|
detail = "using temporary backend dependency environment: $tempVenv"
|
|
setup_log = $setupLog
|
|
setup_exit_code = 0
|
|
}
|
|
}
|
|
|
|
$auditPath = Join-Path $artifactDir "npm-audit.json"
|
|
Push-Location $frontendDir
|
|
try {
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
$ErrorActionPreference = "Continue"
|
|
try {
|
|
$auditOutput = npm.cmd audit --json 2>&1
|
|
$auditExit = $LASTEXITCODE
|
|
} finally {
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
($auditOutput -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $auditPath
|
|
$auditJson = Get-Content -Raw -Encoding UTF8 $auditPath | ConvertFrom-Json
|
|
$auditTotals = $auditJson.metadata.vulnerabilities
|
|
$totalVulnerabilities = [int]$auditTotals.total
|
|
$highCritical = [int]$auditTotals.high + [int]$auditTotals.critical
|
|
if ($auditExit -eq 0 -and $totalVulnerabilities -eq 0) {
|
|
Add-Check -Key "npm_audit" -Label "npm audit" -Status "pass" -Detail "npm audit reports 0 vulnerabilities" -Meta @{
|
|
artifact = $auditPath
|
|
}
|
|
} elseif ($highCritical -eq 0) {
|
|
Add-Check -Key "npm_audit" -Label "npm audit" -Status "warn" -Detail "npm audit has no high/critical vulnerabilities but still reports lower risks" -Meta @{
|
|
total = $totalVulnerabilities
|
|
moderate = [int]$auditTotals.moderate
|
|
artifact = $auditPath
|
|
}
|
|
} else {
|
|
Add-Check -Key "npm_audit" -Label "npm audit" -Status "fail" -Detail "npm audit reports high/critical vulnerabilities" -Meta @{
|
|
high = [int]$auditTotals.high
|
|
critical = [int]$auditTotals.critical
|
|
artifact = $auditPath
|
|
}
|
|
}
|
|
|
|
$npmLs = Invoke-CommandWithLog -Name "npm-ls-next-postcss" -Command "npm.cmd ls next postcss eslint-config-next" -WorkingDirectory $frontendDir
|
|
if ($npmLs.exit_code -eq 0) {
|
|
Add-Check -Key "npm_dependency_tree" -Label "npm dependency tree" -Status "pass" -Detail "next/postcss/eslint-config-next tree resolved" -Meta @{
|
|
log = $npmLs.log
|
|
}
|
|
} else {
|
|
Add-Check -Key "npm_dependency_tree" -Label "npm dependency tree" -Status "fail" -Detail "npm dependency tree command failed" -Meta @{
|
|
log = $npmLs.log
|
|
}
|
|
}
|
|
|
|
$lint = Invoke-CommandWithLog -Name "frontend-lint" -Command "npm.cmd run lint" -WorkingDirectory $frontendDir
|
|
if ($lint.exit_code -eq 0) {
|
|
Add-Check -Key "frontend_lint" -Label "frontend lint" -Status "pass" -Detail "npm run lint passed" -Meta @{ log = $lint.log }
|
|
} else {
|
|
Add-Check -Key "frontend_lint" -Label "frontend lint" -Status "fail" -Detail "npm run lint failed" -Meta @{ log = $lint.log }
|
|
}
|
|
|
|
if ($SkipBuild) {
|
|
Add-Check -Key "frontend_build" -Label "frontend build" -Status "warn" -Required $false -Detail "Skipped by -SkipBuild"
|
|
} else {
|
|
$build = Invoke-CommandWithLog -Name "frontend-build" -Command "npm.cmd run build" -WorkingDirectory $frontendDir
|
|
if ($build.exit_code -eq 0) {
|
|
Add-Check -Key "frontend_build" -Label "frontend build" -Status "pass" -Detail "npm run build passed" -Meta @{ log = $build.log }
|
|
} else {
|
|
Add-Check -Key "frontend_build" -Label "frontend build" -Status "fail" -Detail "npm run build failed" -Meta @{ log = $build.log }
|
|
}
|
|
}
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
|
|
$pythonForPipCheck = Resolve-PythonCommand -ArtifactDir $artifactDir
|
|
if ($pythonForPipCheck.setup_exit_code -and $pythonForPipCheck.setup_exit_code -ne 0) {
|
|
Add-Check -Key "backend_pip_check" -Label "backend pip check" -Status "fail" -Detail $pythonForPipCheck.detail -Meta @{
|
|
setup_log = $pythonForPipCheck.setup_log
|
|
setup_exit_code = $pythonForPipCheck.setup_exit_code
|
|
}
|
|
} else {
|
|
$pipCheck = Invoke-CommandWithLog -Name "backend-pip-check" -Command "$($pythonForPipCheck.command) -m pip check" -WorkingDirectory $repoRoot
|
|
if ($pipCheck.exit_code -eq 0) {
|
|
Add-Check -Key "backend_pip_check" -Label "backend pip check" -Status "pass" -Detail "pip check passed; $($pythonForPipCheck.detail)" -Meta @{
|
|
log = $pipCheck.log
|
|
setup_log = $pythonForPipCheck.setup_log
|
|
}
|
|
} else {
|
|
Add-Check -Key "backend_pip_check" -Label "backend pip check" -Status "fail" -Detail "pip check failed; $($pythonForPipCheck.detail)" -Meta @{
|
|
log = $pipCheck.log
|
|
setup_log = $pythonForPipCheck.setup_log
|
|
}
|
|
}
|
|
}
|
|
|
|
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
$warningsList = @($checks | Where-Object { $_.status -eq "warn" })
|
|
$overallStatus = if ($failedRequired.Count -gt 0) { "FAIL" } elseif ($warningsList.Count -gt 0) { "WARN" } else { "PASS" }
|
|
|
|
$summary = [ordered]@{
|
|
name = "phase5-dependency-governance"
|
|
status = $overallStatus
|
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
artifact_dir = $artifactDir
|
|
checks = $checks
|
|
totals = [ordered]@{
|
|
total = $checks.Count
|
|
failed_required = $failedRequired.Count
|
|
warnings = $warningsList.Count
|
|
}
|
|
}
|
|
Save-Json -Path $summaryJson -Value $summary
|
|
|
|
$markdown = New-Object System.Collections.Generic.List[string]
|
|
$markdown.Add("# Phase 5 Dependency Governance") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("- Status: $overallStatus") | Out-Null
|
|
$markdown.Add("- Generated at: $($summary.generated_at)") | 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("Local artifacts:") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add('- `dependency-governance-summary.json`') | Out-Null
|
|
$markdown.Add('- `npm-audit.json`') | Out-Null
|
|
$markdown.Add('- `*.log`') | Out-Null
|
|
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
|
|
|
|
Write-Host "PHASE5_DEPENDENCY_GOVERNANCE_$overallStatus $artifactDir"
|
|
if ($overallStatus -eq "FAIL") {
|
|
throw "Phase 5 dependency governance failed. See $summaryMd"
|
|
}
|