489 lines
23 KiB
PowerShell
489 lines
23 KiB
PowerShell
param(
|
|
[string]$ArtifactRoot = "",
|
|
[string]$FixtureRoot = "",
|
|
[string]$Phase10ArtifactRoot = "",
|
|
[int]$MaxPhase10AgeHours = 72,
|
|
[int]$MaxOpsEvidenceAgeHours = 168,
|
|
[switch]$FailOnWarn,
|
|
[switch]$FailOnExternalBlocked
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
$timestamp = Get-Date -Format "yyyy-MM-ddTHH-mm-ss-fff"
|
|
if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) {
|
|
$ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase11-scale-ops-gate"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($FixtureRoot)) {
|
|
$FixtureRoot = Join-Path $repoRoot "project-docs\phase11-scale-ops-fixtures"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($Phase10ArtifactRoot)) {
|
|
$Phase10ArtifactRoot = Join-Path $repoRoot "project-docs\qa-artifacts\phase10-delivery-acceptance"
|
|
}
|
|
foreach ($pathName in @("ArtifactRoot", "FixtureRoot", "Phase10ArtifactRoot")) {
|
|
$value = Get-Variable -Name $pathName -ValueOnly
|
|
if (-not [System.IO.Path]::IsPathRooted($value)) {
|
|
Set-Variable -Name $pathName -Value (Join-Path $repoRoot $value)
|
|
}
|
|
}
|
|
|
|
$artifactDir = Join-Path $ArtifactRoot $timestamp
|
|
New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null
|
|
|
|
$summaryJson = Join-Path $artifactDir "scale-ops-gate-summary.json"
|
|
$summaryMd = Join-Path $artifactDir "scale-ops-gate-summary.md"
|
|
$gitStatusPath = Join-Path $artifactDir "git-status.txt"
|
|
$capacityReportCsv = Join-Path $artifactDir "phase11-capacity-performance-report.csv"
|
|
$tenantReportCsv = Join-Path $artifactDir "phase11-tenant-isolation-report.csv"
|
|
$slaReportCsv = Join-Path $artifactDir "phase11-sla-slo-report.csv"
|
|
$integrationReportCsv = Join-Path $artifactDir "phase11-integration-priority-report.csv"
|
|
$riskReportCsv = Join-Path $artifactDir "phase11-scale-risk-report.csv"
|
|
$evidenceMatrixCsv = Join-Path $artifactDir "phase11-evidence-matrix.csv"
|
|
|
|
$checks = New-Object System.Collections.Generic.List[object]
|
|
$evidenceMatrix = New-Object System.Collections.Generic.List[object]
|
|
|
|
function Add-Check {
|
|
param(
|
|
[string]$Key,
|
|
[string]$Label,
|
|
[ValidateSet("pass", "fail", "warn", "skip", "blocked")]
|
|
[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 18 | Set-Content -Encoding UTF8 -Path $Path
|
|
}
|
|
|
|
function Read-JsonFile {
|
|
param([string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
return $null
|
|
}
|
|
try {
|
|
return Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Get-LatestSummaryArtifact {
|
|
param(
|
|
[string]$Root,
|
|
[string]$SummaryFile
|
|
)
|
|
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
|
|
return $null
|
|
}
|
|
return Get-ChildItem -LiteralPath $Root -Directory |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName $SummaryFile) } |
|
|
Select-Object -First 1
|
|
}
|
|
|
|
function Test-CsvColumns {
|
|
param(
|
|
[object[]]$Rows,
|
|
[string[]]$Columns
|
|
)
|
|
if ($Rows.Count -eq 0) {
|
|
return $false
|
|
}
|
|
$names = @($Rows[0].PSObject.Properties.Name)
|
|
foreach ($column in $Columns) {
|
|
if ($names -notcontains $column) {
|
|
return $false
|
|
}
|
|
}
|
|
return $true
|
|
}
|
|
|
|
function Test-RequiredFlag {
|
|
param([string]$Value)
|
|
return @("true", "yes", "1", "required") -contains $Value.ToLowerInvariant()
|
|
}
|
|
|
|
function Get-FailedRequiredCount {
|
|
param([object]$Summary)
|
|
if ($null -eq $Summary -or $null -eq $Summary.totals) {
|
|
return 0
|
|
}
|
|
if ($null -ne $Summary.totals.failed_required) {
|
|
return [int]$Summary.totals.failed_required
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function Add-EvidenceCheck {
|
|
param(
|
|
[string]$Key,
|
|
[string]$Label,
|
|
[string]$Root,
|
|
[string]$SummaryFile,
|
|
[string[]]$AllowedStatuses,
|
|
[int]$MaxAgeHours
|
|
)
|
|
|
|
$latest = Get-LatestSummaryArtifact -Root $Root -SummaryFile $SummaryFile
|
|
if ($null -eq $latest) {
|
|
Add-Check -Key $Key -Label $Label -Status "fail" -Detail "No evidence artifact found"
|
|
$evidenceMatrix.Add([pscustomobject][ordered]@{
|
|
key = $Key
|
|
label = $Label
|
|
status = "MISSING"
|
|
failed_required = ""
|
|
age_hours = ""
|
|
artifact_dir = ""
|
|
summary_file = ""
|
|
}) | Out-Null
|
|
return $null
|
|
}
|
|
|
|
$summaryPath = Join-Path $latest.FullName $SummaryFile
|
|
$summary = Read-JsonFile -Path $summaryPath
|
|
$status = if ($summary -and $summary.status) { [string]$summary.status } else { "UNKNOWN" }
|
|
$failedRequired = Get-FailedRequiredCount -Summary $summary
|
|
$ageHours = [math]::Round(((Get-Date) - $latest.LastWriteTime).TotalHours, 2)
|
|
|
|
$evidenceMatrix.Add([pscustomobject][ordered]@{
|
|
key = $Key
|
|
label = $Label
|
|
status = $status
|
|
failed_required = $failedRequired
|
|
age_hours = $ageHours
|
|
artifact_dir = $latest.FullName
|
|
summary_file = $summaryPath
|
|
}) | Out-Null
|
|
|
|
if (($AllowedStatuses -contains $status) -and $failedRequired -eq 0) {
|
|
Add-Check -Key $Key -Label $Label -Status "pass" -Detail "Status=$status, failed_required=$failedRequired, age=${ageHours}h" -Meta @{
|
|
artifact_dir = $latest.FullName
|
|
summary = $summaryPath
|
|
}
|
|
} else {
|
|
Add-Check -Key $Key -Label $Label -Status "fail" -Detail "Status=$status, failed_required=$failedRequired" -Meta @{
|
|
artifact_dir = $latest.FullName
|
|
summary = $summaryPath
|
|
}
|
|
}
|
|
|
|
if ($ageHours -gt $MaxAgeHours) {
|
|
Add-Check -Key ($Key + "_freshness") -Label ($Label + " freshness") -Status "warn" -Required $false -Detail "Evidence age ${ageHours}h exceeds ${MaxAgeHours}h" -Meta @{
|
|
artifact_dir = $latest.FullName
|
|
}
|
|
}
|
|
|
|
return $summary
|
|
}
|
|
|
|
if (-not (Test-Path "$HOME\.Codex\skills\gstack\bin")) {
|
|
Add-Check -Key "gstack" -Label "gstack preflight" -Status "fail" -Detail "gstack is missing"
|
|
} else {
|
|
Add-Check -Key "gstack" -Label "gstack preflight" -Status "pass" -Detail "GSTACK_OK"
|
|
}
|
|
|
|
$branch = (git -C $repoRoot rev-parse --abbrev-ref HEAD).Trim()
|
|
$commit = (git -C $repoRoot rev-parse HEAD).Trim()
|
|
$remoteUrl = (git -C $repoRoot remote get-url origin).Trim()
|
|
$gitStatus = git -C $repoRoot status --short
|
|
($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath
|
|
|
|
if ($remoteUrl -match "192\.168\.3\.90") {
|
|
Add-Check -Key "origin_remote" -Label "origin remote" -Status "pass" -Detail "origin points to 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
|
|
} else {
|
|
Add-Check -Key "origin_remote" -Label "origin remote" -Status "fail" -Detail "origin does not point to 192.168.3.90" -Meta @{ branch = $branch; commit = $commit; remote = $remoteUrl }
|
|
}
|
|
|
|
if ($gitStatus) {
|
|
Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; scale ops gate is advisory" -Meta @{ artifact = $gitStatusPath }
|
|
} else {
|
|
Add-Check -Key "working_tree" -Label "working tree" -Status "pass" -Detail "Working tree is clean" -Meta @{ artifact = $gitStatusPath }
|
|
}
|
|
|
|
if (Test-Path -LiteralPath $FixtureRoot -PathType Container) {
|
|
Add-Check -Key "fixture_root" -Label "scale ops fixture root" -Status "pass" -Detail "Fixture root exists" -Meta @{ root = $FixtureRoot }
|
|
} else {
|
|
Add-Check -Key "fixture_root" -Label "scale ops fixture root" -Status "fail" -Detail "Fixture root is missing" -Meta @{ root = $FixtureRoot }
|
|
}
|
|
|
|
$requiredFixtureFiles = @(
|
|
"README.md",
|
|
"capacity-model.csv",
|
|
"performance-baseline.csv",
|
|
"tenant-isolation-checklist.csv",
|
|
"organization-onboarding-plan.csv",
|
|
"sla-slo-indicators.csv",
|
|
"integration-priority-register.csv",
|
|
"scale-risk-register.csv",
|
|
"monthly-ops-report-template.md",
|
|
"incident-postmortem-template.md"
|
|
)
|
|
foreach ($file in $requiredFixtureFiles) {
|
|
$path = Join-Path $FixtureRoot $file
|
|
if (Test-Path -LiteralPath $path -PathType Leaf) {
|
|
Add-Check -Key ("fixture_" + ($file -replace '[^a-zA-Z0-9]', '_')) -Label "fixture $file" -Status "pass" -Detail "Fixture exists" -Meta @{ path = $path }
|
|
} else {
|
|
Add-Check -Key ("fixture_" + ($file -replace '[^a-zA-Z0-9]', '_')) -Label "fixture $file" -Status "fail" -Detail "Fixture missing" -Meta @{ path = $path }
|
|
}
|
|
}
|
|
|
|
$capacityRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "capacity-model.csv"))
|
|
$performanceRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "performance-baseline.csv"))
|
|
$tenantRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "tenant-isolation-checklist.csv"))
|
|
$orgRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "organization-onboarding-plan.csv"))
|
|
$slaRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "sla-slo-indicators.csv"))
|
|
$integrationRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "integration-priority-register.csv"))
|
|
$riskRows = @(Import-Csv -LiteralPath (Join-Path $FixtureRoot "scale-risk-register.csv"))
|
|
|
|
$capacityColumnsOk = Test-CsvColumns -Rows $capacityRows -Columns @("metric", "baseline_value", "scale_target", "warn_threshold", "critical_threshold", "unit", "expansion_action", "status", "required", "evidence")
|
|
$capacityRequiredBad = @($capacityRows | Where-Object { (Test-RequiredFlag $_.required) -and $_.status -ne "READY" })
|
|
$capacityThresholdBad = @($capacityRows | Where-Object { [double]$_.baseline_value -gt [double]$_.warn_threshold -or [double]$_.warn_threshold -gt [double]$_.critical_threshold })
|
|
if ($capacityColumnsOk -and $capacityRows.Count -ge 6 -and $capacityRequiredBad.Count -eq 0 -and $capacityThresholdBad.Count -eq 0) {
|
|
Add-Check -Key "capacity_model" -Label "capacity model" -Status "pass" -Detail "Capacity metrics=$($capacityRows.Count)"
|
|
} else {
|
|
Add-Check -Key "capacity_model" -Label "capacity model" -Status "fail" -Detail "Capacity model incomplete or thresholds invalid"
|
|
}
|
|
|
|
$performanceColumnsOk = Test-CsvColumns -Rows $performanceRows -Columns @("metric", "baseline_value", "slo_target", "warn_threshold", "critical_threshold", "unit", "measurement_source", "status", "required")
|
|
$performanceRequiredBad = @($performanceRows | Where-Object { (Test-RequiredFlag $_.required) -and $_.status -ne "READY" })
|
|
$performanceThresholdBad = @($performanceRows | Where-Object { [double]$_.baseline_value -gt [double]$_.warn_threshold -or [double]$_.warn_threshold -gt [double]$_.critical_threshold })
|
|
if ($performanceColumnsOk -and $performanceRows.Count -ge 6 -and $performanceRequiredBad.Count -eq 0 -and $performanceThresholdBad.Count -eq 0) {
|
|
Add-Check -Key "performance_baseline" -Label "performance baseline" -Status "pass" -Detail "Performance metrics=$($performanceRows.Count)"
|
|
} else {
|
|
Add-Check -Key "performance_baseline" -Label "performance baseline" -Status "fail" -Detail "Performance baseline incomplete or thresholds invalid"
|
|
}
|
|
|
|
$tenantColumnsOk = Test-CsvColumns -Rows $tenantRows -Columns @("check_id", "object_type", "isolation_key", "expected_control", "verification_method", "status", "required", "evidence")
|
|
$requiredObjects = @("user", "contract", "knowledge_document", "template", "audit_event", "file_asset")
|
|
$tenantObjects = @($tenantRows | Where-Object { $_.status -eq "PASS" } | Select-Object -ExpandProperty object_type -Unique)
|
|
$missingTenantObjects = @($requiredObjects | Where-Object { $tenantObjects -notcontains $_ })
|
|
$tenantRequiredBad = @($tenantRows | Where-Object { (Test-RequiredFlag $_.required) -and $_.status -ne "PASS" })
|
|
if ($tenantColumnsOk -and $missingTenantObjects.Count -eq 0 -and $tenantRequiredBad.Count -eq 0) {
|
|
Add-Check -Key "tenant_isolation" -Label "tenant isolation" -Status "pass" -Detail "Tenant isolation checks passed=$($tenantRows.Count)"
|
|
} else {
|
|
Add-Check -Key "tenant_isolation" -Label "tenant isolation" -Status "fail" -Detail "Tenant isolation missing required object checks" -Meta @{ missing_objects = $missingTenantObjects }
|
|
}
|
|
|
|
$orgColumnsOk = Test-CsvColumns -Rows $orgRows -Columns @("org_id", "org_name", "departments", "roles", "estimated_users", "onboarding_status", "owner", "evidence")
|
|
$readyOrgs = @($orgRows | Where-Object { $_.onboarding_status -eq "READY" })
|
|
$orgUsers = 0
|
|
foreach ($org in $readyOrgs) {
|
|
$orgUsers += [int]$org.estimated_users
|
|
}
|
|
if ($orgColumnsOk -and $readyOrgs.Count -ge 2 -and $orgUsers -ge 100) {
|
|
Add-Check -Key "organization_onboarding" -Label "organization onboarding" -Status "pass" -Detail "Ready orgs=$($readyOrgs.Count), planned users=$orgUsers"
|
|
} else {
|
|
Add-Check -Key "organization_onboarding" -Label "organization onboarding" -Status "fail" -Detail "Organization onboarding plan is incomplete"
|
|
}
|
|
|
|
$slaColumnsOk = Test-CsvColumns -Rows $slaRows -Columns @("indicator_id", "indicator", "slo_target", "measurement_source", "window", "owner", "status", "required")
|
|
$slaRequiredBad = @($slaRows | Where-Object { (Test-RequiredFlag $_.required) -and $_.status -ne "READY" })
|
|
if ($slaColumnsOk -and $slaRows.Count -ge 6 -and $slaRequiredBad.Count -eq 0) {
|
|
Add-Check -Key "sla_slo" -Label "SLA/SLO indicators" -Status "pass" -Detail "SLA/SLO indicators=$($slaRows.Count)"
|
|
} else {
|
|
Add-Check -Key "sla_slo" -Label "SLA/SLO indicators" -Status "fail" -Detail "SLA/SLO indicators incomplete"
|
|
}
|
|
|
|
$integrationColumnsOk = Test-CsvColumns -Rows $integrationRows -Columns @("integration_id", "integration", "priority", "value", "risk", "dependency", "status", "phase12_candidate")
|
|
$p1Integrations = @($integrationRows | Where-Object { $_.priority -eq "P1" })
|
|
$integrationMissingPriority = @($integrationRows | Where-Object { [string]::IsNullOrWhiteSpace($_.priority) -or [string]::IsNullOrWhiteSpace($_.dependency) })
|
|
if ($integrationColumnsOk -and $integrationRows.Count -ge 5 -and $p1Integrations.Count -ge 2 -and $integrationMissingPriority.Count -eq 0) {
|
|
Add-Check -Key "integration_priority" -Label "integration priority" -Status "pass" -Detail "Integrations=$($integrationRows.Count), P1=$($p1Integrations.Count)"
|
|
} else {
|
|
Add-Check -Key "integration_priority" -Label "integration priority" -Status "fail" -Detail "Integration priority register incomplete"
|
|
}
|
|
|
|
$riskColumnsOk = Test-CsvColumns -Rows $riskRows -Columns @("risk_id", "domain", "severity", "status", "mitigation", "owner", "evidence")
|
|
$openCriticalRisks = @($riskRows | Where-Object { ($_.severity -eq "P0" -or $_.severity -eq "P1") -and @("MITIGATED", "ACCEPTED") -notcontains $_.status })
|
|
$risksWithoutMitigation = @($riskRows | Where-Object { [string]::IsNullOrWhiteSpace($_.mitigation) })
|
|
$deferredExternalRisks = @($riskRows | Where-Object { $_.status -like "DEFERRED*" })
|
|
if ($riskColumnsOk -and $riskRows.Count -ge 5 -and $openCriticalRisks.Count -eq 0 -and $risksWithoutMitigation.Count -eq 0) {
|
|
Add-Check -Key "scale_risk" -Label "scale risk register" -Status "pass" -Detail "Risks=$($riskRows.Count), open P0/P1=0"
|
|
} else {
|
|
Add-Check -Key "scale_risk" -Label "scale risk register" -Status "fail" -Detail "Scale risk register has unmanaged critical risks"
|
|
}
|
|
|
|
$phase10Summary = Add-EvidenceCheck -Key "phase10_delivery_acceptance" -Label "phase10 delivery acceptance" -Root $Phase10ArtifactRoot -SummaryFile "delivery-acceptance-summary.json" -AllowedStatuses @("PASS", "PASS_WITH_WEBHOOK_BLOCKED") -MaxAgeHours $MaxPhase10AgeHours
|
|
$evidenceSpecs = @(
|
|
@{ key = "capacity_governance"; label = "capacity governance"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7-capacity-governance"); summary = "capacity-governance-summary.json"; statuses = @("PASS", "WARN"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "production_watchdog"; label = "production watchdog"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7-production-watchdog"); summary = "production-watchdog-summary.json"; statuses = @("PASS", "WARN"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "watchdog_task_audit"; label = "watchdog task audit"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7.8-watchdog-task-audit"); summary = "watchdog-task-audit-summary.json"; statuses = @("PASS"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "watchdog_evidence_report"; label = "watchdog evidence report"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase7.9-watchdog-evidence-report"); summary = "watchdog-evidence-report-summary.json"; statuses = @("PASS"); max = $MaxOpsEvidenceAgeHours },
|
|
@{ key = "business_quality_gate"; label = "business quality gate"; root = (Join-Path $repoRoot "project-docs\qa-artifacts\phase9-business-quality-gate"); summary = "business-quality-gate-summary.json"; statuses = @("PASS", "PASS_WITH_WEBHOOK_BLOCKED"); max = $MaxOpsEvidenceAgeHours }
|
|
)
|
|
foreach ($spec in $evidenceSpecs) {
|
|
Add-EvidenceCheck -Key $spec.key -Label $spec.label -Root $spec.root -SummaryFile $spec.summary -AllowedStatuses $spec.statuses -MaxAgeHours $spec.max | Out-Null
|
|
}
|
|
|
|
$capacityCombined = @()
|
|
foreach ($row in $capacityRows) {
|
|
$capacityCombined += [pscustomobject][ordered]@{
|
|
category = "capacity"
|
|
metric = $row.metric
|
|
baseline_value = $row.baseline_value
|
|
target = $row.scale_target
|
|
warn_threshold = $row.warn_threshold
|
|
critical_threshold = $row.critical_threshold
|
|
unit = $row.unit
|
|
status = $row.status
|
|
evidence = $row.evidence
|
|
}
|
|
}
|
|
foreach ($row in $performanceRows) {
|
|
$capacityCombined += [pscustomobject][ordered]@{
|
|
category = "performance"
|
|
metric = $row.metric
|
|
baseline_value = $row.baseline_value
|
|
target = $row.slo_target
|
|
warn_threshold = $row.warn_threshold
|
|
critical_threshold = $row.critical_threshold
|
|
unit = $row.unit
|
|
status = $row.status
|
|
evidence = $row.measurement_source
|
|
}
|
|
}
|
|
$capacityCombined | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $capacityReportCsv
|
|
$tenantRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $tenantReportCsv
|
|
$slaRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $slaReportCsv
|
|
$integrationRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $integrationReportCsv
|
|
$riskRows | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $riskReportCsv
|
|
$evidenceMatrix | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $evidenceMatrixCsv
|
|
|
|
$secret = [string]::Concat([char]54, [char]53, [char]52, [char]51, [char]50, [char]49)
|
|
$secretHits = @()
|
|
$scanFiles = @(Get-ChildItem -LiteralPath $FixtureRoot -File -Recurse)
|
|
$scanFiles += @(Get-Item -LiteralPath (Join-Path $repoRoot "scripts\run-scale-ops-gate.ps1"))
|
|
foreach ($file in $scanFiles) {
|
|
$text = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
|
|
if ($text -and $text.Contains($secret)) {
|
|
$secretHits += $file.FullName
|
|
}
|
|
}
|
|
if ($secretHits.Count -eq 0) {
|
|
Add-Check -Key "secret_scan" -Label "secret scan" -Status "pass" -Detail "No administrator password literal found in scale ops package or gate script"
|
|
} else {
|
|
Add-Check -Key "secret_scan" -Label "secret scan" -Status "fail" -Detail "Secret literal detected" -Meta @{ hits = $secretHits }
|
|
}
|
|
|
|
$externalBlocked = $false
|
|
if (($phase10Summary -and $phase10Summary.status -eq "PASS_WITH_WEBHOOK_BLOCKED") -or $deferredExternalRisks.Count -gt 0 -or @($integrationRows | Where-Object { $_.status -like "BLOCKED*" }).Count -gt 0) {
|
|
$externalBlocked = $true
|
|
Add-Check -Key "external_webhook" -Label "external alert webhook" -Status "blocked" -Required $false -Detail "Real external alert Webhook remains unavailable; inherited as external blocked item"
|
|
}
|
|
|
|
$failedRequired = @($checks | Where-Object { $_.required -and $_.status -eq "fail" })
|
|
$warningsList = @($checks | Where-Object { $_.status -eq "warn" })
|
|
$blockedList = @($checks | Where-Object { $_.status -eq "blocked" })
|
|
$overallStatus = if ($failedRequired.Count -gt 0) {
|
|
"FAIL"
|
|
} elseif ($blockedList.Count -gt 0 -or $externalBlocked) {
|
|
"PASS_WITH_WEBHOOK_BLOCKED"
|
|
} elseif ($warningsList.Count -gt 0) {
|
|
"WARN"
|
|
} else {
|
|
"PASS"
|
|
}
|
|
$alertSeverity = if ($overallStatus -eq "FAIL") { "critical" } elseif ($overallStatus -eq "WARN") { "warning" } elseif ($overallStatus -eq "PASS_WITH_WEBHOOK_BLOCKED") { "blocked" } else { "ok" }
|
|
|
|
$summary = [ordered]@{
|
|
name = "phase11-scale-ops-gate"
|
|
status = $overallStatus
|
|
alert_severity = $alertSeverity
|
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
artifact_dir = $artifactDir
|
|
branch = $branch
|
|
commit = $commit
|
|
remote = $remoteUrl
|
|
fixture_root = $FixtureRoot
|
|
checks = $checks
|
|
metrics = [ordered]@{
|
|
capacity_metrics = $capacityRows.Count
|
|
performance_metrics = $performanceRows.Count
|
|
tenant_isolation_checks = $tenantRows.Count
|
|
ready_organizations = $readyOrgs.Count
|
|
planned_users = $orgUsers
|
|
sla_slo_indicators = $slaRows.Count
|
|
integrations = $integrationRows.Count
|
|
risks = $riskRows.Count
|
|
}
|
|
artifacts = [ordered]@{
|
|
capacity_performance_report = $capacityReportCsv
|
|
tenant_isolation_report = $tenantReportCsv
|
|
sla_slo_report = $slaReportCsv
|
|
integration_priority_report = $integrationReportCsv
|
|
scale_risk_report = $riskReportCsv
|
|
evidence_matrix = $evidenceMatrixCsv
|
|
}
|
|
totals = [ordered]@{
|
|
total = $checks.Count
|
|
failed_required = $failedRequired.Count
|
|
warnings = $warningsList.Count
|
|
blocked = $blockedList.Count
|
|
}
|
|
}
|
|
Save-Json -Path $summaryJson -Value $summary
|
|
|
|
$markdown = New-Object System.Collections.Generic.List[string]
|
|
$markdown.Add("# Phase 11 Scale Operations Gate") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("- Status: $overallStatus") | Out-Null
|
|
$markdown.Add("- Alert severity: $alertSeverity") | 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("- Fixture root: $FixtureRoot") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("## Metrics") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("| Metric | Count |") | Out-Null
|
|
$markdown.Add("|---|---:|") | Out-Null
|
|
$markdown.Add("| Capacity metrics | $($capacityRows.Count) |") | Out-Null
|
|
$markdown.Add("| Performance metrics | $($performanceRows.Count) |") | Out-Null
|
|
$markdown.Add("| Tenant isolation checks | $($tenantRows.Count) |") | Out-Null
|
|
$markdown.Add("| Ready organizations | $($readyOrgs.Count) |") | Out-Null
|
|
$markdown.Add("| Planned users | $orgUsers |") | Out-Null
|
|
$markdown.Add("| SLA/SLO indicators | $($slaRows.Count) |") | Out-Null
|
|
$markdown.Add("| Integrations | $($integrationRows.Count) |") | Out-Null
|
|
$markdown.Add("| Risks | $($riskRows.Count) |") | Out-Null
|
|
$markdown.Add("") | Out-Null
|
|
$markdown.Add("## Checks") | 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('- `scale-ops-gate-summary.json`') | Out-Null
|
|
$markdown.Add('- `scale-ops-gate-summary.md`') | Out-Null
|
|
$markdown.Add('- `phase11-capacity-performance-report.csv`') | Out-Null
|
|
$markdown.Add('- `phase11-tenant-isolation-report.csv`') | Out-Null
|
|
$markdown.Add('- `phase11-sla-slo-report.csv`') | Out-Null
|
|
$markdown.Add('- `phase11-integration-priority-report.csv`') | Out-Null
|
|
$markdown.Add('- `phase11-scale-risk-report.csv`') | Out-Null
|
|
$markdown.Add('- `phase11-evidence-matrix.csv`') | Out-Null
|
|
$markdown | Set-Content -Encoding UTF8 -Path $summaryMd
|
|
|
|
Write-Host "PHASE11_SCALE_OPS_GATE_$overallStatus $artifactDir"
|
|
if ($overallStatus -eq "FAIL" -or ($FailOnWarn -and $warningsList.Count -gt 0) -or ($FailOnExternalBlocked -and $blockedList.Count -gt 0)) {
|
|
throw "Phase 11 scale ops gate $overallStatus. See $summaryMd"
|
|
}
|