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 b980b5ba796e..75a8c8ef6c5e 100644 --- a/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs +++ b/.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs @@ -317,13 +317,27 @@ function closingReferencesIn(text) { return references; } -function standardRevertedHash(message) { - return message +export function standardRevertedHash(message) { + const paragraphs = message .trim() .split(/\n\s*\n/) - .map((paragraph) => paragraph.trim()) - .map((paragraph) => paragraph.match(/^This reverts commit ([0-9a-f]{7,40})\.$/i)?.[1]) - .find(Boolean); + .map((paragraph) => paragraph.trim()); + const messageIsRevert = /^(?:[a-z][a-z0-9-]*(?:\([^)]+\))?!?:\s*)?revert\b/i.test( + paragraphs[0] ?? "", + ); + for (const [index, paragraph] of paragraphs.entries()) { + const revertedHash = paragraph.match(/^This reverts commit ([0-9a-f]{7,40})\.$/i)?.[1]; + if (!revertedHash) { + continue; + } + // GitHub squash messages can embed a reverted intermediate commit. Its + // marker follows the corresponding bullet and does not revert the squash. + if (!messageIsRevert && /^\*\s+Revert\b/i.test(paragraphs[index - 1] ?? "")) { + continue; + } + return revertedHash; + } + return undefined; } function handlesIn(text) { diff --git a/scripts/docker/install-sh-smoke/run.sh b/scripts/docker/install-sh-smoke/run.sh index fa9491811152..8727df3bed9d 100755 --- a/scripts/docker/install-sh-smoke/run.sh +++ b/scripts/docker/install-sh-smoke/run.sh @@ -482,6 +482,8 @@ if (typeof updateStep.command !== "string" || !updateStep.command.includes(expec throw new Error(`global update step missing expected tgz URL: ${JSON.stringify(updateStep)}`); } const doctorStep = steps.find((step) => step?.name === "openclaw doctor"); +// Every baseline that passes verify_installed_cli implements this contract; +// the sole earlier npm artifact has no CLI and cannot reach this parser. if (!doctorStep) { throw new Error("missing openclaw doctor step in update JSON"); } diff --git a/test/scripts/verify-release-notes.test.ts b/test/scripts/verify-release-notes.test.ts index 65aa5f54b9b0..344aeb04684f 100644 --- a/test/scripts/verify-release-notes.test.ts +++ b/test/scripts/verify-release-notes.test.ts @@ -9,6 +9,7 @@ import { cumulativeShippedPullRequests, highlightCountError, releaseNoteReferences, + standardRevertedHash, subtractShippedPullRequests, withoutExcludedContributionRecords, } from "../../.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs"; @@ -32,6 +33,54 @@ function git(cwd: string, args: string[]): string { } describe("release-note verification", () => { + it("ignores nested revert markers in squash-merge bodies", () => { + const nestedRevert = [ + "feat(android): render display math (#101435)", + "", + "* feat(android): render display math", + "", + ' * Revert "docs(changelog): note display math"', + "", + `This reverts commit ${"a".repeat(40)}.`, + ].join("\n"); + const topLevelRevert = [ + 'Revert "fix(qa): keep smoke profile on one channel (#101173)" (#101184)', + "", + `This reverts commit ${"b".repeat(40)}.`, + ].join("\n"); + const squashRevert = [ + "Revert chat session picker inline search (#85527)", + "", + '* Revert "fix(ui): keep chat session search inline (#85490)"', + "", + `This reverts commit ${"c".repeat(40)}.`, + "", + "* fix(ui): clear applied chat picker search on empty input", + ].join("\n"); + const conventionalSquashRevert = [ + "chore: revert dependency guard backfill machinery (#87867)", + "", + '* Revert "ci: isolate dependency guard backfill label (#87882)"', + "", + `This reverts commit ${"d".repeat(40)}.`, + "", + "* ci: preserve clawsweeper bot label filter", + ].join("\n"); + const explainedTopLevelRevert = [ + "revert: restore a provider default", + "", + "The replacement broke non-native endpoints.", + "", + `This reverts commit ${"e".repeat(40)}.`, + ].join("\n"); + + expect(standardRevertedHash(nestedRevert)).toBeUndefined(); + expect(standardRevertedHash(topLevelRevert)).toBe("b".repeat(40)); + expect(standardRevertedHash(squashRevert)).toBe("c".repeat(40)); + expect(standardRevertedHash(conventionalSquashRevert)).toBe("d".repeat(40)); + expect(standardRevertedHash(explainedTopLevelRevert)).toBe("e".repeat(40)); + }); + it("counts only top-level Highlights bullets and enforces the 5-8 policy input", () => { const highlights = [ "### Highlights",