
























1+#!/usr/bin/env node
2+3+import { existsSync } from "node:fs";
4+import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5+import path from "node:path";
6+import { fileURLToPath } from "node:url";
7+8+const CHANGELOG_PATH = "CHANGELOG.md";
9+const PACKAGE_JSON_PATH = "package.json";
10+const BACKUP_PATH = path.join(".artifacts", "package-changelog", "CHANGELOG.md.prepack-backup");
11+const MAX_PACKAGED_CHANGELOG_BYTES = 500 * 1024;
12+const MIN_RELEASE_SECTION_BODY_BYTES = 32;
13+const UNRELEASED_HEADING = "Unreleased";
14+const RELEASE_HEADING_PATTERN =
15+/^##\s+([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(?:(?:-(?:alpha|beta)\.[1-9][0-9]*)|(?:-[1-9][0-9]*))?)(?:\s+.*)?$/u;
16+const RELEASE_VERSION_PATTERN =
17+/^([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*)(?:(?:-(?:alpha|beta)\.[1-9][0-9]*)|(?:-[1-9][0-9]*))?$/u;
18+const PRERELEASE_VERSION_PATTERN =
19+/^([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*)-(?:alpha|beta)\.[1-9][0-9]*$/u;
20+21+export function resolvePackageChangelogVersions(packageVersion) {
22+const match = RELEASE_VERSION_PATTERN.exec(packageVersion);
23+if (!match) {
24+throw new Error(
25+`Unsupported OpenClaw package version for changelog packaging: ${packageVersion}`,
26+);
27+}
28+if (PRERELEASE_VERSION_PATTERN.test(packageVersion)) {
29+return [packageVersion, match[1], UNRELEASED_HEADING];
30+}
31+return [packageVersion];
32+}
33+34+function splitLines(content) {
35+return content.replace(/^\uFEFF/u, "").split(/\r?\n/u);
36+}
37+38+function parseLevelTwoHeading(line) {
39+const releaseMatch = RELEASE_HEADING_PATTERN.exec(line);
40+if (releaseMatch) {
41+return releaseMatch[1];
42+}
43+return /^##\s+Unreleased(?:\s+.*)?$/u.test(line) ? UNRELEASED_HEADING : null;
44+}
45+46+function findLevelTwoHeadings(lines) {
47+return lines.flatMap((line, index) => {
48+const version = parseLevelTwoHeading(line);
49+return version ? [{ index, version }] : [];
50+});
51+}
52+53+function extractPreamble(lines, firstHeadingIndex) {
54+return lines.slice(0, firstHeadingIndex).join("\n").trimEnd();
55+}
56+57+export function extractCurrentPackageChangelog(content, packageVersion) {
58+const targetVersions = resolvePackageChangelogVersions(packageVersion);
59+const lines = splitLines(content);
60+const headings = findLevelTwoHeadings(lines);
61+const heading = targetVersions
62+.map((version) => headings.find((entry) => entry.version === version))
63+.find((entry) => entry !== undefined);
64+if (!heading) {
65+throw new Error(
66+`CHANGELOG.md does not contain a release section for ${targetVersions.join(" or ")}.`,
67+);
68+}
69+const nextHeading = headings.find((entry) => entry.index > heading.index);
70+const firstLevelTwoHeadingIndex = lines.findIndex((line) => line.startsWith("## "));
71+const preamble = extractPreamble(lines, firstLevelTwoHeadingIndex);
72+const releaseSection = lines
73+.slice(heading.index, nextHeading?.index ?? lines.length)
74+.join("\n")
75+.trimEnd();
76+const releaseBody = releaseSection.split(/\r?\n/u).slice(1).join("\n").trim();
77+const releaseBodyBytes = Buffer.byteLength(releaseBody, "utf8");
78+if (releaseBodyBytes < MIN_RELEASE_SECTION_BODY_BYTES) {
79+throw new Error(
80+`Packaged changelog section for ${heading.version} is only ${releaseBodyBytes} body bytes, which is below the ${MIN_RELEASE_SECTION_BODY_BYTES} byte safety minimum.`,
81+);
82+}
83+const packaged = `${preamble}\n\n${releaseSection}\n`;
84+const packagedBytes = Buffer.byteLength(packaged, "utf8");
85+if (packagedBytes > MAX_PACKAGED_CHANGELOG_BYTES) {
86+throw new Error(
87+`Packaged changelog is ${packagedBytes} bytes, which exceeds the ${MAX_PACKAGED_CHANGELOG_BYTES} byte safety limit.`,
88+);
89+}
90+return packaged;
91+}
92+93+async function readPackageVersion(cwd) {
94+const packageJsonPath = path.join(cwd, PACKAGE_JSON_PATH);
95+const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
96+if (typeof packageJson.version !== "string") {
97+throw new Error("package.json version must be a string.");
98+}
99+return packageJson.version;
100+}
101+102+export async function restorePackageChangelog(cwd = process.cwd()) {
103+const backupPath = path.join(cwd, BACKUP_PATH);
104+if (!existsSync(backupPath)) {
105+return false;
106+}
107+const changelogPath = path.join(cwd, CHANGELOG_PATH);
108+const backup = await readFile(backupPath, "utf8");
109+await writeFile(changelogPath, backup, "utf8");
110+await rm(backupPath, { force: true });
111+return true;
112+}
113+114+export async function preparePackageChangelog(cwd = process.cwd()) {
115+await restorePackageChangelog(cwd);
116+const changelogPath = path.join(cwd, CHANGELOG_PATH);
117+const backupPath = path.join(cwd, BACKUP_PATH);
118+const original = await readFile(changelogPath, "utf8");
119+const packageVersion = await readPackageVersion(cwd);
120+const packaged = extractCurrentPackageChangelog(original, packageVersion);
121+if (packaged === original) {
122+return false;
123+}
124+await mkdir(path.dirname(backupPath), { recursive: true });
125+await writeFile(backupPath, original, "utf8");
126+await writeFile(changelogPath, packaged, "utf8");
127+return true;
128+}
129+130+async function main(argv = process.argv.slice(2)) {
131+const command = argv[0];
132+if (command === "prepare") {
133+const changed = await preparePackageChangelog();
134+console.error(
135+changed
136+ ? "package-changelog: wrote current release notes for package tarball."
137+ : "package-changelog: source changelog already matches package notes.",
138+);
139+return;
140+}
141+if (command === "restore") {
142+const restored = await restorePackageChangelog();
143+console.error(
144+restored
145+ ? "package-changelog: restored source CHANGELOG.md."
146+ : "package-changelog: no packaged changelog backup to restore.",
147+);
148+return;
149+}
150+console.error("Usage: node scripts/package-changelog.mjs <prepare|restore>");
151+process.exitCode = 1;
152+}
153+154+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
155+await main();
156+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。