diff --git a/packages/release-tooling/src/release-notes/collect.ts b/packages/release-tooling/src/release-notes/collect.ts index d846891446..bc8ca2092a 100644 --- a/packages/release-tooling/src/release-notes/collect.ts +++ b/packages/release-tooling/src/release-notes/collect.ts @@ -502,8 +502,8 @@ function loadPreviousPrereleaseChangesets(version: string): Set { } /** - * 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, diff --git a/packages/release-tooling/test/release-collection.test.ts b/packages/release-tooling/test/release-collection.test.ts index 8618db86c5..e537db1c44 100644 --- a/packages/release-tooling/test/release-collection.test.ts +++ b/packages/release-tooling/test/release-collection.test.ts @@ -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,