diff --git a/scripts/package-changelog.mjs b/scripts/package-changelog.mjs index a72c6115e4a..13963153005 100644 --- a/scripts/package-changelog.mjs +++ b/scripts/package-changelog.mjs @@ -105,7 +105,27 @@ export async function restorePackageChangelog(cwd = process.cwd()) { return false; } const changelogPath = path.join(cwd, CHANGELOG_PATH); - const backup = await readFile(backupPath, "utf8"); + const [backup, current] = await Promise.all([ + readFile(backupPath, "utf8"), + readFile(changelogPath, "utf8"), + ]); + if (current !== backup) { + const packageVersion = await readPackageVersion(cwd); + let expectedPackaged; + try { + expectedPackaged = extractCurrentPackageChangelog(backup, packageVersion); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error( + `Refusing to restore stale packaged changelog backup from ${BACKUP_PATH}: ${message}`, + ); + } + if (current !== expectedPackaged) { + throw new Error( + `Refusing to restore packaged changelog backup from ${BACKUP_PATH} because CHANGELOG.md has changed since the backup was written.`, + ); + } + } await writeFile(changelogPath, backup, "utf8"); await rm(backupPath, { force: true }); return true; diff --git a/test/scripts/package-changelog.test.ts b/test/scripts/package-changelog.test.ts index a6b1a0224e0..671c26c53ba 100644 --- a/test/scripts/package-changelog.test.ts +++ b/test/scripts/package-changelog.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -167,4 +167,29 @@ Docs: https://docs.openclaw.ai rmSync(root, { recursive: true, force: true }); } }); + + it("refuses to restore stale backups over current changelog edits", async () => { + const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-package-changelog-")); + const backupPath = path.join( + root, + ".artifacts", + "package-changelog", + "CHANGELOG.md.prepack-backup", + ); + const editedChangelog = cumulativeChangelog.replace("- Current fix.", "- Current fix edited."); + try { + writeFileSync(path.join(root, "package.json"), '{"version":"2026.5.28-beta.1"}\n', "utf8"); + writeFileSync(path.join(root, "CHANGELOG.md"), editedChangelog, "utf8"); + mkdirSync(path.dirname(backupPath), { recursive: true }); + writeFileSync(backupPath, cumulativeChangelog, "utf8"); + + await expect(restorePackageChangelog(root)).rejects.toThrow( + "Refusing to restore packaged changelog backup", + ); + expect(readFileSync(path.join(root, "CHANGELOG.md"), "utf8")).toBe(editedChangelog); + expect(existsSync(backupPath)).toBe(true); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); });