Files
openclaw/test/scripts/render-github-release-notes.test.ts
Peter Steinberger eefe2e8837 fix(release): let alpha tags publish from their exact changelog heading
Shipped alpha tags carry a dedicated ## X-alpha.N heading with no base
section (see v2026.6.20-alpha.1's tagged CHANGELOG.md); the renderer and
candidate provenance now prefer that dedicated heading for alpha and
correction tags while beta/stable remain pinned to the stable base
section per #103222.
2026-07-10 02:06:53 -07:00

398 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
GITHUB_RELEASE_BODY_MAX_BYTES,
GITHUB_RELEASE_BODY_MAX_CHARACTERS,
extractChangelogSection,
formatShippedBaselineExclusions,
parseShippedBaselineExclusions,
releaseNotesVersionForTag,
renderGithubReleaseNotes,
verifyGithubReleaseNotes,
} from "../../scripts/render-github-release-notes.mjs";
const repository = "openclaw/openclaw";
const tag = "v2026.7.1-beta.3";
const version = "2026.7.1";
function changelogFor(record: string): string {
return [
"# Changelog",
"",
`## ${version}`,
"",
"### Highlights",
"",
"- A grouped user-facing highlight.",
"",
"### Fixes",
"",
"- A grouped user-facing fix.",
"",
"### Complete contribution record",
"",
record,
"",
"## 2026.6.11",
"",
"- Previous release.",
"",
].join("\n");
}
describe("GitHub release-note rendering", () => {
it("emits the complete matching section including its version heading when it fits", () => {
const rendered = renderGithubReleaseNotes({
changelog: changelogFor("- **PR #123** fix: example. Thanks @contributor."),
version,
tag,
repository,
});
expect(rendered.mode).toBe("full");
expect(rendered.body).toBe(
[
`## ${version}`,
"",
"### Highlights",
"",
"- A grouped user-facing highlight.",
"",
"### Fixes",
"",
"- A grouped user-facing fix.",
"",
"### Complete contribution record",
"",
"- **PR #123** fix: example. Thanks @contributor.",
].join("\n"),
);
});
it("replaces an oversized contribution record with a tag-pinned link", () => {
const oversizedRecord = `- **PR #123** ${"record-only-detail ".repeat(9_000)}`;
const rendered = renderGithubReleaseNotes({
changelog: changelogFor(oversizedRecord),
version,
tag,
repository,
});
expect(rendered.mode).toBe("compact");
expect(rendered.body).toContain(`## ${version}\n\n### Highlights`);
expect(rendered.body).toContain("- A grouped user-facing fix.");
expect(rendered.body).toContain("### Complete contribution record");
expect(rendered.body).toContain(
"https://github.com/openclaw/openclaw/blob/v2026.7.1-beta.3/CHANGELOG.md#complete-contribution-record",
);
expect(rendered.body).not.toContain("record-only-detail");
expect(rendered.size.characters).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_CHARACTERS);
expect(rendered.size.bytes).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_BYTES);
});
it("keeps a fitting full section and omits only a proof tail that would overflow", () => {
const nearlyFullRecord = `- **PR #123** ${"x".repeat(124_500)}`;
const changelog = changelogFor(nearlyFullRecord);
const withoutProof = renderGithubReleaseNotes({
changelog,
version,
tag,
repository,
});
const withProof = renderGithubReleaseNotes({
changelog,
version,
tag,
repository,
verification: `### Release verification\n\n- proof: ${"y".repeat(1_000)}`,
});
expect(withoutProof.mode).toBe("full");
expect(withProof.mode).toBe("full");
expect(withProof.verificationIncluded).toBe(false);
expect(withProof.verificationOmitted).toBe(true);
expect(withProof.body).toBe(withoutProof.body);
expect(withProof.body).not.toContain("### Release verification");
expect(withProof.size.characters).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_CHARACTERS);
expect(withProof.size.bytes).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_BYTES);
});
it("uses the full form at exactly 125,000 bytes and compacts at 125,001", () => {
const seed = renderGithubReleaseNotes({
changelog: changelogFor("x"),
version,
tag,
repository,
});
const exactRecordLength =
GITHUB_RELEASE_BODY_MAX_BYTES - seed.size.bytes + Buffer.byteLength("x");
const exact = renderGithubReleaseNotes({
changelog: changelogFor("x".repeat(exactRecordLength)),
version,
tag,
repository,
});
const over = renderGithubReleaseNotes({
changelog: changelogFor("x".repeat(exactRecordLength + 1)),
version,
tag,
repository,
});
expect(exact.mode).toBe("full");
expect(exact.size).toEqual({
characters: GITHUB_RELEASE_BODY_MAX_CHARACTERS,
bytes: GITHUB_RELEASE_BODY_MAX_BYTES,
});
expect(over.mode).toBe("compact");
});
it("compacts when multibyte text exceeds the byte limit before the character limit", () => {
const rendered = renderGithubReleaseNotes({
changelog: changelogFor("é".repeat(63_000)),
version,
tag,
repository,
});
expect(rendered.mode).toBe("compact");
expect(rendered.size.bytes).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_BYTES);
expect(rendered.size.characters).toBeLessThanOrEqual(GITHUB_RELEASE_BODY_MAX_CHARACTERS);
});
it("normalizes correction tags to the stable changelog section", () => {
expect(releaseNotesVersionForTag("v2026.7.1-2")).toBe("2026.7.1");
const rendered = renderGithubReleaseNotes({
changelog: changelogFor("- **PR #123** fix: correction."),
version,
tag: "v2026.7.1-2",
repository,
});
expect(rendered.body).toContain("## 2026.7.1");
});
it("prefers a correction tag's dedicated changelog section when one exists", () => {
const changelog = [
"# Changelog",
"",
"## 2026.7.1-2",
"",
"- Correction-only fix.",
"",
`## ${version}`,
"",
"- Stable release notes.",
"",
].join("\n");
const rendered = renderGithubReleaseNotes({
changelog,
version,
tag: "v2026.7.1-2",
repository,
});
expect(rendered.body).toContain("## 2026.7.1-2");
expect(rendered.body).toContain("Correction-only fix.");
expect(rendered.body).not.toContain("Stable release notes.");
});
it("round-trips canonical shipped baseline exclusions and rejects malformed metadata", () => {
const line = formatShippedBaselineExclusions([
{ ref: "v2026.6.11", count: 2, pullRequests: [108, 101] },
{ ref: "v2026.6.10-beta.2", count: 0, pullRequests: [] },
]);
expect(line).toBe(
"Shipped baseline exclusions: v2026.6.10-beta.2 (0 PRs); v2026.6.11 (2 PRs: #101, #108).",
);
expect(parseShippedBaselineExclusions(line)).toEqual([
{ ref: "v2026.6.10-beta.2", count: 0, pullRequests: [] },
{ ref: "v2026.6.11", count: 2, pullRequests: [101, 108] },
]);
expect(() =>
parseShippedBaselineExclusions("Shipped baseline exclusion: v2026.6.11 (8 PRs)."),
).toThrow("malformed shipped baseline exclusion");
expect(() =>
parseShippedBaselineExclusions("Shipped baseline exclusions: v2026.6.11 (2 PRs: #101)."),
).toThrow("invalid shipped baseline exclusion count");
});
it("rejects tag/version drift and legacy oversized ledger anchors", () => {
expect(() =>
renderGithubReleaseNotes({
changelog: changelogFor("- **PR #123** fix: example."),
version,
tag: "v2026.7.2-beta.1",
repository,
}),
).toThrow("requires CHANGELOG.md version 2026.7.2");
expect(() =>
renderGithubReleaseNotes({
changelog: changelogFor(
`### Complete contribution ledger\n\n${"legacy ".repeat(20_000)}`,
).replace("### Complete contribution record\n\n", ""),
version,
tag,
repository,
}),
).toThrow("cannot be compacted without a complete contribution record");
});
it("prefers an alpha tag's exact changelog heading over the Unreleased fallback", () => {
// Shipped alpha tags carry their own heading with no base section,
// matching the tagged CHANGELOG.md shape of v2026.6.20-alpha.1.
const changelog = [
"# Changelog",
"",
"## 2026.7.1-alpha.2",
"",
"- Alpha-only fix.",
"",
"## 2026.6.11",
"",
"- Previous release.",
"",
].join("\n");
const rendered = renderGithubReleaseNotes({
changelog,
version,
tag: "v2026.7.1-alpha.2",
repository,
});
expect(rendered.body).toContain("## 2026.7.1-alpha.2");
expect(rendered.body).toContain("Alpha-only fix.");
expect(rendered.body).not.toContain("Previous release.");
});
it("permits the Unreleased fallback only for alpha tags", () => {
const changelog = changelogFor("- **PR #123** fix: example.").replace(
"## 2026.7.1",
"## Unreleased",
);
const rendered = renderGithubReleaseNotes({
changelog,
version,
tag: "v2026.7.1-alpha.1",
repository,
});
expect(rendered.body).toContain("## 2026.7.1");
expect(rendered.body).not.toContain("## Unreleased");
expect(() =>
renderGithubReleaseNotes({
changelog,
version,
tag,
repository,
}),
).toThrow("CHANGELOG.md does not contain ## 2026.7.1");
expect(() =>
renderGithubReleaseNotes({
changelog,
version: "foo",
tag: "vfoo-alpha.1",
repository,
}),
).toThrow("invalid release tag");
});
it("ignores fenced pseudo-headings and handles a release heading at EOF", () => {
const fenced = [
`## ${version}`,
"",
"```md",
"## 2099.1.1",
"```",
"",
"### Fixes",
"",
"- Still in the current release.",
"",
"## 2026.6.11",
].join("\n");
expect(extractChangelogSection(fenced, version)).toContain("- Still in the current release.");
expect(extractChangelogSection(`## ${version}`, version)).toBe(`## ${version}`);
});
it("compacts at the real contribution record instead of a fenced pseudo-heading", () => {
const changelog = changelogFor(`- **PR #123** ${"record-only-detail ".repeat(9_000)}`).replace(
"### Fixes",
["```md", "### Complete contribution record", "```", "", "### Fixes"].join("\n"),
);
const rendered = renderGithubReleaseNotes({ changelog, version, tag, repository });
expect(rendered.mode).toBe("compact");
expect(rendered.body).toContain("```md\n### Complete contribution record\n```");
expect(rendered.body).toContain("- A grouped user-facing fix.");
expect(rendered.body).not.toContain("record-only-detail");
});
it("verifies the exact generated compact body and optional proof tail", () => {
const changelog = changelogFor(`- **PR #123** ${"z".repeat(130_000)}`);
const verification = "### Release verification\n\n- release SHA: `abc123`";
const rendered = renderGithubReleaseNotes({
changelog,
version,
tag,
repository,
verification,
});
expect(
verifyGithubReleaseNotes({
body: rendered.body,
changelog,
version,
tag,
repository,
}),
).toMatchObject({ matches: true, mode: "compact" });
expect(
verifyGithubReleaseNotes({
body: rendered.body.replace("tag-pinned", "mutable"),
changelog,
version,
tag,
repository,
}).matches,
).toBe(false);
expect(
verifyGithubReleaseNotes({
body: rendered.body,
changelog,
version,
tag: "v2026.7.1-beta.2",
repository,
}).matches,
).toBe(false);
});
it("does not treat fenced verification headings as appended proof", () => {
const changelog = changelogFor(
[
"```md",
"### Release verification",
"",
"- Example only.",
"```",
"",
"- **PR #123** fix: example.",
].join("\n"),
);
const rendered = renderGithubReleaseNotes({ changelog, version, tag, repository });
expect(
verifyGithubReleaseNotes({
body: rendered.body,
changelog,
version,
tag,
repository,
}),
).toMatchObject({ matches: true, verificationIncluded: false });
});
});