mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(release): report seeded changelog provenance truthfully (#119897)
* fix(release): report seeded changelog provenance truthfully * fix(release): reject legacy provenance formatting * fix(release): enforce complete provenance records
This commit is contained in:
@@ -358,7 +358,38 @@ describe("release candidate checklist", () => {
|
||||
targetSha,
|
||||
isAncestor: () => true,
|
||||
}),
|
||||
).toThrow("duplicate contribution record PR rows: #123");
|
||||
).toThrow("duplicate contribution record PR #123");
|
||||
});
|
||||
|
||||
it("rejects canonical provenance whose unique total does not match the PR rows", () => {
|
||||
const targetSha = "b".repeat(40);
|
||||
const changelog = [
|
||||
"# Changelog",
|
||||
"",
|
||||
"## 2026.7.1",
|
||||
"",
|
||||
"### Highlights",
|
||||
"",
|
||||
"- User-facing notes.",
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
`This audited record covers the complete base..${targetSha} history: 1 in-range PR + 1 retained seed-only PR = 2 unique PRs.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
"- **PR #123** fix: example.",
|
||||
].join("\n");
|
||||
|
||||
expect(() =>
|
||||
validateCandidateChangelogProvenance({
|
||||
changelog,
|
||||
version: "2026.7.1",
|
||||
tag: "v2026.7.1-beta.3",
|
||||
targetSha,
|
||||
isAncestor: () => true,
|
||||
}),
|
||||
).toThrow("contribution record row count 1 != 2");
|
||||
});
|
||||
|
||||
it("uses numbered historical record rows and skips Unreleased baseline rows", () => {
|
||||
@@ -379,7 +410,7 @@ describe("release candidate checklist", () => {
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
"This audited record covers the complete base..HEAD history: 0 merged PRs.",
|
||||
"This audited record covers the complete base..HEAD history: 1 merged PR.",
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
|
||||
@@ -6,6 +6,52 @@ import {
|
||||
renderContributionRecordEntry,
|
||||
} from "../../.agents/skills/openclaw-changelog-update/scripts/verify-release-notes.mjs";
|
||||
|
||||
const targetSha = "a".repeat(40);
|
||||
|
||||
function contributionLedger({
|
||||
nodes,
|
||||
seededPullRequests = [],
|
||||
sourcePullRequests = [],
|
||||
sourceReferences = [],
|
||||
}: {
|
||||
nodes: Map<number, Record<string, unknown>>;
|
||||
seededPullRequests?: number[];
|
||||
sourcePullRequests?: number[];
|
||||
sourceReferences?: number[];
|
||||
}) {
|
||||
return ledgerFor(
|
||||
"v2026.7.2-beta.7",
|
||||
targetSha,
|
||||
[...nodes.keys()],
|
||||
nodes,
|
||||
new Map(),
|
||||
new Map(),
|
||||
{ issuesByPullRequest: new Map() },
|
||||
{
|
||||
legacyIssues: new Map(),
|
||||
pullRequests: new Map(
|
||||
seededPullRequests.map((number) => [
|
||||
number,
|
||||
{ externalReferences: [], references: [], thanks: [] },
|
||||
]),
|
||||
),
|
||||
},
|
||||
new Set(sourcePullRequests),
|
||||
sourceReferences,
|
||||
[],
|
||||
[],
|
||||
new Set(),
|
||||
[],
|
||||
Date.parse("2026-08-05T00:00:00Z"),
|
||||
) as ReturnType<typeof ledgerFor> & {
|
||||
provenance: {
|
||||
inRangePullRequests: number;
|
||||
retainedSeedOnlyPullRequests: number;
|
||||
uniquePullRequests: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
describe("renderContributionRecordEntry", () => {
|
||||
it("keeps external and linked issue references without repeating PR title references", () => {
|
||||
expect(
|
||||
@@ -101,7 +147,7 @@ describe("renderContributionRecordEntry", () => {
|
||||
|
||||
const result = ledgerFor(
|
||||
"v2026.6.11",
|
||||
"HEAD",
|
||||
targetSha,
|
||||
[125],
|
||||
nodes,
|
||||
new Map(),
|
||||
@@ -120,6 +166,126 @@ describe("renderContributionRecordEntry", () => {
|
||||
expect(result.ledger).toContain("- **PR #125** Thanks @carol and @alice and @bob.");
|
||||
});
|
||||
|
||||
it("counts associated and PR-typed source refs before retained seed-only rows", () => {
|
||||
const nodes = new Map(
|
||||
[1, 2, 3].map((number) => [
|
||||
number,
|
||||
{
|
||||
__typename: "PullRequest",
|
||||
closingIssuesReferences: { nodes: [] },
|
||||
mergedAt: "2026-08-04T00:00:00Z",
|
||||
title: `fix: contribution ${number}`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = contributionLedger({
|
||||
nodes,
|
||||
seededPullRequests: [1, 3],
|
||||
sourcePullRequests: [1],
|
||||
sourceReferences: [2],
|
||||
});
|
||||
|
||||
expect(result.provenance).toEqual({
|
||||
inRangePullRequests: 2,
|
||||
retainedSeedOnlyPullRequests: 1,
|
||||
uniquePullRequests: 3,
|
||||
});
|
||||
expect(result.ledger).toContain("2 in-range PRs + 1 retained seed-only PR = 3 unique PRs.");
|
||||
});
|
||||
|
||||
it("reports zero retained seed-only PRs when every row is in range", () => {
|
||||
const nodes = new Map([
|
||||
[
|
||||
1,
|
||||
{
|
||||
__typename: "PullRequest",
|
||||
closingIssuesReferences: { nodes: [] },
|
||||
mergedAt: "2026-08-04T00:00:00Z",
|
||||
title: "fix: in-range contribution",
|
||||
},
|
||||
],
|
||||
]);
|
||||
const result = contributionLedger({ nodes, sourcePullRequests: [1] });
|
||||
|
||||
expect(result.provenance).toMatchObject({
|
||||
inRangePullRequests: 1,
|
||||
retainedSeedOnlyPullRequests: 0,
|
||||
uniquePullRequests: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports all rows as retained seed-only when the release range has no PRs", () => {
|
||||
const nodes = new Map(
|
||||
[1, 2].map((number) => [
|
||||
number,
|
||||
{
|
||||
__typename: "PullRequest",
|
||||
closingIssuesReferences: { nodes: [] },
|
||||
mergedAt: "2026-08-04T00:00:00Z",
|
||||
title: `fix: seeded contribution ${number}`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = contributionLedger({ nodes, seededPullRequests: [1, 2] });
|
||||
|
||||
expect(result.provenance).toMatchObject({
|
||||
inRangePullRequests: 0,
|
||||
retainedSeedOnlyPullRequests: 2,
|
||||
uniquePullRequests: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a forged canonical range and seed partition", () => {
|
||||
const source = [
|
||||
"## 2026.7.1",
|
||||
"",
|
||||
"### Highlights",
|
||||
"",
|
||||
"- Highlight one.",
|
||||
"- Highlight two.",
|
||||
"- Highlight three.",
|
||||
"- Highlight four.",
|
||||
"- Highlight five.",
|
||||
"",
|
||||
"### Changes",
|
||||
"",
|
||||
"### Fixes",
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
`This audited record covers the complete base..${targetSha} history: 0 in-range PRs + 1 retained seed-only PR = 1 unique PR.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
"- **PR #456**",
|
||||
].join("\n");
|
||||
const entry = {
|
||||
number: 456,
|
||||
title: "fix: example",
|
||||
editorialEligible: true,
|
||||
priorReferences: [],
|
||||
externalReferences: [],
|
||||
linkedIssues: [],
|
||||
thanks: [],
|
||||
};
|
||||
|
||||
expect(
|
||||
ledgerChecks(
|
||||
{
|
||||
source,
|
||||
expectedProvenance: {
|
||||
inRangePullRequests: 1,
|
||||
retainedSeedOnlyPullRequests: 0,
|
||||
uniquePullRequests: 1,
|
||||
},
|
||||
},
|
||||
[entry],
|
||||
new Map([[456, { __typename: "PullRequest" }]]),
|
||||
[],
|
||||
),
|
||||
).toContain("contribution record provenance partition does not match generated inventory");
|
||||
});
|
||||
|
||||
it("retains references from a verbose record when the source title changes", () => {
|
||||
const record = contributionRecordFor({
|
||||
source: [
|
||||
@@ -159,6 +325,8 @@ describe("renderContributionRecordEntry", () => {
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
"- **PR #456** Related openclaw/imsg#141.",
|
||||
@@ -200,6 +368,8 @@ describe("renderContributionRecordEntry", () => {
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
line,
|
||||
@@ -239,6 +409,8 @@ describe("renderContributionRecordEntry", () => {
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
`This audited record covers the complete base..${targetSha} history: 1 merged PR.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
line,
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
GITHUB_RELEASE_BODY_MAX_BYTES,
|
||||
GITHUB_RELEASE_BODY_MAX_CHARACTERS,
|
||||
extractChangelogSection,
|
||||
formatContributionRecordProvenance,
|
||||
formatShippedBaselineExclusions,
|
||||
parseContributionRecordProvenance,
|
||||
parseShippedBaselineExclusions,
|
||||
releaseNotesVersionForTag,
|
||||
renderGithubReleaseNotes,
|
||||
@@ -40,6 +42,67 @@ function changelogFor(record: string): string {
|
||||
}
|
||||
|
||||
describe("GitHub release-note rendering", () => {
|
||||
it("round-trips canonical contribution provenance and accepts published legacy lines", () => {
|
||||
const target = "a".repeat(40);
|
||||
const singular = formatContributionRecordProvenance({
|
||||
base: "v2026.7.2-beta.7",
|
||||
target,
|
||||
inRangePullRequests: 1,
|
||||
retainedSeedOnlyPullRequests: 0,
|
||||
uniquePullRequests: 1,
|
||||
});
|
||||
const commaSeparated = formatContributionRecordProvenance({
|
||||
base: "v2026.7.2-beta.7",
|
||||
target,
|
||||
inRangePullRequests: 1_234,
|
||||
retainedSeedOnlyPullRequests: 56,
|
||||
uniquePullRequests: 1_290,
|
||||
});
|
||||
|
||||
expect(singular).toContain("1 in-range PR + 0 retained seed-only PRs = 1 unique PR.");
|
||||
expect(commaSeparated).toContain(
|
||||
"1,234 in-range PRs + 56 retained seed-only PRs = 1,290 unique PRs.",
|
||||
);
|
||||
expect(
|
||||
parseContributionRecordProvenance(
|
||||
[singular, "", "#### Pull requests", "", "- **PR #123** fix: canonical example."].join(
|
||||
"\n",
|
||||
),
|
||||
),
|
||||
).toEqual({
|
||||
base: "v2026.7.2-beta.7",
|
||||
target,
|
||||
inRangePullRequests: 1,
|
||||
retainedSeedOnlyPullRequests: 0,
|
||||
uniquePullRequests: 1,
|
||||
});
|
||||
const legacy = parseContributionRecordProvenance(
|
||||
[
|
||||
`This audited record covers the complete v2026.7.2-beta.6..02d06caeb0febe7ec3c0df1454b85c38f3fb27d1 history: 1 merged PR. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
"- **PR #123** fix: legacy example.",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(legacy).toMatchObject({ uniquePullRequests: 1 });
|
||||
expect(() => formatContributionRecordProvenance(legacy!)).toThrow("requires split PR counts");
|
||||
expect(() =>
|
||||
parseContributionRecordProvenance(
|
||||
commaSeparated.replace("= 1,290 unique PRs", "= 1,291 unique PRs"),
|
||||
),
|
||||
).toThrow("provenance arithmetic is invalid");
|
||||
expect(() =>
|
||||
parseContributionRecordProvenance(commaSeparated.replace("1,234", "1234")),
|
||||
).toThrow("provenance is malformed");
|
||||
expect(() =>
|
||||
parseContributionRecordProvenance(singular.replace("1 in-range", "001 in-range")),
|
||||
).toThrow("provenance is malformed");
|
||||
expect(() => parseContributionRecordProvenance(singular)).toThrow(
|
||||
"positive contribution record requires a Pull requests section",
|
||||
);
|
||||
});
|
||||
|
||||
it("emits the complete matching section including its version heading when it fits", () => {
|
||||
const rendered = renderGithubReleaseNotes({
|
||||
changelog: changelogFor("- **PR #123** fix: example. Thanks @contributor."),
|
||||
|
||||
@@ -74,7 +74,11 @@ describe("release-note verification", () => {
|
||||
"",
|
||||
"### Complete contribution record",
|
||||
"",
|
||||
`This audited record covers the complete base..${target} history: 1 merged PR.`,
|
||||
`This audited record covers the complete base..${target} history: 1 in-range PR + 0 retained seed-only PRs = 1 unique PR.`,
|
||||
"",
|
||||
"#### Pull requests",
|
||||
"",
|
||||
"- **PR #123** fix: example.",
|
||||
].join("\n"),
|
||||
}),
|
||||
).toBe(target);
|
||||
@@ -948,7 +952,15 @@ describe("release-note verification", () => {
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("1 errors");
|
||||
expect(JSON.parse(readFileSync(manifestPath, "utf8")).version).toBe("2026.7.1");
|
||||
expect(JSON.parse(readFileSync(manifestPath, "utf8"))).toMatchObject({
|
||||
schemaVersion: 3,
|
||||
version: "2026.7.1",
|
||||
source: {
|
||||
inRangePullRequests: 0,
|
||||
retainedSeedOnlyPullRequests: 0,
|
||||
uniquePullRequests: 0,
|
||||
},
|
||||
});
|
||||
expect(readFileSync(join(cwd, "CHANGELOG.md"), "utf8")).toBe(changelog);
|
||||
} finally {
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user