diff --git a/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs b/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs index e79bf2d2b7a7..e65b6f8fb7b3 100644 --- a/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs +++ b/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs @@ -1,7 +1,15 @@ #!/usr/bin/env node import { execFileSync, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { @@ -2004,6 +2012,19 @@ function releaseChecks(changelog, version, releaseTags) { return checks; } +function writeFileAtomic(filePath, contents) { + const directory = path.dirname(filePath); + mkdirSync(directory, { recursive: true }); + const tempDirectory = mkdtempSync(path.join(directory, `.${path.basename(filePath)}.tmp-`)); + const tempPath = path.join(tempDirectory, path.basename(filePath)); + try { + writeFileSync(tempPath, contents); + renameSync(tempPath, filePath); + } finally { + rmSync(tempDirectory, { force: true, recursive: true }); + } +} + function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { @@ -2173,32 +2194,32 @@ function main() { ledger, relationships.directCommits, ); - if (options.manifestPath) { - writeFileSync(options.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + writeFileAtomic(options.manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); } + let candidateChangelog = changelog; + let candidateSection = section; if (options.writeLedger) { - changelog = replaceLedger( + candidateChangelog = replaceLedger( changelog, section, ledger.ledger, ledger.pullRequests, relationships.directCommits, ); - writeFileSync("CHANGELOG.md", changelog); - section = sectionFor(changelog, options.version); + candidateSection = sectionFor(candidateChangelog, options.version); } const errors = ledgerChecks( - section, + candidateSection, ledger.pullRequests, nodes, relationships.directCommits, source.shippedBaselines, ); const github = options.checkGithub - ? releaseChecks(changelog, options.version, options.releaseTags) + ? releaseChecks(candidateChangelog, options.version, options.releaseTags) : []; for (const check of github) { if (!check.matches) { @@ -2207,6 +2228,11 @@ function main() { ); } } + if (errors.length === 0) { + if (options.writeLedger) { + writeFileAtomic("CHANGELOG.md", candidateChangelog); + } + } const result = { base: options.base, diff --git a/test/scripts/verify-release-notes.test.ts b/test/scripts/verify-release-notes.test.ts index ccc9decc3a2b..9bdd243c1ea8 100644 --- a/test/scripts/verify-release-notes.test.ts +++ b/test/scripts/verify-release-notes.test.ts @@ -470,6 +470,57 @@ describe("release-note verification", () => { } }); + it("leaves CHANGELOG.md untouched when the rendered ledger fails validation", () => { + const cwd = mkdtempSync(join(tmpdir(), "openclaw-release-notes-")); + try { + git(cwd, ["init", "-q"]); + const changelog = [ + "# Changelog", + "", + "## 2026.7.1", + "", + "### Highlights", + "", + "- Only one highlight.", + "", + "### Changes", + "", + "### Fixes", + "", + ].join("\n"); + writeFileSync(join(cwd, "CHANGELOG.md"), changelog); + git(cwd, ["add", "CHANGELOG.md"]); + git(cwd, ["commit", "-qm", "initial"]); + const manifestPath = join(cwd, "release-manifest.json"); + + const result = spawnSync( + process.execPath, + [ + verifier, + "--base", + "HEAD", + "--target", + "HEAD", + "--main-ref", + "HEAD", + "--manifest", + manifestPath, + "--version", + "2026.7.1", + "--write-ledger", + ], + { cwd, encoding: "utf8" }, + ); + + expect(result.status).toBe(1); + expect(result.stdout).toContain("1 errors"); + expect(JSON.parse(readFileSync(manifestPath, "utf8")).version).toBe("2026.7.1"); + expect(readFileSync(join(cwd, "CHANGELOG.md"), "utf8")).toBe(changelog); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } + }); + it("rejects a release base that is not an ancestor of the target", () => { const cwd = mkdtempSync(join(tmpdir(), "openclaw-release-notes-")); try {