Files
tutti/apps/desktop/scripts/upsert-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

188 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { buildLimitedGithubReleaseBody } from "./lib/githubReleaseBody.mjs";
const SECTION_START = "<!-- tutti-desktop-release-summary:start -->";
const SECTION_END = "<!-- tutti-desktop-release-summary:end -->";
const REVIEW_START = "<!-- tutti-desktop-release-review:start -->";
const REVIEW_END = "<!-- tutti-desktop-release-review:end -->";
const SUGGESTION_START = "<!-- tutti-desktop-release-suggestion:start -->";
const SUGGESTION_END = "<!-- tutti-desktop-release-suggestion:end -->";
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function sectionPattern(start, end) {
return new RegExp(
`\\n*${escapeRegex(start)}[\\s\\S]*?${escapeRegex(end)}\\n*`,
"g"
);
}
function removeSection(body, start, end) {
return body.replace(sectionPattern(start, end), "\n").trimEnd();
}
function extractSection(body, start, end) {
const match = body.match(sectionPattern(start, end));
return match?.[0]?.trim() ?? "";
}
function renderLocaleSummary(title, localeSummary) {
const lines = [`### ${title}`, "", localeSummary.headline, ""];
for (const section of localeSummary.sections ?? []) {
lines.push(`#### ${section.title}`);
for (const item of section.items ?? []) {
lines.push(`- ${item}`);
}
lines.push("");
}
return lines;
}
function renderReviewSection(summary) {
return [
REVIEW_START,
"## Release Notes",
"",
...renderLocaleSummary("中文", summary.zh),
...renderLocaleSummary("English", summary.en),
REVIEW_END
].join("\n");
}
function renderSuggestionSection(summary) {
return [
SUGGESTION_START,
"<details>",
"<summary>Current generated suggestions (review only)</summary>",
"",
...renderLocaleSummary("中文建议", summary.zh),
...renderLocaleSummary("English suggestions", summary.en),
"</details>",
SUGGESTION_END
].join("\n");
}
function removeManagedSection(body) {
return removeSection(body, SECTION_START, SECTION_END);
}
function renderReleaseSummarySection(summary) {
return renderReviewSection(summary);
}
function buildUpdatedReleaseBody({ existingBody, summary }) {
const existingReview = extractSection(existingBody, REVIEW_START, REVIEW_END);
let cleanedBody = removeManagedSection(existingBody);
cleanedBody = removeSection(cleanedBody, REVIEW_START, REVIEW_END);
cleanedBody = removeSection(cleanedBody, SUGGESTION_START, SUGGESTION_END);
return buildLimitedGithubReleaseBody({
existingBody: cleanedBody,
leadingSections: [
existingReview || renderReviewSection(summary),
renderSuggestionSection(summary)
]
});
}
function buildPublishedReleaseBody(body) {
return removeSection(body, SUGGESTION_START, SUGGESTION_END);
}
function parseLocaleSection(sectionBody, title, nextTitle = null) {
const start = sectionBody.indexOf(`### ${title}`);
if (start < 0) throw new Error(`Missing review locale: ${title}`);
const contentStart = start + `### ${title}`.length;
const next = nextTitle
? sectionBody.indexOf(`### ${nextTitle}`, contentStart)
: sectionBody.length;
const lines = sectionBody
.slice(contentStart, next < 0 ? sectionBody.length : next)
.split("\n")
.map((line) => line.trim());
const headline = lines.find((line) => line && !line.startsWith("#"));
if (!headline) throw new Error(`${title} review headline is required`);
const sections = [];
let current = null;
for (const line of lines) {
if (line.startsWith("#### ")) {
current = { title: line.slice(5).trim(), items: [] };
sections.push(current);
} else if (
current &&
line &&
!line.startsWith("#") &&
!line.startsWith("<!--")
) {
current.items.push(line.startsWith("- ") ? line.slice(2).trim() : line);
}
}
const validSections = sections.filter(
(section) => section.title && section.items.length > 0
);
if (validSections.length === 0) {
throw new Error(`${title} review must contain at least one section`);
}
return { headline, sections: validSections, qaFocus: [] };
}
function buildApprovedReleaseSummary({ body, generatedSummary, reviewedAt }) {
const review = extractSection(body, REVIEW_START, REVIEW_END);
if (!review) throw new Error("Release review section is missing");
return {
...generatedSummary,
generatedAt: reviewedAt ?? generatedSummary.generatedAt,
summarySource: "human-reviewed",
warning: undefined,
zh: parseLocaleSection(review, "中文", "English"),
en: parseLocaleSection(review, "English")
};
}
async function main() {
const [existingBodyPath, summaryPath, outputBodyPath] = process.argv.slice(2);
if (!existingBodyPath || !summaryPath || !outputBodyPath) {
throw new Error(
"Usage: upsert-release-summary.mjs <existing-body> <summary-json> <output-body>"
);
}
const existingBody = await readFile(existingBodyPath, "utf8");
const summary = JSON.parse(await readFile(summaryPath, "utf8"));
const updated = buildUpdatedReleaseBody({ existingBody, summary });
await writeFile(
outputBodyPath,
process.env.RELEASE_SUMMARY_MODE === "published"
? buildPublishedReleaseBody(updated)
: updated,
"utf8"
);
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
void main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}
export {
REVIEW_END,
REVIEW_START,
SECTION_END,
SECTION_START,
SUGGESTION_END,
SUGGESTION_START,
buildApprovedReleaseSummary,
buildPublishedReleaseBody,
buildUpdatedReleaseBody,
removeManagedSection,
renderReleaseSummarySection
};