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:
Vincent Koc
2026-08-06 19:22:16 +08:00
committed by GitHub
parent 8c64ca24b1
commit d73eb23b29
9 changed files with 410 additions and 56 deletions
@@ -69,11 +69,17 @@ every human `Thanks @...` attribution.
audit; it contains every referenced PR, eligible contributor credit,
inline issue context, every direct commit, and an editorial-eligibility
classification for PRs and direct commits
- schema version 3 is the required ephemeral manifest contract. Regenerate
older manifests; version 2 is not a supported downstream-reader boundary
- for a historical backfill, add `--seed-ref <pre-backfill-ref>` once so
contribution records from the prior changelog are retained even when an
older merged commit omitted its PR number; the verifier excludes records
for work reverted after the base tag, including beta work reverted before
the stable release
- generated provenance reports in-range PRs separately from retained
seed-only PRs, then states the unique row total. A PR present in both
inventories counts as in-range; never describe the seed-inclusive total
as work merged in the current release range
- add repeatable `--shipped-ref <prior-shipped-tag>` when the reachable main
closeout differs from the shipped tag or later forward-port commits
re-associate PRs that were already released. Each tag is a cumulative
@@ -150,6 +156,8 @@ every human `Thanks @...` attribution.
PR references explicitly present in active commit subjects/bodies. It
preserves author/co-author credit and any issue references in the original
title
- the provenance arithmetic and unique total must match the rendered PR
rows exactly; candidate validation rejects malformed or forged counts
- direct commits remain in the manifest with GitHub-resolved author,
co-author, issue, and editorial-eligibility data. They inform grouped
prose but are never rendered as a public `#### Direct commits` dump. Add
@@ -14,7 +14,9 @@ import path from "node:path";
import { pathToFileURL } from "node:url";
import {
extractChangelogReleaseSections,
formatContributionRecordProvenance,
formatShippedBaselineExclusions,
parseContributionRecordProvenance,
parseShippedBaselineExclusions,
releaseNotesVersionForTag,
verifyGithubReleaseNotes,
@@ -671,10 +673,8 @@ export function contributionRecordTarget(section) {
if (recordStart < 0) {
return undefined;
}
return section.source
.slice(recordStart)
.match(/^This audited record covers the complete \S+\.\.(?<target>[0-9a-f]{40}) history:/mu)
?.groups?.target;
const target = parseContributionRecordProvenance(section.source.slice(recordStart))?.target;
return target && /^[0-9a-f]{40}$/u.test(target) ? target : undefined;
}
export function pullRequestTitleFromCommitSubject(subject, number) {
@@ -688,19 +688,12 @@ function completeContributionRecord(section, label) {
fail(`${label} is missing ### Complete contribution record`);
}
const recordSource = section.source.slice(recordStart);
const provenance = recordSource.match(
/^This audited record covers the complete \S+\.\.[0-9a-f]{40} history: (?<count>[0-9]+) merged PRs?\./mu,
);
if (!provenance?.groups?.count) {
const provenance = parseContributionRecordProvenance(recordSource);
if (!provenance || !/^[0-9a-f]{40}$/u.test(provenance.target)) {
fail(`${label} is missing exact complete contribution record provenance`);
}
const record = contributionRecordFor(section);
const declaredCount = Number(provenance.groups.count);
if (record.pullRequests.size !== declaredCount) {
fail(
`${label} contribution record declares ${declaredCount} PRs but contains ${record.pullRequests.size}`,
);
}
const declaredCount = provenance.uniquePullRequests;
return { record, declaredCount };
}
@@ -2069,6 +2062,10 @@ export function ledgerFor(
!revertedReferences.has(entry.number),
);
const issues = entries.filter((entry) => entry.type === "Issue");
const inRangePullRequestNumbers = new Set([
...sourcePullRequests,
...[...sourceReferences].filter((number) => nodes.get(number)?.__typename === "PullRequest"),
]);
const legacyIssues = legacyIssuesByPullRequest(priorRecord, nodes);
const records = pullRequests.map((entry) => {
const priorEntry = priorRecord.pullRequests.get(entry.number);
@@ -2097,11 +2094,20 @@ export function ledgerFor(
thanks,
});
});
const inRangePullRequests = records.filter((entry) =>
inRangePullRequestNumbers.has(entry.number),
).length;
const retainedSeedOnlyPullRequests = records.length - inRangePullRequests;
const provenance = {
inRangePullRequests,
retainedSeedOnlyPullRequests,
uniquePullRequests: records.length,
};
const shippedBaselineLine = formatShippedBaselineExclusions(shippedBaselines);
const ledger = [
"### Complete contribution record",
"",
`This audited record covers the complete ${base}..${target} history: ${records.length} merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.`,
`${formatContributionRecordProvenance({ base, target, ...provenance })} The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.`,
...(shippedBaselineLine ? ["", shippedBaselineLine] : []),
"",
"#### Pull requests",
@@ -2113,6 +2119,7 @@ export function ledgerFor(
issues,
ledger,
pullRequests: records,
provenance,
titleReferences: titleReferences(records),
};
}
@@ -2176,6 +2183,22 @@ export function ledgerChecks(section, pullRequests, nodes, directCommits, shippe
return errors;
}
const ledger = section.source.slice(ledgerStart);
try {
const provenance = parseContributionRecordProvenance(ledger);
if (!provenance) {
errors.push("missing exact complete contribution record provenance");
} else if (
section.expectedProvenance &&
provenance.inRangePullRequests !== undefined &&
(provenance.inRangePullRequests !== section.expectedProvenance.inRangePullRequests ||
provenance.retainedSeedOnlyPullRequests !==
section.expectedProvenance.retainedSeedOnlyPullRequests)
) {
errors.push("contribution record provenance partition does not match generated inventory");
}
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
}
const expectedShippedBaselineLine = formatShippedBaselineExclusions(shippedBaselines);
try {
const sectionShippedBaselineLine = formatShippedBaselineExclusions(
@@ -2312,7 +2335,7 @@ function manifestFor(options, source, ledger, directCommitRecords) {
}));
const unlinkedCommits = directCommits.filter((commit) => commit.references.length === 0);
return {
schemaVersion: 2,
schemaVersion: 3,
base: options.base,
target: options.target,
mergeBase: source.mergeBase,
@@ -2320,7 +2343,7 @@ function manifestFor(options, source, ledger, directCommitRecords) {
shippedBaselines: source.shippedBaselines,
source: {
references: ledger.entries.length,
pullRequests: ledger.pullRequests.length,
...ledger.provenance,
issues: ledger.issues.length,
directCommits: directCommits.length,
unlinkedCommits: unlinkedCommits.length,
@@ -2589,7 +2612,7 @@ function main() {
}
const errors = ledgerChecks(
candidateSection,
{ ...candidateSection, expectedProvenance: ledger.provenance },
ledger.pullRequests,
nodes,
relationships.directCommits,
@@ -2619,7 +2642,7 @@ function main() {
shippedBaselines: source.shippedBaselines,
source: {
references: references.length,
pullRequests: ledger.pullRequests.length,
...ledger.provenance,
issues: ledger.issues.length,
directCommits: manifest.directCommits.length,
unlinkedCommits: manifest.unlinkedCommits.length,
@@ -2642,7 +2665,7 @@ function main() {
? `, GitHub snapshot ${githubSnapshotState.hits} hits/${githubSnapshotState.misses} misses`
: "";
process.stdout.write(
`${options.version}: ${ledger.pullRequests.length} PRs, ${ledger.issues.length} issues, ${errors.length === 0 ? "verified" : `${errors.length} errors`}${snapshotSummary}\n`,
`${options.version}: ${ledger.provenance.uniquePullRequests} unique PRs (${ledger.provenance.inRangePullRequests} in-range + ${ledger.provenance.retainedSeedOnlyPullRequests} retained seed-only), ${ledger.issues.length} issues, ${errors.length === 0 ? "verified" : `${errors.length} errors`}${snapshotSummary}\n`,
);
}
if (errors.length > 0) {
+7 -31
View File
@@ -30,6 +30,7 @@ import {
extractChangelogReleaseSections,
extractChangelogSection,
formatShippedBaselineExclusions,
parseContributionRecordProvenance,
parseShippedBaselineExclusions,
releaseNotesSectionForTag,
releaseNotesVersionForTag,
@@ -751,36 +752,13 @@ function candidateContributionRecordPullRequests(
Number(match.groups.number),
);
const rows = new Set(rowNumbers);
if (rows.size !== rowNumbers.length) {
const seen = new Set();
const duplicates = rowNumbers.filter((number) => {
if (seen.has(number)) {
return true;
}
seen.add(number);
return false;
});
throw new Error(
`${label} contains duplicate contribution record PR rows: ${[...new Set(duplicates)]
.map((number) => `#${number}`)
.join(", ")}`,
);
}
const provenance = parseContributionRecordProvenance(record);
if (!requireExactProvenance) {
return rows;
}
const provenance = record.match(
/^This audited record covers the complete \S+\.\.[0-9a-f]{40} history: (?<count>[0-9]+) merged PRs?\./mu,
);
if (!provenance?.groups?.count) {
if (!provenance || !/^[0-9a-f]{40}$/u.test(provenance.target)) {
throw new Error(`${label} is missing exact complete contribution record provenance`);
}
const declaredCount = Number(provenance.groups.count);
if (rows.size !== declaredCount) {
throw new Error(
`${label} contribution record declares ${declaredCount} PRs but contains ${rows.size}`,
);
}
return rows;
}
@@ -884,12 +862,10 @@ export function validateCandidateChangelogProvenance({
section,
`CHANGELOG.md ## ${sectionVersion}`,
);
const provenance = record.match(
/^This audited record covers the complete (?<base>\S+)\.\.(?<target>[0-9a-f]{40}) history:/mu,
);
const base = provenance?.groups?.base;
const recordedTarget = provenance?.groups?.target;
if (!base || !recordedTarget) {
const provenance = parseContributionRecordProvenance(record);
const base = provenance?.base;
const recordedTarget = provenance?.target;
if (!base || !recordedTarget || !/^[0-9a-f]{40}$/u.test(recordedTarget)) {
throw new Error(
`CHANGELOG.md ## ${sectionVersion} is missing exact complete contribution record provenance`,
);
+13
View File
@@ -7,6 +7,12 @@ export function extractChangelogSection(changelog: unknown, version: unknown): u
export function releaseNotesVersionForTag(tag: unknown): unknown;
export function formatShippedBaselineExclusions(baselines: ShippedBaselineExclusion[]): string;
export function parseShippedBaselineExclusions(section: string): ShippedBaselineExclusion[];
export function formatContributionRecordProvenance(
provenance: ContributionRecordProvenance,
): string;
export function parseContributionRecordProvenance(
section: string,
): ContributionRecordProvenance | undefined;
export function dedicatedSectionVersionForTag(tag: unknown): unknown;
export function releaseNotesSectionForTag(
changelog: unknown,
@@ -69,3 +75,10 @@ export type ShippedBaselineExclusion = {
count: number;
pullRequests: number[];
};
export type ContributionRecordProvenance = {
base: string;
target: string;
inRangePullRequests?: number;
retainedSeedOnlyPullRequests?: number;
uniquePullRequests: number;
};
+56
View File
@@ -40,6 +40,62 @@ function validateTag(tag) {
}
}
export function formatContributionRecordProvenance(provenance) {
const { base, target, inRangePullRequests, retainedSeedOnlyPullRequests } = provenance;
if (inRangePullRequests === undefined || retainedSeedOnlyPullRequests === undefined) {
fail("canonical contribution record provenance requires split PR counts");
}
const uniquePullRequests = inRangePullRequests + retainedSeedOnlyPullRequests;
const count = (value) => value.toLocaleString("en-US");
const prs = (value) => `PR${value === 1 ? "" : "s"}`;
return `This audited record covers the complete ${base}..${target} history: ${count(inRangePullRequests)} in-range ${prs(inRangePullRequests)} + ${count(retainedSeedOnlyPullRequests)} retained seed-only ${prs(retainedSeedOnlyPullRequests)} = ${count(uniquePullRequests)} unique ${prs(uniquePullRequests)}.`;
}
export function parseContributionRecordProvenance(section) {
const rows = Array.from(section.matchAll(/^- \*\*PR #(\d+)\*\*/gmu), (match) => Number(match[1]));
const duplicate = rows.find((value, index) => rows.indexOf(value) !== index);
if (duplicate !== undefined) {
fail(`duplicate contribution record PR #${duplicate}`);
}
const line = section.match(/^This audited record covers the complete .+$/mu)?.[0];
if (!line) {
return undefined;
}
const canonical = line.match(
/^This audited record covers the complete (?<base>\S+)\.\.(?<target>[0-9a-f]{40}) history: (?<inRange>0|[1-9][0-9]{0,2}(?:,[0-9]{3})*) in-range PRs? \+ (?<seedOnly>0|[1-9][0-9]{0,2}(?:,[0-9]{3})*) retained seed-only PRs? = (?<unique>0|[1-9][0-9]{0,2}(?:,[0-9]{3})*) unique PRs?\./u,
);
const legacy =
canonical ??
line.match(
/^This audited record covers the complete (?<base>\S+)\.\.(?<target>\S+) history: (?<unique>[0-9]+) merged PRs?\./u,
);
if (!legacy?.groups) {
fail("release contribution record provenance is malformed");
}
const number = (value) => Number(value.replaceAll(",", ""));
const provenance = {
base: legacy.groups.base,
target: legacy.groups.target,
uniquePullRequests: number(legacy.groups.unique),
};
if (canonical?.groups) {
provenance.inRangePullRequests = number(canonical.groups.inRange);
provenance.retainedSeedOnlyPullRequests = number(canonical.groups.seedOnly);
if (
provenance.inRangePullRequests + provenance.retainedSeedOnlyPullRequests !==
provenance.uniquePullRequests
) {
fail("release contribution record provenance arithmetic is invalid");
}
}
if (provenance.uniquePullRequests > 0 && !/^#### Pull requests\r?$/mu.test(section)) {
fail("positive contribution record requires a Pull requests section");
}
if (rows.length !== provenance.uniquePullRequests) {
fail(`contribution record row count ${rows.length} != ${provenance.uniquePullRequests}`);
}
return provenance;
}
function githubReleaseBodySize(body) {
return {
characters: [...body].length,
@@ -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",
"",
+173 -1
View File
@@ -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."),
+14 -2
View File
@@ -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 });