Files
easyai-ai-gateway/scripts/acceptance/report.mjs
T
wangbo 0b681275ed fix(acceptance): 支持显式跳过本地验收
在用户明确授权时,将本地原生与 amd64 制品阶段记录为 skipped/waived,并直接使用当前生产脱敏快照进入线上模拟。修复线上报告合并未写入生产 Run ID 导致后续 promote CAS 必然失败的问题。已通过报告回归测试、bash -n、ShellCheck 和 diff 检查。
2026-07-31 23:15:22 +08:00

399 lines
15 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { createHash } from 'node:crypto';
import { lstat, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
const shaPattern = /^[0-9a-f]{40}$/;
const digestPattern = /^sha256:[0-9a-f]{64}$/;
const hashPattern = /^[0-9a-f]{64}$/;
const unsafeKeyPattern = /(password|secret|token|credential|authorization|api.?key|access.?key|private.?key|connection.?string|database.?url|proxy)/i;
function parseArgs(argv) {
const command = argv.shift();
const values = {};
while (argv.length > 0) {
const key = argv.shift();
if (!key?.startsWith('--') || argv.length === 0) throw new Error('invalid arguments');
values[key.slice(2)] = argv.shift();
}
return { command, values };
}
async function regularJSON(path) {
const absolute = resolve(path);
const info = await lstat(absolute);
if (!info.isFile() || info.isSymbolicLink()) throw new Error(`${path} must be a regular non-symlink file`);
return JSON.parse(await readFile(absolute, 'utf8'));
}
async function writeNewJSON(path, value) {
const absolute = resolve(path);
await mkdir(dirname(absolute), { recursive: true, mode: 0o700 });
await writeFile(absolute, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
}
async function replaceJSONAtomically(input, output, value) {
const inputPath = resolve(input);
const outputPath = resolve(output);
if (inputPath !== outputPath) {
await writeNewJSON(outputPath, value);
return;
}
const temporary = `${outputPath}.tmp-${process.pid}-${Date.now()}`;
try {
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
await rename(temporary, outputPath);
} catch (error) {
await unlink(temporary).catch(() => {});
throw error;
}
}
function assertSecretSafe(value, path = '$') {
if (Array.isArray(value)) {
value.forEach((item, index) => assertSecretSafe(item, `${path}[${index}]`));
return;
}
if (value && typeof value === 'object') {
for (const [key, nested] of Object.entries(value)) {
if (key !== 'secretSafe' && unsafeKeyPattern.test(key)) {
throw new Error(`unsafe report key at ${path}.${key}`);
}
assertSecretSafe(nested, `${path}.${key}`);
}
return;
}
if (typeof value === 'string' && /(?:postgres(?:ql)?|https?):\/\/[^\s]+/i.test(value)) {
throw new Error(`URL-like value is forbidden at ${path}`);
}
}
function validate(report) {
if (report.schemaVersion !== 'acceptance-report/v1' || report.secretSafe !== true) {
throw new Error('unsupported or non-secret-safe acceptance report');
}
if (!report.runId || !shaPattern.test(report.release?.sourceSha ?? '') ||
!digestPattern.test(report.release?.apiImageDigest ?? '') ||
!digestPattern.test(report.release?.workerImageDigest ?? '') ||
!shaPattern.test(report.snapshot?.sourceReleaseSha ?? '') ||
!hashPattern.test(report.snapshot?.configHash ?? '') ||
!hashPattern.test(report.snapshot?.sha256 ?? '')) {
throw new Error('acceptance report identity fields are invalid');
}
if (!report.stages || !Array.isArray(report.gates) ||
report.promotion?.requiresManualConfirmation !== true ||
!['validation', 'live', 'not-applicable'].includes(report.promotion?.trafficMode)) {
throw new Error('acceptance report stages, gates, or promotion are invalid');
}
for (const [name, stage] of Object.entries(report.stages)) {
if (!['passed', 'failed', 'not-run', 'skipped'].includes(stage?.status)) {
throw new Error(`invalid stage status for ${name}`);
}
}
assertSecretSafe(report);
}
async function buildOnlineWaiver(values) {
for (const key of ['release-sha', 'api-digest', 'snapshot', 'waiver-reason', 'output']) {
if (!values[key]) throw new Error(`--${key} is required`);
}
const snapshot = await regularJSON(values.snapshot);
if (!shaPattern.test(values['release-sha']) || !digestPattern.test(values['api-digest']) ||
snapshot.schemaVersion !== 'acceptance-snapshot/v1' ||
!shaPattern.test(snapshot.source?.releaseSha ?? '') ||
!hashPattern.test(snapshot.source?.configHash ?? '') ||
!hashPattern.test(snapshot.snapshotSha256 ?? '') ||
!/^[a-z0-9_]{3,80}$/.test(values['waiver-reason'])) {
throw new Error('online waiver identity fields or reason are invalid');
}
const report = {
schemaVersion: 'acceptance-report/v1',
runId: `online-waiver-${values['release-sha'].slice(0, 12)}`,
release: {
sourceSha: values['release-sha'],
apiImageDigest: values['api-digest'],
workerImageDigest: values['api-digest']
},
snapshot: {
sourceReleaseSha: snapshot.source.releaseSha,
configHash: snapshot.source.configHash,
sha256: snapshot.snapshotSha256
},
stages: {
localNative: { status: 'skipped', waiverReason: values['waiver-reason'] },
amd64Artifact: { status: 'skipped', waiverReason: values['waiver-reason'] },
onlineSimulation: { status: 'not-run' },
realCanary: { status: 'not-run' }
},
certifiedProfile: null,
gates: [
{
id: 'local_acceptance_explicitly_waived',
passed: true,
detail: values['waiver-reason']
}
],
promotion: {
ready: false,
requiresManualConfirmation: true,
trafficMode: 'not-applicable'
},
createdAt: new Date().toISOString(),
secretSafe: true
};
validate(report);
await writeNewJSON(values.output, report);
process.stdout.write('acceptance_report_online_waiver=PASS\n');
}
async function loadReports(directory) {
const entries = (await readdir(resolve(directory), { withFileTypes: true }))
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
.map((entry) => entry.name)
.sort();
const reports = [];
for (const name of entries) {
const report = await regularJSON(resolve(directory, name));
if (report.schemaVersion === 'acceptance-load-report/v1') {
if (report.secretSafe !== true) throw new Error(`load report ${name} is not secret-safe`);
reports.push({
file: name,
profile: report.profile,
passed: report.passed,
phases: report.phases,
failure: report.failure,
failureOperation: report.failureOperation,
startedAt: report.startedAt,
finishedAt: report.finishedAt
});
}
}
return reports;
}
async function buildLocal(values) {
for (const key of ['runtime', 'snapshot', 'reports', 'artifact', 'output']) {
if (!values[key]) throw new Error(`--${key} is required`);
}
const runtime = await regularJSON(values.runtime);
const snapshot = await regularJSON(values.snapshot);
const artifact = await regularJSON(values.artifact);
const loads = await loadReports(values.reports);
if (runtime.schemaVersion !== 'acceptance-runtime/v1' ||
snapshot.schemaVersion !== 'acceptance-snapshot/v1' ||
artifact.schemaVersion !== 'acceptance-artifact-smoke/v1') {
throw new Error('local report inputs have incompatible schema versions');
}
if (runtime.releaseSha !== artifact.releaseSha ||
artifact.apiImageDigest !== values['api-digest'] ||
runtime.snapshotConfigHash !== snapshot.source.configHash ||
runtime.snapshotSha256 !== snapshot.snapshotSha256) {
throw new Error('local report CAS identity mismatch');
}
const localPassed = loads.length > 0 && loads.every((item) => item.passed === true);
const gates = [
{ id: 'local_load_reports', passed: localPassed, detail: `${loads.length} load reports` },
{ id: 'amd64_artifact_smoke', passed: artifact.passed === true }
];
const report = {
schemaVersion: 'acceptance-report/v1',
runId: runtime.runId,
release: {
sourceSha: artifact.releaseSha,
apiImageDigest: artifact.apiImageDigest,
workerImageDigest: artifact.apiImageDigest
},
snapshot: {
sourceReleaseSha: snapshot.source.releaseSha,
configHash: snapshot.source.configHash,
sha256: snapshot.snapshotSha256
},
stages: {
localNative: { status: localPassed ? 'passed' : 'failed', loadReports: loads },
amd64Artifact: { status: artifact.passed ? 'passed' : 'failed', ...artifact },
onlineSimulation: { status: 'not-run' },
realCanary: { status: 'not-run' }
},
certifiedProfile: values['certified-profile'] ? {
name: values['certified-profile'],
source: 'local-candidate-only'
} : null,
gates,
promotion: {
ready: false,
requiresManualConfirmation: true,
trafficMode: 'not-applicable'
},
createdAt: new Date().toISOString(),
secretSafe: true
};
validate(report);
await writeNewJSON(values.output, report);
process.stdout.write(`acceptance_report_local=PASS sha256=${createHash('sha256').update(JSON.stringify(report)).digest('hex')}\n`);
}
async function buildLocalPartial(values) {
for (const key of ['runtime', 'snapshot', 'reports', 'output', 'failure-phase']) {
if (!values[key]) throw new Error(`--${key} is required`);
}
const runtime = await regularJSON(values.runtime);
const snapshot = await regularJSON(values.snapshot);
const loads = await loadReports(values.reports);
if (runtime.schemaVersion !== 'acceptance-runtime/v1' ||
snapshot.schemaVersion !== 'acceptance-snapshot/v1' ||
!shaPattern.test(runtime.releaseSha ?? '') ||
!digestPattern.test(runtime.apiImageDigest ?? '') ||
!digestPattern.test(runtime.workerImageDigest ?? '') ||
runtime.snapshotConfigHash !== snapshot.source.configHash ||
runtime.snapshotSha256 !== snapshot.snapshotSha256) {
throw new Error('partial local report inputs have incompatible CAS identity');
}
const completedLoadsPassed = loads.every((item) => item.passed === true);
const failureGate = values['failure-gate'] ?? 'local_execution_incomplete';
if (!/^[a-z0-9_]+$/.test(failureGate)) {
throw new Error('--failure-gate must be a stable lowercase gate ID');
}
const report = {
schemaVersion: 'acceptance-report/v1',
runId: runtime.runId,
release: {
sourceSha: runtime.releaseSha,
apiImageDigest: runtime.apiImageDigest,
workerImageDigest: runtime.workerImageDigest
},
snapshot: {
sourceReleaseSha: snapshot.source.releaseSha,
configHash: snapshot.source.configHash,
sha256: snapshot.snapshotSha256
},
stages: {
localNative: {
status: 'failed',
failurePhase: values['failure-phase'],
completedLoadReportsPassed: completedLoadsPassed,
loadReports: loads
},
amd64Artifact: { status: 'not-run' },
onlineSimulation: { status: 'not-run' },
realCanary: { status: 'not-run' }
},
certifiedProfile: values['certified-profile'] ? {
name: values['certified-profile'],
source: 'last-local-stable-candidate'
} : null,
gates: [
{
id: failureGate,
passed: false,
detail: `stopped during ${values['failure-phase']}`
},
{
id: 'completed_load_reports',
passed: completedLoadsPassed,
detail: `${loads.length} load reports`
}
],
promotion: {
ready: false,
requiresManualConfirmation: true,
trafficMode: 'not-applicable'
},
createdAt: new Date().toISOString(),
secretSafe: true
};
validate(report);
await writeNewJSON(values.output, report);
process.stdout.write('acceptance_report_local_partial=PASS\n');
}
async function mergeProduction(values) {
for (const key of ['local-report', 'production-summary', 'output']) {
if (!values[key]) throw new Error(`--${key} is required`);
}
const local = await regularJSON(values['local-report']);
const production = await regularJSON(values['production-summary']);
validate(local);
if (!production.runId || local.release.sourceSha !== production.releaseSha ||
local.release.apiImageDigest !== production.apiImageDigest ||
local.snapshot.configHash !== production.snapshotConfigHash ||
local.snapshot.sha256 !== production.snapshotSha256) {
throw new Error('production summary does not match local acceptance CAS fields');
}
local.runId = production.runId;
local.stages.onlineSimulation = {
status: production.passed === true ? 'passed' : 'failed',
stableCapacityProfile: production.stableCapacityProfile,
tasks: production.tasks,
geminiTasks: production.geminiTasks,
videoTasks: production.videoTasks
};
local.stages.realCanary = {
status: production.realCanaryPassed === true ? 'passed' : 'failed'
};
local.certifiedProfile = production.certifiedProfile ?? local.certifiedProfile;
local.gates.push(...(production.gates ?? []));
local.promotion = {
ready: production.passed === true && production.realCanaryPassed === true &&
local.gates.every((gate) => gate.passed === true),
requiresManualConfirmation: true,
trafficMode: 'validation'
};
local.createdAt = new Date().toISOString();
validate(local);
await writeNewJSON(values.output, local);
process.stdout.write(`acceptance_report_production=PASS promotion_ready=${local.promotion.ready}\n`);
}
const { command, values } = parseArgs(process.argv.slice(2));
if (command === 'validate') {
const report = await regularJSON(values.input ?? '');
validate(report);
process.stdout.write('acceptance_report_validate=PASS\n');
} else if (command === 'build-local') {
await buildLocal(values);
} else if (command === 'build-local-partial') {
await buildLocalPartial(values);
} else if (command === 'build-online-waiver') {
await buildOnlineWaiver(values);
} else if (command === 'merge-production') {
await mergeProduction(values);
} else if (command === 'mark-promoted') {
const input = values.input ?? '';
const output = values.output ?? input;
const report = await regularJSON(input);
validate(report);
if (report.promotion.ready !== true || report.promotion.trafficMode !== 'validation') {
throw new Error('only a ready validation report can be marked promoted');
}
report.promotion.trafficMode = 'live';
report.promotion.promotedAt = new Date().toISOString();
await replaceJSONAtomically(input, output, report);
process.stdout.write('acceptance_report_promoted=PASS\n');
} else if (command === 'attach-monitor') {
const input = values.input ?? '';
const output = values.output ?? input;
const report = await regularJSON(input);
const monitor = await regularJSON(values.monitor ?? '');
validate(report);
if (monitor.schemaVersion !== 'acceptance-monitor-report/v1' ||
monitor.runId !== report.runId || monitor.releaseSha !== report.release.sourceSha ||
monitor.secretSafe !== true) {
throw new Error('monitor report does not match overall acceptance report');
}
report.stages.postPromotionMonitor = {
status: monitor.passed === true ? 'passed' : 'failed',
startedAt: monitor.startedAt,
finishedAt: monitor.finishedAt,
samples: monitor.samples,
failureGateId: monitor.failureGateId
};
if (monitor.passed !== true) {
report.promotion.ready = false;
report.promotion.trafficMode = 'validation';
}
await replaceJSONAtomically(input, output, report);
process.stdout.write(`acceptance_report_monitor=PASS monitor_passed=${monitor.passed === true}\n`);
} else {
throw new Error('usage: report.mjs {validate|build-local|build-local-partial|build-online-waiver|merge-production|mark-promoted|attach-monitor} [options]');
}