Files
tutti/apps/desktop/scripts/validate-release-summary.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

145 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
import { readFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const schemaVersion = "tutti.desktop.release.summary.v1";
const releaseTagPattern =
/^v(?<version>(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?<channel>rc|beta)\.(?:0|[1-9]\d*))?)$/u;
function requireNonEmptyString(value, label) {
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${label} must be a non-empty string`);
}
}
function validateLocale(locale, label) {
if (!locale || typeof locale !== "object" || Array.isArray(locale)) {
throw new Error(`${label} must be an object`);
}
requireNonEmptyString(locale.headline, `${label}.headline`);
if (!Array.isArray(locale.sections) || locale.sections.length === 0) {
throw new Error(`${label}.sections must be a non-empty array`);
}
for (const [sectionIndex, section] of locale.sections.entries()) {
requireNonEmptyString(
section?.title,
`${label}.sections[${sectionIndex}].title`
);
if (!Array.isArray(section?.items) || section.items.length === 0) {
throw new Error(
`${label}.sections[${sectionIndex}].items must be a non-empty array`
);
}
for (const [itemIndex, item] of section.items.entries()) {
requireNonEmptyString(
item,
`${label}.sections[${sectionIndex}].items[${itemIndex}]`
);
}
}
if (!Array.isArray(locale.qaFocus)) {
throw new Error(`${label}.qaFocus must be an array`);
}
for (const [itemIndex, item] of locale.qaFocus.entries()) {
requireNonEmptyString(item, `${label}.qaFocus[${itemIndex}]`);
}
}
function validateReleaseSummary(summary, expected = {}) {
if (!summary || typeof summary !== "object" || Array.isArray(summary)) {
throw new Error("release summary must be an object");
}
if (summary.schemaVersion !== schemaVersion) {
throw new Error(
`Unexpected release summary schema: ${summary.schemaVersion}`
);
}
const tagMatch = releaseTagPattern.exec(summary.tag);
if (!tagMatch?.groups) {
throw new Error(`Unsupported release summary tag: ${summary.tag}`);
}
const derivedChannel = tagMatch.groups.channel ?? "stable";
const derivedPrerelease = derivedChannel !== "stable";
if (summary.version !== tagMatch.groups.version) {
throw new Error("release summary version does not match its tag");
}
if (summary.channel !== derivedChannel) {
throw new Error("release summary channel does not match its tag");
}
if (summary.prerelease !== derivedPrerelease) {
throw new Error(
"release summary prerelease flag does not match its channel"
);
}
requireNonEmptyString(summary.targetCommit, "release summary targetCommit");
requireNonEmptyString(summary.generatedAt, "release summary generatedAt");
if (Number.isNaN(Date.parse(summary.generatedAt))) {
throw new Error("release summary generatedAt must be an ISO date-time");
}
if (
summary.summarySource !== "agnes" &&
summary.summarySource !== "fallback" &&
summary.summarySource !== "human-reviewed"
) {
throw new Error(
"release summary summarySource must be agnes, fallback, or human-reviewed"
);
}
if (
!summary.compare ||
typeof summary.compare !== "object" ||
Array.isArray(summary.compare)
) {
throw new Error("release summary compare must be an object");
}
if (
summary.compare.from !== null &&
typeof summary.compare.from !== "string"
) {
throw new Error("release summary compare.from must be a string or null");
}
requireNonEmptyString(summary.compare.range, "release summary compare.range");
requireNonEmptyString(summary.compare.to, "release summary compare.to");
validateLocale(summary.zh, "release summary zh");
validateLocale(summary.en, "release summary en");
for (const [field, expectedValue] of Object.entries(expected)) {
if (expectedValue && summary[field] !== expectedValue) {
throw new Error(
`release summary ${field} does not match the staged release`
);
}
}
return summary;
}
async function main() {
const [summaryPath] = process.argv.slice(2);
if (!summaryPath) {
throw new Error("Usage: validate-release-summary.mjs <summary-json-path>");
}
const summary = JSON.parse(await readFile(summaryPath, "utf8"));
validateReleaseSummary(summary, {
tag: process.env.RELEASE_TAG,
channel: process.env.RELEASE_CHANNEL,
targetCommit: process.env.RELEASE_TARGET
});
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
void main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
});
}
export { schemaVersion, validateReleaseSummary };