This commit is contained in:
83
scripts/gated-analysis.ps1
Normal file → Executable file
83
scripts/gated-analysis.ps1
Normal file → Executable file
@@ -32,6 +32,64 @@ function Get-ProjectFiles {
|
||||
}
|
||||
}
|
||||
|
||||
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...'
|
||||
|
||||
@@ -120,7 +178,7 @@ function Send-DiscordReport {
|
||||
return
|
||||
}
|
||||
|
||||
$status = if ($SmellsPassed -and $Tests.Passed) { 'passed' } else { 'failed' }
|
||||
$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' }
|
||||
@@ -178,6 +236,27 @@ Tests: $testStatus
|
||||
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
|
||||
@@ -188,5 +267,5 @@ if (-not $tests.Passed) {
|
||||
$exitCode = $tests.ExitCode
|
||||
}
|
||||
|
||||
Send-DiscordReport -SmellsPassed $smellsPassed -Tests $tests
|
||||
Send-DiscordReport -SmellsPassed ($smellsPassed -and $manifestPassed) -Tests $tests
|
||||
exit $exitCode
|
||||
|
||||
92
scripts/validate-manifest.mjs
Executable file
92
scripts/validate-manifest.mjs
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validates zone/data/manifest.json for Phase 3 manifest v2.
|
||||
* Exit 0 on success, 1 on validation errors (prints to stderr).
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const manifestPath = resolve(root, 'data/manifest.json');
|
||||
|
||||
const errors = [];
|
||||
|
||||
function fail(msg) {
|
||||
errors.push(msg);
|
||||
}
|
||||
|
||||
function requireArray(name, value) {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
fail(`${name} must be a non-empty array`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function requireObject(name, value) {
|
||||
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
fail(`${name} must be an object`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let manifest;
|
||||
try {
|
||||
const raw = readFileSync(manifestPath, 'utf8');
|
||||
manifest = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
console.error(`Cannot read manifest: ${manifestPath}: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (manifest.version !== 2) {
|
||||
fail(`version must be 2 (got ${manifest.version})`);
|
||||
}
|
||||
|
||||
requireObject('dashboard', manifest.dashboard);
|
||||
requireArray('links', manifest.links);
|
||||
requireArray('builds', manifest.builds);
|
||||
requireObject('status', manifest.status);
|
||||
requireObject('integrations', manifest.integrations);
|
||||
requireArray('widgets', manifest.widgets);
|
||||
|
||||
const widgetTypes = new Set([
|
||||
'queue',
|
||||
'badge',
|
||||
'timesheet',
|
||||
'log',
|
||||
'buildStatus',
|
||||
'authoringDetail',
|
||||
]);
|
||||
const widgetIds = new Set();
|
||||
|
||||
for (const w of manifest.widgets || []) {
|
||||
if (!w.id) fail('widget missing id');
|
||||
else if (widgetIds.has(w.id)) fail(`duplicate widget id: ${w.id}`);
|
||||
else widgetIds.add(w.id);
|
||||
if (!w.type || !widgetTypes.has(w.type)) {
|
||||
fail(`widget ${w.id || '?'} has invalid type: ${w.type}`);
|
||||
}
|
||||
if (!w.title) fail(`widget ${w.id} missing title`);
|
||||
if (w.type === 'log' && !w.logKey) {
|
||||
fail(`widget ${w.id} type log requires logKey`);
|
||||
}
|
||||
if (w.type === 'buildStatus' && !w.lastRunKey) {
|
||||
fail(`widget ${w.id} type buildStatus requires lastRunKey`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const b of manifest.builds || []) {
|
||||
if (!b.id) fail('build missing id');
|
||||
if (!b.lastRunKey) fail(`build ${b.id} missing lastRunKey`);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
console.error('Manifest validation failed:');
|
||||
for (const e of errors) console.error(` - ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`OK: ${manifestPath} (v2, ${manifest.widgets.length} widgets)`);
|
||||
Reference in New Issue
Block a user