ci(release): source release notes from changesets (#11195)

This commit is contained in:
Taesu
2026-09-07 09:25:53 +00:00
committed by GitHub
parent 597d39cb4a
commit b112dbd075
2 changed files with 68 additions and 17 deletions
@@ -502,8 +502,8 @@ function loadPreviousPrereleaseChangesets(version: string): Set<string> {
}
/**
* Collects release entries using git history as the ground truth,
* enriched with changeset descriptions where available.
* Collects release entries from consumed changesets and enriches them with
* pull request metadata from git history.
*
* Handles the cherry-pick history gap (where the previous tag is not
* a direct ancestor) using PR-number deduplication.
@@ -650,18 +650,13 @@ export async function collectEntries(
if (seenPRs.has(prNumber)) continue;
// A PR with a changeset should appear even if its type is docs:/chore:/etc.
// A consumed changeset is the release intent. Git history only provides
// metadata for that entry.
const subject = parsed.subject.replace(/\s*\(#\d+\)$/, "").trim();
const descMatch = changesetByDesc.get(subject.toLowerCase());
const changeset = changesetByPR.get(prNumber) ?? descMatch;
if (descMatch) consumedOrphans.add(descMatch);
if (
!changeset &&
["chore", "docs", "ci", "test", "style", "build"].includes(parsed.type)
) {
continue;
}
if (!changeset) continue;
seenPRs.add(prNumber);
@@ -671,13 +666,13 @@ export async function collectEntries(
let packageName: string;
let breaking = parsed.breaking;
const changesetDescription = changeset?.description ?? null;
if (changeset?.breaking) breaking = true;
const changesetDescription = changeset.description;
if (changeset.breaking) breaking = true;
title = subject;
domain = resolveDomain(parsed.scope || undefined, []);
packageName =
changeset?.packageNames.length === 1
changeset.packageNames.length === 1
? changeset.packageNames[0]!
: resolvePackage(parsed.scope || undefined, []);
@@ -687,20 +682,20 @@ export async function collectEntries(
title = prInfo.title;
domain = classifyEntry(prInfo, parsed.scope || undefined, prInfo.files);
packageName =
changeset?.packageNames.length === 1
changeset.packageNames.length === 1
? changeset.packageNames[0]!
: resolvePackage(parsed.scope || undefined, prInfo.files);
if (prInfo.labels.includes("breaking")) breaking = true;
}
const releasePackages =
changeset?.packageNames.length && changeset.packageNames.length > 0
changeset.packageNames.length > 0
? [...new Set(changeset.packageNames)]
: [packageName];
for (const releasePackage of releasePackages) {
entries.push({
id: `${changeset ? `pr-${prNumber}` : `git-${prNumber}`}:${releasePackage}`,
id: `pr-${prNumber}:${releasePackage}`,
rewriteKey: `pr-${prNumber}`,
title,
changesetDescription,
@@ -184,9 +184,11 @@ function createChangesetStateFixture(
interface ReleaseFixtureOptions {
mergeVersionPR?: boolean;
consumeLaterChangeset?: boolean;
includePRWithoutChangeset?: boolean;
splitVersionCommit?: boolean;
separateChangesetCommit?: boolean;
staleReleaseTag?: boolean;
supersedeChangeset?: boolean;
uppercaseChangesetId?: boolean;
}
@@ -220,7 +222,7 @@ function createReleaseWithFollowUpCommit(options: ReleaseFixtureOptions = {}): {
: options.separateChangesetCommit
? "standalone-description"
: "pr-42";
const changesetPath = `.changeset/${changesetId}.md`;
let changesetPath = `.changeset/${changesetId}.md`;
const changesetDescription = options.separateChangesetCommit
? "Require explicit migration"
: "Require an explicit migration after the release.";
@@ -246,6 +248,38 @@ function createReleaseWithFollowUpCommit(options: ReleaseFixtureOptions = {}): {
"fix(session): require explicit migration (#42)",
]);
if (options.includePRWithoutChangeset) {
writeFixtureFile(
workspace,
"packages/better-auth/vitest.config.ts",
"export default {};",
);
git(workspace, ["add", "."]);
git(workspace, ["commit", "-m", "fix: isolate package tests (#43)"]);
}
if (options.supersedeChangeset) {
rmSync(resolve(workspace, changesetPath));
changesetPath = ".changeset/pr-44.md";
writeFixtureFile(
workspace,
changesetPath,
[
"---",
'"better-auth": patch',
"---",
"",
"Replace the superseded release intent.",
].join("\n"),
);
git(workspace, ["add", "."]);
git(workspace, [
"commit",
"-m",
"revert: replace the superseded change (#44)",
]);
}
rmSync(resolve(workspace, changesetPath));
if (options.splitVersionCommit) {
git(workspace, ["add", "."]);
@@ -533,6 +567,28 @@ describe("release changeset collection", () => {
}
});
it("uses consumed changesets as the release entry source", () => {
const fixture = createReleaseWithFollowUpCommit({
includePRWithoutChangeset: true,
supersedeChangeset: true,
});
try {
const result = collectReleaseNotes("2.0.0", {
branch: "HEAD",
commitRef: fixture.commitRef,
workspace: fixture.workspace,
});
expect(result.status, result.stderr).toBe(0);
expect(result.stdout).toContain("Replace the superseded release intent.");
expect(result.stdout).toContain("pull/44");
expect(result.stdout).not.toContain("pull/42");
expect(result.stdout).not.toContain("pull/43");
} finally {
rmSync(fixture.workspace, { recursive: true });
}
});
it("preserves mixed-case changeset identifiers", () => {
const fixture = createReleaseWithFollowUpCommit({
uppercaseChangesetId: true,