Files
zone/scripts/gated-analysis.ps1
Zaine 569c9853bf
All checks were successful
Zone / build (push) Successful in 13s
updates
2026-06-04 14:18:21 +01:00

272 lines
7.8 KiB
PowerShell
Executable File

#!/usr/bin/env pwsh
[CmdletBinding()]
param(
[string]$DiscordWebhookUrl = $env:DISCORD_WEBHOOK_URL,
[string]$Repository = $env:GITHUB_REPOSITORY,
[string]$RefName = $env:GITHUB_REF_NAME,
[string]$Sha = $env:GITHUB_SHA,
[string]$RunUrl = $env:GITHUB_SERVER_URL
)
$ErrorActionPreference = 'Stop'
$exitCode = 0
$smellFailures = New-Object System.Collections.Generic.List[string]
$smellWarnings = New-Object System.Collections.Generic.List[string]
function Add-SmellFailure {
param([string]$Message)
$script:smellFailures.Add($Message)
}
function Add-SmellWarning {
param([string]$Message)
$script:smellWarnings.Add($Message)
}
function Get-ProjectFiles {
$excludeDirs = @('.git', 'node_modules', 'dist', 'build', 'coverage')
Get-ChildItem -Path . -File -Recurse -Force |
Where-Object {
$pathParts = $_.FullName.Substring((Get-Location).Path.Length).Split([IO.Path]::DirectorySeparatorChar, [StringSplitOptions]::RemoveEmptyEntries)
-not ($pathParts | Where-Object { $excludeDirs -contains $_ })
}
}
function Test-ManifestSchema {
Write-Host 'Validating data/manifest.json...'
$manifestPath = Join-Path (Get-Location) 'data/manifest.json'
if (-not (Test-Path -LiteralPath $manifestPath)) {
Add-SmellFailure 'data/manifest.json is missing.'
return $false
}
try {
$manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json
} catch {
Add-SmellFailure "data/manifest.json is not valid JSON: $($_.Exception.Message)"
return $false
}
$requiredRoots = @('dashboard', 'links', 'builds', 'status')
foreach ($key in $requiredRoots) {
if (-not ($manifest.PSObject.Properties.Name -contains $key)) {
Add-SmellFailure "manifest missing required key '$key'."
return $false
}
}
$linkIds = New-Object 'System.Collections.Generic.HashSet[string]'
foreach ($link in @($manifest.links)) {
if ([string]::IsNullOrWhiteSpace($link.id)) {
Add-SmellFailure 'manifest link missing id.'
return $false
}
if (-not $linkIds.Add([string]$link.id)) {
Add-SmellFailure "duplicate manifest link id '$($link.id)'."
return $false
}
}
$buildIds = New-Object 'System.Collections.Generic.HashSet[string]'
foreach ($build in @($manifest.builds)) {
if ([string]::IsNullOrWhiteSpace($build.id)) {
Add-SmellFailure 'manifest build missing id.'
return $false
}
if (-not $buildIds.Add([string]$build.id)) {
Add-SmellFailure "duplicate manifest build id '$($build.id)'."
return $false
}
foreach ($pathKey in @('start', 'status', 'logs')) {
$section = $build.$pathKey
if ($null -eq $section -or [string]::IsNullOrWhiteSpace($section.path)) {
Add-SmellFailure "manifest build '$($build.id)' missing $pathKey.path."
return $false
}
}
}
Write-Host 'Manifest schema validation passed.'
return $true
}
function Test-CodeSmells {
Write-Host 'Running gated smell checks...'
$files = @(Get-ProjectFiles)
$debtMarkerPattern = '\b(T' + 'ODO|F' + 'IXME|H' + 'ACK|X' + 'XX|W' + 'IP)\b'
foreach ($file in $files) {
$relativePath = Resolve-Path -Relative $file.FullName
if ($file.Length -gt 1MB) {
Add-SmellWarning "$relativePath is larger than 1 MB."
}
$content = Get-Content -Raw -LiteralPath $file.FullName
if ($content -match '(?m)^(<<<<<<<|=======|>>>>>>>)') {
Add-SmellFailure "$relativePath contains merge conflict markers."
}
if ($content -match '(?i)(discord(?:_webhook)?_url|password|api[_-]?key|secret)\s*[:=]\s*["''][^"'']{8,}["'']') {
Add-SmellFailure "$relativePath may contain a committed secret."
}
$todoMatches = [regex]::Matches($content, "(?i)$debtMarkerPattern")
if ($todoMatches.Count -gt 0) {
Add-SmellWarning "$relativePath contains $($todoMatches.Count) debt marker(s)."
}
}
if ($smellFailures.Count -gt 0) {
Write-Host 'Smell checks failed:'
$smellFailures | ForEach-Object { Write-Host " - $_" }
return $false
}
if ($smellWarnings.Count -gt 0) {
Write-Host 'Smell check warnings:'
$smellWarnings | ForEach-Object { Write-Host " - $_" }
} else {
Write-Host 'No smell check warnings found.'
}
return $true
}
function Invoke-TestSuite {
Write-Host 'Running test suite...'
$tempRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [IO.Path]::GetTempPath() }
$outputFile = Join-Path $tempRoot 'zone-test-output.txt'
& npm test 2>&1 | Tee-Object -FilePath $outputFile
$testExitCode = $LASTEXITCODE
$output = Get-Content -Raw -LiteralPath $outputFile
return [pscustomobject]@{
Passed = ($testExitCode -eq 0)
ExitCode = $testExitCode
Output = $output.Trim()
}
}
function Get-ShortText {
param(
[string]$Text,
[int]$MaxLength = 1400
)
if ([string]::IsNullOrWhiteSpace($Text)) {
return 'No output captured.'
}
if ($Text.Length -le $MaxLength) {
return $Text
}
return $Text.Substring($Text.Length - $MaxLength)
}
function Send-DiscordReport {
param(
[bool]$SmellsPassed,
[object]$Tests
)
if ([string]::IsNullOrWhiteSpace($DiscordWebhookUrl)) {
Write-Host 'DISCORD_WEBHOOK_URL is not set; skipping Discord report.'
return
}
$status = if ($SmellsPassed -and $Tests.Passed -and $manifestPassed) { 'passed' } else { 'failed' }
$color = if ($status -eq 'passed') { 5763719 } else { 15548997 }
$shaShort = if ($Sha -and $Sha.Length -ge 7) { $Sha.Substring(0, 7) } else { $Sha }
$repoText = if ($Repository) { $Repository } else { 'zone' }
$refText = if ($RefName) { $RefName } else { 'unknown ref' }
$smellStatus = if ($SmellsPassed) { 'passed' } else { 'failed' }
$testStatus = if ($Tests.Passed) { 'passed' } else { "failed (exit $($Tests.ExitCode))" }
$warningText = if ($smellWarnings.Count -gt 0) { ($smellWarnings -join "`n") } else { 'None' }
$failureText = if ($smellFailures.Count -gt 0) { ($smellFailures -join "`n") } else { 'None' }
$description = @"
Repository: $repoText
Ref: $refText
Commit: $shaShort
Smell checks: $smellStatus
Tests: $testStatus
"@
$fields = @(
@{
name = 'Smell failures'
value = Get-ShortText -Text $failureText -MaxLength 900
inline = $false
},
@{
name = 'Smell warnings'
value = Get-ShortText -Text $warningText -MaxLength 900
inline = $false
},
@{
name = 'Test output'
value = "``````text`n$(Get-ShortText -Text $Tests.Output -MaxLength 900)`n``````"
inline = $false
}
)
$payload = @{
username = 'Zone CI'
embeds = @(
@{
title = "Gated analysis $status"
description = $description.Trim()
color = $color
fields = $fields
timestamp = (Get-Date).ToUniversalTime().ToString('o')
}
)
}
if ($RunUrl -and $env:GITHUB_RUN_ID) {
$payload.embeds[0].url = "$RunUrl/$repoText/actions/runs/$($env:GITHUB_RUN_ID)"
}
Invoke-RestMethod -Method Post -Uri $DiscordWebhookUrl -ContentType 'application/json' -Body ($payload | ConvertTo-Json -Depth 8) | Out-Null
Write-Host 'Discord report published.'
}
function Test-ManifestV2Validator {
Write-Host 'Running scripts/validate-manifest.mjs...'
$validator = Join-Path (Get-Location) 'scripts/validate-manifest.mjs'
if (-not (Test-Path -LiteralPath $validator)) {
Add-SmellFailure 'scripts/validate-manifest.mjs is missing.'
return $false
}
$output = & node $validator 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Add-SmellFailure "validate-manifest.mjs failed: $output"
return $false
}
Write-Host $output.Trim()
return $true
}
$manifestPassed = (Test-ManifestSchema) -and (Test-ManifestV2Validator)
if (-not $manifestPassed) {
$exitCode = 1
}
$smellsPassed = Test-CodeSmells
if (-not $smellsPassed) {
$exitCode = 1
}
$tests = Invoke-TestSuite
if (-not $tests.Passed) {
$exitCode = $tests.ExitCode
}
Send-DiscordReport -SmellsPassed ($smellsPassed -and $manifestPassed) -Tests $tests
exit $exitCode