mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 01:51:42 +00:00
fix(release): validate GitHub notes before publish (#103222)
This commit is contained in:
committed by
GitHub
parent
0d2db46956
commit
31f76b9792
182
test/scripts/prepare-github-release-notes.test.ts
Normal file
182
test/scripts/prepare-github-release-notes.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
appendGitHubReleaseVerification,
|
||||
GITHUB_RELEASE_VERIFICATION_RESERVE_CHARACTERS,
|
||||
GITHUB_RELEASE_VERIFICATION_RESERVE_UTF8_BYTES,
|
||||
MAX_GITHUB_RELEASE_NOTES_CHARACTERS,
|
||||
MAX_GITHUB_RELEASE_NOTES_UTF8_BYTES,
|
||||
MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS,
|
||||
MAX_GITHUB_RELEASE_SOURCE_NOTES_UTF8_BYTES,
|
||||
prepareGitHubReleaseNotes,
|
||||
} from "../../scripts/prepare-github-release-notes.mjs";
|
||||
|
||||
describe("prepareGitHubReleaseNotes", () => {
|
||||
it("preserves the stable-base heading for prerelease notes", () => {
|
||||
const changelog = [
|
||||
"## Unreleased",
|
||||
"",
|
||||
"Future work.",
|
||||
"",
|
||||
"## 2026.7.1",
|
||||
"",
|
||||
"Release work.",
|
||||
"",
|
||||
"## 2026.6.11",
|
||||
"",
|
||||
"Older work.",
|
||||
].join("\n");
|
||||
|
||||
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toBe(
|
||||
"## 2026.7.1\n\nRelease work.\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses an exact alpha heading before the Unreleased fallback", () => {
|
||||
const changelog = [
|
||||
"## Unreleased",
|
||||
"",
|
||||
"Future nightly work.",
|
||||
"",
|
||||
"## 2026.7.2-alpha.3",
|
||||
"",
|
||||
"Nightly release work.",
|
||||
"",
|
||||
"## 2026.7.1",
|
||||
"",
|
||||
"Stable work.",
|
||||
].join("\n");
|
||||
|
||||
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.2-alpha.3")).toBe(
|
||||
"## 2026.7.2-alpha.3\n\nNightly release work.\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the Unreleased fallback for alpha branches without an exact heading", () => {
|
||||
const changelog = "## Unreleased\n\nNightly release work.\n\n## 2026.7.1\n\nStable work.\n";
|
||||
|
||||
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.2-alpha.3")).toBe(
|
||||
"## Unreleased\n\nNightly release work.\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers an exact numeric correction heading", () => {
|
||||
const changelog = [
|
||||
"## 2026.7.1-2",
|
||||
"",
|
||||
"Correction-specific work.",
|
||||
"",
|
||||
"## 2026.7.1",
|
||||
"",
|
||||
"Base release work.",
|
||||
].join("\n");
|
||||
|
||||
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-2")).toBe(
|
||||
"## 2026.7.1-2\n\nCorrection-specific work.\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the base heading for numeric correction tags", () => {
|
||||
const changelog = "## 2026.7.1\n\nBase release work.\n\n## 2026.6.11\n\nOlder work.\n";
|
||||
|
||||
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-2")).toBe(
|
||||
"## 2026.7.1\n\nBase release work.\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("requires the stable-base section for beta and stable releases", () => {
|
||||
const changelog = "## Unreleased\n\nCandidate work.\n\n## 2026.6.11\n\nOlder work.\n";
|
||||
|
||||
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
|
||||
"does not contain release notes for 2026.7.1",
|
||||
);
|
||||
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1")).toThrow(
|
||||
"does not contain release notes for 2026.7.1",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a heading without release-note content", () => {
|
||||
const changelog = "## 2026.7.1\n\n## 2026.6.11\n\nOlder work.\n";
|
||||
|
||||
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
|
||||
"does not contain release-note content",
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts multibyte source sections below the character and byte budgets", () => {
|
||||
const changelog = `## 2026.7.1\n\n${"é".repeat(59_000)}\n`;
|
||||
|
||||
expect(prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toHaveLength(59_014);
|
||||
});
|
||||
|
||||
it("rejects multibyte source sections that exceed the UTF-8 byte budget", () => {
|
||||
const changelog = `## 2026.7.1\n\n${"é".repeat(70_000)}\n`;
|
||||
|
||||
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
|
||||
`complete source section exceeds the ${MAX_GITHUB_RELEASE_SOURCE_NOTES_UTF8_BYTES}-byte source safety budget`,
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a source body exactly at the proof-reserving limit", () => {
|
||||
const prefix = "## 2026.7.1\n\n";
|
||||
const content = "x".repeat(MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS - prefix.length - 1);
|
||||
|
||||
expect(prepareGitHubReleaseNotes(`${prefix}${content}\n`, "v2026.7.1-beta.3")).toHaveLength(
|
||||
MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects source sections that consume the required proof reserve", () => {
|
||||
const prefix = "## 2026.7.1\n\n";
|
||||
const content = "x".repeat(MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS - prefix.length);
|
||||
const changelog = `${prefix}${content}\n`;
|
||||
|
||||
expect(() => prepareGitHubReleaseNotes(changelog, "v2026.7.1-beta.3")).toThrow(
|
||||
"complete source section exceeds the 120000-character source budget",
|
||||
);
|
||||
});
|
||||
|
||||
it("appends and replaces canonical release verification within the reserve", () => {
|
||||
const notes = "## 2026.7.1\n\nRelease work.\n\n### Release verification\n\n- stale proof\n";
|
||||
const proof = "### Release verification\n\n- current proof";
|
||||
|
||||
expect(appendGitHubReleaseVerification(notes, proof)).toBe(
|
||||
"## 2026.7.1\n\nRelease work.\n\n### Release verification\n\n- current proof\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects proof that exceeds its reserved capacity", () => {
|
||||
const proof = `### Release verification\n\n${"x".repeat(
|
||||
GITHUB_RELEASE_VERIFICATION_RESERVE_CHARACTERS,
|
||||
)}`;
|
||||
|
||||
expect(() => appendGitHubReleaseVerification("## 2026.7.1\n\nRelease work.\n", proof)).toThrow(
|
||||
"exceeds the reserved 5000-character budget",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects multibyte proof that exceeds its reserved byte capacity", () => {
|
||||
const proof = `### Release verification\n\n${"é".repeat(
|
||||
GITHUB_RELEASE_VERIFICATION_RESERVE_UTF8_BYTES / 2,
|
||||
)}`;
|
||||
|
||||
expect(() => appendGitHubReleaseVerification("## 2026.7.1\n\nRelease work.\n", proof)).toThrow(
|
||||
"exceeds the reserved 5000-byte safety budget",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the combined body under GitHub's hard limit", () => {
|
||||
const prefix = "## 2026.7.1\n\n";
|
||||
const content = "x".repeat(MAX_GITHUB_RELEASE_SOURCE_NOTES_CHARACTERS - prefix.length - 1);
|
||||
const notes = prepareGitHubReleaseNotes(`${prefix}${content}\n`, "v2026.7.1");
|
||||
const proof = `### Release verification\n\n${"x".repeat(
|
||||
GITHUB_RELEASE_VERIFICATION_RESERVE_CHARACTERS - 40,
|
||||
)}`;
|
||||
|
||||
expect(appendGitHubReleaseVerification(notes, proof).length).toBeLessThanOrEqual(
|
||||
MAX_GITHUB_RELEASE_NOTES_CHARACTERS,
|
||||
);
|
||||
expect(
|
||||
Buffer.byteLength(appendGitHubReleaseVerification(notes, proof), "utf8"),
|
||||
).toBeLessThanOrEqual(MAX_GITHUB_RELEASE_NOTES_UTF8_BYTES);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user