feat(acceptance): 建立同构验收与弹性容量体系
实现本地三节点 K3s 同构环境、脱敏生产快照、Gemini 图片和多参考图视频协议模拟、统一验收报告及故障注入。\n\n新增 Worker 容量控制器、资源与连接预算、任务恢复保护,并将生产验收拆分为 validation 执行和人工 CAS 放量。\n\n验证包括 Go 全量测试、PostgreSQL HTTP 集成测试、go vet、OpenAPI、ShellCheck、前端检查、迁移及发布脚本测试。
This commit is contained in:
@@ -32,6 +32,7 @@ function releaseIntegrity(manifest) {
|
||||
const payload = structuredClone(manifest)
|
||||
delete payload.integrity
|
||||
delete payload.deployment
|
||||
delete payload.certification
|
||||
return createHash('sha256').update(JSON.stringify(payload)).digest('hex')
|
||||
}
|
||||
|
||||
@@ -58,6 +59,8 @@ function validate(manifest) {
|
||||
'migrationsChanged', 'images', 'smoke', 'integrity',
|
||||
]
|
||||
if (manifest.deployment !== undefined) rootKeys.push('deployment')
|
||||
if (manifest.certification !== undefined) rootKeys.push('certification')
|
||||
if (manifest.migrationVersion !== undefined) rootKeys.push('migrationVersion')
|
||||
requireExactKeys(manifest, rootKeys, 'manifest')
|
||||
if (manifest.schemaVersion !== 1) fail('schemaVersion must be 1')
|
||||
if (!shaPattern.test(manifest.sourceSha || '')) fail('sourceSha must be a full lowercase Git SHA')
|
||||
@@ -84,6 +87,10 @@ function validate(manifest) {
|
||||
if (manifest.migrationsChanged && !manifest.components.includes('api')) {
|
||||
fail('migration changes require the api component')
|
||||
}
|
||||
if (manifest.migrationVersion !== undefined &&
|
||||
!/^\d{4}_[a-z0-9][a-z0-9_]*$/.test(manifest.migrationVersion)) {
|
||||
fail('migrationVersion must identify the latest migration without .sql')
|
||||
}
|
||||
if (!manifest.images || typeof manifest.images !== 'object') fail('images must be an object')
|
||||
requireExactKeys(manifest.images, ['api', 'web'], 'images')
|
||||
for (const component of allowedComponents) {
|
||||
@@ -126,6 +133,28 @@ function validate(manifest) {
|
||||
fail('deployment record is invalid')
|
||||
}
|
||||
}
|
||||
if (manifest.certification !== undefined) {
|
||||
if (!manifest.certification || typeof manifest.certification !== 'object') {
|
||||
fail('certification must be an object')
|
||||
}
|
||||
requireExactKeys(manifest.certification, [
|
||||
'status', 'runId', 'certifiedAt', 'configHash', 'capacityProfile',
|
||||
'workerInstanceSlots', 'workerMaxReplicasNingbo', 'workerMaxReplicasHongkong',
|
||||
], 'certification')
|
||||
if (manifest.certification.status !== 'passed' ||
|
||||
!/^[0-9a-f-]{36}$/.test(manifest.certification.runId || '') ||
|
||||
!Number.isFinite(Date.parse(manifest.certification.certifiedAt)) ||
|
||||
!/^[0-9a-f]{64}$/.test(manifest.certification.configHash || '') ||
|
||||
!['P24', 'P28', 'P32'].includes(manifest.certification.capacityProfile) ||
|
||||
!Number.isInteger(manifest.certification.workerInstanceSlots) ||
|
||||
manifest.certification.workerInstanceSlots < 1 ||
|
||||
!Number.isInteger(manifest.certification.workerMaxReplicasNingbo) ||
|
||||
manifest.certification.workerMaxReplicasNingbo < 1 ||
|
||||
!Number.isInteger(manifest.certification.workerMaxReplicasHongkong) ||
|
||||
manifest.certification.workerMaxReplicasHongkong < 1) {
|
||||
fail('certification record is invalid')
|
||||
}
|
||||
}
|
||||
|
||||
const forbiddenKey = /(password|secret|token|credential|private.?key)/i
|
||||
const visit = (value, path = '') => {
|
||||
@@ -166,6 +195,7 @@ if (command === 'create') {
|
||||
targetPlatform: 'linux/amd64',
|
||||
components,
|
||||
migrationsChanged,
|
||||
migrationVersion: requireEnv('RELEASE_MIGRATION_VERSION', /^\d{4}_[a-z0-9][a-z0-9_]*$/),
|
||||
images: {
|
||||
api: requireEnv('RELEASE_API_IMAGE', digestImagePattern),
|
||||
web: requireEnv('RELEASE_WEB_IMAGE', digestImagePattern),
|
||||
@@ -206,6 +236,31 @@ if (command === 'record-deployment') {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (command === 'record-certification') {
|
||||
if (!field) fail('record-certification requires an output path')
|
||||
const certifiedAt = requireEnv('RELEASE_CERTIFIED_AT')
|
||||
const runId = requireEnv('RELEASE_ACCEPTANCE_RUN_ID', /^[0-9a-f-]{36}$/)
|
||||
const configHash = requireEnv('RELEASE_CONFIG_HASH', /^[0-9a-f]{64}$/)
|
||||
const capacityProfile = requireEnv('RELEASE_CAPACITY_PROFILE', /^P(?:24|28|32)$/)
|
||||
const workerInstanceSlots = Number(process.env.RELEASE_WORKER_INSTANCE_SLOTS || '')
|
||||
const workerMaxReplicasNingbo = Number(process.env.RELEASE_WORKER_MAX_REPLICAS_NINGBO || '')
|
||||
const workerMaxReplicasHongkong = Number(process.env.RELEASE_WORKER_MAX_REPLICAS_HONGKONG || '')
|
||||
manifest.certification = {
|
||||
status: 'passed',
|
||||
runId,
|
||||
certifiedAt,
|
||||
configHash,
|
||||
capacityProfile,
|
||||
workerInstanceSlots,
|
||||
workerMaxReplicasNingbo,
|
||||
workerMaxReplicasHongkong,
|
||||
}
|
||||
validate(manifest)
|
||||
writeFileSync(field, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx', mode: 0o600 })
|
||||
console.log(`release_manifest=CERTIFIED release=${manifest.releaseId} run=${runId}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (command === 'validate') {
|
||||
const digest = createHash('sha256').update(readFileSync(path)).digest('hex')
|
||||
console.log(`release_manifest=PASS release=${manifest.releaseId} sha256=${digest}`)
|
||||
|
||||
Reference in New Issue
Block a user