Files
story/scripts/smoke-release-evidence.ps1
T

101 lines
10 KiB
PowerShell
Raw Normal View History

2026-08-26 06:41:07 +08:00
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$root = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $root
$api = 'http://localhost:58080'
$tokenUrl = 'http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
$tenant = '10000000-0000-0000-0000-000000000010'
$candidate = '0.54.0-rc.local'
$key = 'release-evidence-smoke-' + [Guid]::NewGuid().ToString('N')
$idempotencyKeys = @($key)
$requestID = 'release-evidence-smoke-' + [Guid]::NewGuid().ToString('N')
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('voice-release-evidence-' + [Guid]::NewGuid().ToString('N'))
$snapshotPath = Join-Path $temporaryRoot 'acceptance.json'
$indexPath = Join-Path $temporaryRoot 'evidence-index.json'
$bundlePath = Join-Path $temporaryRoot 'candidate-evidence.zip'
$expandedPath = Join-Path $temporaryRoot 'expanded'
$weekdayOffset = (([int](Get-Date).DayOfWeek) + 6) % 7
$completedWeekStart = (Get-Date).Date.AddDays(-$weekdayOffset - 7)
function Write-JsonUtf8([string]$path, $value, [int]$depth = 20) {
$json = $value | ConvertTo-Json -Depth $depth
[System.IO.File]::WriteAllText($path, $json, [System.Text.UTF8Encoding]::new($false))
}
try {
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
$token = (Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{
grant_type = 'password'; client_id = 'voice-platform-web'; username = 'owner'; password = 'LocalOnly123!'; scope = 'openid'
}).access_token
$buildDigest = (docker image inspect data-terminal-cloud-api --format '{{.Id}}').Trim()
if ($buildDigest -notmatch '^sha256:[a-f0-9]{64}$') { throw '无法取得API候选镜像摘要' }
$body = @{
candidate_version = $candidate; build_digest = $buildDigest; environment = 'LAB'; run_type = 'PILOT'
scope = '本地PILOT候选证据包纵向验证'; required_dependencies = @('oidc_jwks')
reason = '验证技术验收快照与阶段8外部证据索引可形成带摘要的候选交付包'
} | ConvertTo-Json -Compress
$response = Invoke-RestMethod -Method Post -Uri "$api/api/v1/ops/diagnostics/acceptance-snapshots" -Headers @{
Authorization = "Bearer $token"; 'X-Tenant-Id' = $tenant; 'Idempotency-Key' = $key; 'X-Request-Id' = $requestID
} -ContentType 'application/json' -Body $body
if (-not $response.data.overall_gate_satisfied) { throw '当前技术验收快照门禁未通过,不能生成候选证据包' }
Write-JsonUtf8 $snapshotPath $response.data
$dailyPaths = @()
for ($day = 0; $day -lt 7; $day++) {
$localDate = $completedWeekStart.AddDays($day).ToString('yyyy-MM-dd')
$dailyKey = 'release-evidence-daily-' + [Guid]::NewGuid().ToString('N')
$idempotencyKeys += $dailyKey
$dailyBody = @{candidate_version = $candidate; build_digest = $buildDigest; local_date = $localDate; timezone = 'Asia/Shanghai'; reason = '生成已结束试点自然周的候选证据包日报'} | ConvertTo-Json -Compress
$dailyResponse = Invoke-RestMethod -Method Post -Uri "$api/api/v1/ops/diagnostics/pilot-daily-snapshots" -Headers @{
Authorization = "Bearer $token"; 'X-Tenant-Id' = $tenant; 'Idempotency-Key' = $dailyKey; 'X-Request-Id' = $requestID
} -ContentType 'application/json' -Body $dailyBody
$dailyPath = Join-Path $temporaryRoot "pilot-daily-$localDate.json"
Write-JsonUtf8 $dailyPath $dailyResponse.data
$dailyPaths += $dailyPath
}
$weeklyKey = 'release-evidence-weekly-' + [Guid]::NewGuid().ToString('N')
$idempotencyKeys += $weeklyKey
$weekStartText = $completedWeekStart.ToString('yyyy-MM-dd')
$weeklyBody = @{candidate_version = $candidate; build_digest = $buildDigest; week_start = $weekStartText; timezone = 'Asia/Shanghai'; reason = '生成已结束试点自然周的候选证据包周报'} | ConvertTo-Json -Compress
$weeklyResponse = Invoke-RestMethod -Method Post -Uri "$api/api/v1/ops/diagnostics/pilot-weekly-snapshots" -Headers @{
Authorization = "Bearer $token"; 'X-Tenant-Id' = $tenant; 'Idempotency-Key' = $weeklyKey; 'X-Request-Id' = $requestID
} -ContentType 'application/json' -Body $weeklyBody
if (-not $weeklyResponse.data.is_complete) { throw '已结束自然周不应生成周中快照' }
$weeklyPath = Join-Path $temporaryRoot "pilot-weekly-$weekStartText.json"
Write-JsonUtf8 $weeklyPath $weeklyResponse.data
$codes = @('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')
$index = @{
schema_version = 'release-evidence-index-v2'; candidate_version = $candidate; build_digest = $buildDigest
items = @($codes | ForEach-Object { @{code = $_; status = 'PENDING'; owner = '待外部责任人确认'; note = '本地证据包保留此外部验收缺口,不以模拟结果代替'; files = @()} })
}
Write-JsonUtf8 $indexPath $index 8
$bundleArguments = @('run','./cmd/release-evidence','-root','.', '-candidate-version',$candidate,'-build-digest',$buildDigest,'-source-commit','local-uncommitted','-acceptance-snapshot',$snapshotPath,'-evidence-index',$indexPath,'-pilot-weekly-snapshot',$weeklyPath,'-out',$bundlePath)
foreach ($dailyPath in $dailyPaths) { $bundleArguments += @('-pilot-daily-snapshot',$dailyPath) }
$resultLine = & go @bundleArguments
$result = $resultLine | ConvertFrom-Json
if ($result.status -ne 'EXTERNAL_VALIDATION_PENDING' -or $result.pending -ne 10 -or $result.verified -ne 0 -or $result.sha256 -notmatch '^sha256:[a-f0-9]{64}$') { throw '证据包生成结果不正确' }
Expand-Archive -LiteralPath $bundlePath -DestinationPath $expandedPath
$manifest = Get-Content -Raw -LiteralPath (Join-Path $expandedPath 'release-manifest.json') | ConvertFrom-Json
2026-08-26 22:04:38 +08:00
if (-not $manifest.technical_gate_satisfied -or $manifest.evidence_status -ne 'EXTERNAL_VALIDATION_PENDING' -or $manifest.pending_count -ne 10 -or $manifest.openapi_version -ne '0.66.0' -or $manifest.schema_version -ne 'release-candidate-evidence-bundle-v3' -or -not $manifest.pilot_evidence_included -or $manifest.pilot_daily_count -ne 7 -or $manifest.pilot_week_start -ne $weekStartText -or $manifest.pilot_timezone -ne 'Asia/Shanghai') { throw '证据包清单内容不正确' }
2026-08-26 06:41:07 +08:00
if ($manifest.firmware_validation.status -ne 'PENDING' -or $manifest.firmware_validation.coverage_complete -or $manifest.firmware_validation.required_device_count -ne 3 -or @($manifest.firmware_validation.required_scenarios).Count -ne 4 -or @($manifest.firmware_validation.gaps).Count -ne 5) { throw '真机固件场景缺口未在证据包中保留' }
if ($manifest.alert_receiver_validation.status -ne 'PENDING' -or $manifest.alert_receiver_validation.coverage_complete -or $manifest.alert_receiver_validation.report_present -or @($manifest.alert_receiver_validation.gaps).Count -ne 2) { throw '真实告警接收器与正式演练缺口未在证据包中保留' }
if ($manifest.kms_rotation_validation.status -ne 'PENDING' -or $manifest.kms_rotation_validation.coverage_complete -or $manifest.kms_rotation_validation.report_present -or @($manifest.kms_rotation_validation.gaps).Count -ne 3) { throw '真实KMS/HSM两类轮换缺口未在证据包中保留' }
if ($manifest.production_recovery_validation.status -ne 'PENDING' -or $manifest.production_recovery_validation.coverage_complete -or $manifest.production_recovery_validation.report_present -or @($manifest.production_recovery_validation.gaps).Count -ne 5) { throw '生产等价四类恢复演练缺口未在证据包中保留' }
if ($manifest.production_infrastructure_validation.status -ne 'PENDING' -or $manifest.production_infrastructure_validation.coverage_complete -or $manifest.production_infrastructure_validation.report_present -or @($manifest.production_infrastructure_validation.gaps).Count -ne 8) { throw '生产基础设施七类组件缺口未在证据包中保留' }
if ($manifest.safari_compatibility_validation.status -ne 'PENDING' -or $manifest.safari_compatibility_validation.coverage_complete -or $manifest.safari_compatibility_validation.report_present -or @($manifest.safari_compatibility_validation.gaps).Count -ne 3) { throw '最近两个真实Safari版本缺口未在证据包中保留' }
if ($manifest.real_device_rtc_validation.status -ne 'PENDING' -or $manifest.real_device_rtc_validation.coverage_complete -or $manifest.real_device_rtc_validation.report_present -or @($manifest.real_device_rtc_validation.gaps).Count -ne 10) { throw '真实终端、三模式和四类RTC场景缺口未在证据包中保留' }
if ($manifest.real_agent_models_validation.status -ne 'PENDING' -or $manifest.real_agent_models_validation.coverage_complete -or $manifest.real_agent_models_validation.report_present -or @($manifest.real_agent_models_validation.gaps).Count -ne 16) { throw '真实生成、Embedding和内容安全质量缺口未在证据包中保留' }
if ($manifest.remote_ci_supply_chain_validation.status -ne 'PENDING' -or $manifest.remote_ci_supply_chain_validation.coverage_complete -or $manifest.remote_ci_supply_chain_validation.report_present -or @($manifest.remote_ci_supply_chain_validation.gaps).Count -ne 20) { throw '远程CI、镜像摘要和SBOM缺口未在证据包中保留' }
foreach ($required in @('evidence-index.json','acceptance/technical-acceptance-snapshot.json','baseline/openapi/openapi.yaml','baseline/docs/external-acceptance-checklist.md',"pilot/weekly/$weekStartText.json",("pilot/daily/" + $completedWeekStart.AddDays(6).ToString('yyyy-MM-dd') + '.json'))) {
if (-not (Test-Path -LiteralPath (Join-Path $expandedPath $required))) { throw "证据包缺少文件:$required" }
}
Write-Host "发布候选证据包烟测通过:文件=$($result.files),PILOT日报=7,周报=$weekStartText,外部待验=$($result.pending),远程CI、真机RTC/固件、Agent模型、告警、KMS、恢复、基础设施和Safari保持PENDING,包摘要=$($result.sha256)。"
} finally {
$quotedKeys = ($idempotencyKeys | ForEach-Object { "'$_'" }) -join ','
docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -c "BEGIN; DELETE FROM audit_event WHERE request_id='$requestID'; DELETE FROM idempotency_record WHERE idempotency_key IN ($quotedKeys); COMMIT;" | Out-Null
if (Test-Path -LiteralPath $temporaryRoot) { Remove-Item -LiteralPath $temporaryRoot -Recurse -Force }
Write-Host '已清理证据包烟测的临时文件、审计和幂等记录。'
}