chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$EvidenceIndex = 'testdata/release-evidence-index.example.v2.json',
|
||||
[string]$OutputPath = '',
|
||||
[switch]$Strict
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$projectRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $projectRoot
|
||||
|
||||
$requiredCodes = @(
|
||||
'REMOTE_CI_SUPPLY_CHAIN',
|
||||
'REAL_DEVICE_RTC',
|
||||
'REAL_DEVICE_FIRMWARE',
|
||||
'REAL_AGENT_MODELS',
|
||||
'PRODUCTION_INFRASTRUCTURE',
|
||||
'PRODUCTION_RECOVERY',
|
||||
'KMS_ROTATION',
|
||||
'REAL_ALERT_RECEIVER',
|
||||
'SAFARI_COMPATIBILITY',
|
||||
'BUSINESS_APPROVAL'
|
||||
)
|
||||
|
||||
function Invoke-GitText {
|
||||
param([Parameter(Mandatory)][string[]]$Arguments)
|
||||
|
||||
$output = @(& git @Arguments 2>$null)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
return @()
|
||||
}
|
||||
return @($output | ForEach-Object { $_.ToString().Trim() } | Where-Object { $_ })
|
||||
}
|
||||
|
||||
function Test-PlaceholderDigest {
|
||||
param([string]$Value)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) { return $true }
|
||||
return $Value -eq ('sha256:' + ('0' * 64))
|
||||
}
|
||||
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
||||
throw 'Git is required for the stage 8 preflight.'
|
||||
}
|
||||
|
||||
$resolvedIndex = (Resolve-Path -LiteralPath $EvidenceIndex).Path
|
||||
$index = Get-Content -Raw -LiteralPath $resolvedIndex | ConvertFrom-Json
|
||||
if ($index.schema_version -ne 'release-evidence-index-v2') {
|
||||
throw "Unsupported evidence index schema: $($index.schema_version)"
|
||||
}
|
||||
|
||||
$commitLines = @(Invoke-GitText -Arguments @('rev-parse', '--verify', 'HEAD'))
|
||||
$commit = if ($commitLines.Count -gt 0) { $commitLines[0] } else { $null }
|
||||
$branchLines = @(Invoke-GitText -Arguments @('branch', '--show-current'))
|
||||
$branch = if ($branchLines.Count -gt 0) { $branchLines[0] } else { $null }
|
||||
$remoteNames = @(Invoke-GitText -Arguments @('remote'))
|
||||
$remotes = @()
|
||||
foreach ($remoteName in $remoteNames) {
|
||||
$remoteURLs = @(Invoke-GitText -Arguments @('remote', 'get-url', $remoteName))
|
||||
$remotes += [ordered]@{
|
||||
name = $remoteName
|
||||
url_configured = $remoteURLs.Count -gt 0
|
||||
}
|
||||
}
|
||||
|
||||
$items = @($index.items)
|
||||
$duplicateCodes = @($items | Group-Object code | Where-Object Count -gt 1 | ForEach-Object Name)
|
||||
$actualCodes = @($items | ForEach-Object { $_.code })
|
||||
$missingCodes = @($requiredCodes | Where-Object { $_ -notin $actualCodes })
|
||||
$unexpectedCodes = @($actualCodes | Where-Object { $_ -notin $requiredCodes })
|
||||
$validStatuses = @('PENDING', 'VERIFIED', 'WAIVED')
|
||||
$invalidStatusCodes = @($items | Where-Object { $_.status -notin $validStatuses } | ForEach-Object code)
|
||||
|
||||
$gates = @($requiredCodes | ForEach-Object {
|
||||
$code = $_
|
||||
$item = @($items | Where-Object code -eq $code | Select-Object -First 1)
|
||||
if ($item.Count -eq 0) {
|
||||
[ordered]@{ code = $code; status = 'MISSING'; owner = ''; file_count = 0 }
|
||||
} else {
|
||||
[ordered]@{
|
||||
code = $code
|
||||
status = $item[0].status
|
||||
owner = $item[0].owner
|
||||
file_count = @($item[0].files).Count
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$pendingCount = @($gates | Where-Object status -eq 'PENDING').Count
|
||||
$verifiedCount = @($gates | Where-Object status -eq 'VERIFIED').Count
|
||||
$waivedCount = @($gates | Where-Object status -eq 'WAIVED').Count
|
||||
|
||||
$requiredLocalServices = @('api', 'callbacks', 'worker', 'web', 'postgres', 'redis', 'minio', 'keycloak')
|
||||
$runningServices = @()
|
||||
$dockerAvailable = [bool](Get-Command docker -ErrorAction SilentlyContinue)
|
||||
if ($dockerAvailable) {
|
||||
$runningServices = @(& docker compose ps --services --status running 2>$null)
|
||||
if ($LASTEXITCODE -ne 0) { $runningServices = @() }
|
||||
}
|
||||
$missingLocalServices = @($requiredLocalServices | Where-Object { $_ -notin $runningServices })
|
||||
|
||||
$externalConfigPrefixes = @(
|
||||
'GITHUB_', 'CI_', 'KUBECONFIG', 'ARK_', 'VOLC_', 'CONTENT_SAFETY_',
|
||||
'AWS_', 'AZURE_', 'GOOGLE_'
|
||||
)
|
||||
$externalConfigNames = @(Get-ChildItem Env: | Where-Object {
|
||||
$name = $_.Name
|
||||
@($externalConfigPrefixes | Where-Object { $name.StartsWith($_, [System.StringComparison]::OrdinalIgnoreCase) }).Count -gt 0
|
||||
} | Sort-Object Name | ForEach-Object Name)
|
||||
|
||||
$indexValid = $duplicateCodes.Count -eq 0 -and $missingCodes.Count -eq 0 -and $unexpectedCodes.Count -eq 0 -and $invalidStatusCodes.Count -eq 0
|
||||
$sourceControlReady = -not [string]::IsNullOrWhiteSpace($commit) -and $remotes.Count -gt 0
|
||||
$entryReady = $sourceControlReady -and $indexValid -and -not (Test-PlaceholderDigest -Value $index.build_digest)
|
||||
|
||||
$blockers = @()
|
||||
if ([string]::IsNullOrWhiteSpace($commit)) { $blockers += 'Git repository has no candidate commit.' }
|
||||
if ($remotes.Count -eq 0) { $blockers += 'Git repository has no configured remote.' }
|
||||
if (Test-PlaceholderDigest -Value $index.build_digest) { $blockers += 'Evidence index still uses a placeholder build digest.' }
|
||||
if (-not $indexValid) { $blockers += 'Evidence index structure or statuses are invalid.' }
|
||||
if ($pendingCount -gt 0) { $blockers += "$pendingCount external evidence gate(s) remain PENDING." }
|
||||
|
||||
$report = [ordered]@{
|
||||
schema_version = 'stage8-external-preflight-v1'
|
||||
generated_at = (Get-Date).ToUniversalTime().ToString('o')
|
||||
repository = [ordered]@{
|
||||
branch = $branch
|
||||
commit = $commit
|
||||
remotes = $remotes
|
||||
source_control_ready = $sourceControlReady
|
||||
}
|
||||
local_runtime = [ordered]@{
|
||||
docker_available = $dockerAvailable
|
||||
required_services = $requiredLocalServices
|
||||
running_service_count = $runningServices.Count
|
||||
missing_required_services = $missingLocalServices
|
||||
ready = $missingLocalServices.Count -eq 0
|
||||
}
|
||||
external_configuration = [ordered]@{
|
||||
detected_variable_names = $externalConfigNames
|
||||
secret_values_read = $false
|
||||
note = 'Variable names are hints only and never count as acceptance evidence.'
|
||||
}
|
||||
evidence = [ordered]@{
|
||||
index_path = $resolvedIndex
|
||||
candidate_version = $index.candidate_version
|
||||
build_digest = $index.build_digest
|
||||
structure_valid = $indexValid
|
||||
duplicate_codes = $duplicateCodes
|
||||
missing_codes = $missingCodes
|
||||
unexpected_codes = $unexpectedCodes
|
||||
invalid_status_codes = $invalidStatusCodes
|
||||
pending = $pendingCount
|
||||
verified = $verifiedCount
|
||||
waived = $waivedCount
|
||||
gates = $gates
|
||||
}
|
||||
readiness = [ordered]@{
|
||||
external_execution_entry_ready = $entryReady
|
||||
authoritative_release_validation_required = $true
|
||||
note = 'This preflight does not replace conformance reports or the release-evidence command.'
|
||||
}
|
||||
blockers = $blockers
|
||||
}
|
||||
|
||||
$json = $report | ConvertTo-Json -Depth 12
|
||||
if (-not [string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
$parent = Split-Path -Parent $OutputPath
|
||||
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
|
||||
New-Item -ItemType Directory -Path $parent | Out-Null
|
||||
}
|
||||
[System.IO.File]::WriteAllText($OutputPath, $json, [System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
Write-Host "Stage 8 preflight: source_control_ready=$sourceControlReady, local_runtime_ready=$($missingLocalServices.Count -eq 0), pending_evidence=$pendingCount, entry_ready=$entryReady"
|
||||
foreach ($blocker in $blockers) { Write-Host "[BLOCKED] $blocker" }
|
||||
if ($OutputPath) { Write-Host "Report written to $OutputPath" }
|
||||
|
||||
if ($Strict -and -not $entryReady) { exit 1 }
|
||||
Reference in New Issue
Block a user