feat(release): add incremental gate and auto CI status for 103 publish

When the runtime artifact is unchanged, carry forward prior gate results and
re-run only REL scenarios; resolve REL-02 from Gitea commit status and allow
stable promotion when canary matches the same artifact bundle.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-27 08:32:12 +08:00
parent 9e2aa4f71d
commit a6a9bb4eab
11 changed files with 533 additions and 10 deletions
+20 -2
View File
@@ -124,10 +124,28 @@ export function validateGateReport(report, {
expectedBranch = 'main',
now = new Date(),
requireFullCatalog = true,
allowExpired = false,
} = {}) {
const errors = [];
if (report?.schema_version !== 1) errors.push('schema_version must be 1');
if (report?.mode !== 'all' && requireFullCatalog) errors.push('release report mode must be all');
if (report?.mode !== 'all' && report?.mode !== 'incremental' && requireFullCatalog) {
errors.push('release report mode must be all or incremental');
}
if (report?.mode === 'incremental') {
const baseline = report?.baseline;
if (!baseline || typeof baseline !== 'object') {
errors.push('incremental report is missing baseline metadata');
} else {
for (const field of ['commit_sha', 'report_path', 'carried_forward', 'reexecuted']) {
if (baseline[field] === undefined || baseline[field] === null || baseline[field] === '') {
errors.push(`incremental baseline is missing ${field}`);
}
}
if (Number(baseline.carried_forward) + Number(baseline.reexecuted) !== report?.scenarios?.length) {
errors.push('incremental baseline counts do not match scenario total');
}
}
}
if (!/^[0-9a-f]{40}$/i.test(report?.commit_sha ?? '')) errors.push('commit_sha must be a full SHA');
if (expectedCommit && report?.commit_sha !== expectedCommit) errors.push('commit_sha does not match candidate');
if (expectedBranch && report?.branch !== expectedBranch) errors.push(`branch must be ${expectedBranch}`);
@@ -148,7 +166,7 @@ export function validateGateReport(report, {
} else {
if (expiresAt <= completedAt) errors.push('report expiry must be after completion');
if (expiresAt - completedAt > 4 * 60 * 60 * 1000) errors.push('report validity exceeds four hours');
if (new Date(now).getTime() > expiresAt) errors.push('report has expired');
if (!allowExpired && new Date(now).getTime() > expiresAt) errors.push('report has expired');
}
if (!Array.isArray(report?.scenarios)) {