diff --git a/release/src/constants.ts b/release/src/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..116d986bdbc56c440c86f3ba67acafe95940e746 --- /dev/null +++ b/release/src/constants.ts @@ -0,0 +1,5 @@ +export const nonUserFacingLabels = [ + '.CI & Tests', + '.Building & Releasing', + 'Type:Documentation', +]; \ No newline at end of file diff --git a/release/src/release-notes.ts b/release/src/release-notes.ts index fcfae35e0f337720d69e13e24db8ec480aaa7ab5..bf247f17bbd89df3845a503c9d0fefa91d225efd 100644 --- a/release/src/release-notes.ts +++ b/release/src/release-notes.ts @@ -1,3 +1,6 @@ +import { match } from 'ts-pattern'; + +import { nonUserFacingLabels } from "./constants"; import { getMilestoneIssues, isLatestRelease } from "./github"; import type { ReleaseProps, Issue } from "./types"; import { @@ -34,15 +37,37 @@ SHA-256 checksum for the {{version}} JAR: {{bug-fixes}} +**Already Fixed** + +Issues that we have recently confirmed to have been fixed at some point in the past. + +{{already-fixed}} + +**Under the Hood** + +{{under-the-hood}} + </details> `; -const isBugIssue = (issue: Issue) => { +const hasLabel = (issue: Issue, label: string) => { if (typeof issue.labels === 'string') { - return issue.labels.includes("Type:Bug"); + return issue.labels.includes(label); } - return issue.labels.some(tag => tag.name === "Type:Bug"); + return issue.labels.some(tag => tag.name === label); +} + +const isBugIssue = (issue: Issue) => { + return hasLabel(issue, "Type:Bug"); +} + +const isAlreadyFixedIssue = (issue: Issue) => { + return hasLabel(issue, ".Already Fixed"); +}; + +const isNonUserFacingIssue = (issue: Issue) => { + return nonUserFacingLabels.some(label => hasLabel(issue, label)); } const formatIssue = (issue: Issue) => `- ${issue.title} (#${issue.number})`; @@ -69,6 +94,35 @@ export const getReleaseTitle = (version: string) => { return `Metabase ${version}`; }; +enum IssueType { + bugFixes = 'bugFixes', + enhancements = 'enhancements', + alreadyFixedIssues = 'alreadyFixedIssues', + underTheHoodIssues = 'underTheHoodIssues', +} + +const issueMap: Record<IssueType, Issue[]> = { + bugFixes: [], + enhancements: [], + alreadyFixedIssues: [], + underTheHoodIssues: [], +}; + +export const categorizeIssues = (issues: Issue[]) => { + return issues.reduce((issueMap, issue) => { + const category: IssueType = match(issue) + .when(isNonUserFacingIssue, () => IssueType.underTheHoodIssues) + .when(isAlreadyFixedIssue, () => IssueType.alreadyFixedIssues) + .when(isBugIssue, () => IssueType.bugFixes) + .otherwise(() => IssueType.enhancements); + + return { + ...issueMap, + [category]: [...issueMap[category], issue], + } + }, { ...issueMap }); +}; + export const generateReleaseNotes = ({ version, checksum, @@ -78,15 +132,16 @@ export const generateReleaseNotes = ({ checksum: string; issues: Issue[]; }) => { - const bugFixes = issues.filter(isBugIssue); - const enhancements = issues.filter(issue => !isBugIssue(issue)); + const issuesByType = categorizeIssues(issues); return releaseTemplate .replace( "{{enhancements}}", - enhancements?.map(formatIssue).join("\n") ?? "", + issuesByType.enhancements.map(formatIssue).join("\n") ?? "", ) - .replace("{{bug-fixes}}", bugFixes?.map(formatIssue).join("\n") ?? "") + .replace("{{bug-fixes}}", issuesByType.bugFixes.map(formatIssue).join("\n") ?? "") + .replace("{{already-fixed}}", issuesByType.alreadyFixedIssues.map(formatIssue).join("\n")) + .replace("{{under-the-hood}}", issuesByType.underTheHoodIssues.map(formatIssue).join("\n")) .replace("{{docker-tag}}", getDockerTag(version)) .replace("{{download-url}}", getDownloadUrl(version)) .replace("{{version}}", version) diff --git a/release/src/release-notes.unit.spec.ts b/release/src/release-notes.unit.spec.ts index 9beaf6e38714706b4da5726c4ec2b7c7db956305..5efd34f43e87ae6baff637674c3d3f8fd4a11be5 100644 --- a/release/src/release-notes.unit.spec.ts +++ b/release/src/release-notes.unit.spec.ts @@ -1,4 +1,4 @@ -import { generateReleaseNotes, getReleaseTitle } from "./release-notes"; +import { generateReleaseNotes, getReleaseTitle, categorizeIssues } from "./release-notes"; import type { Issue } from "./types"; describe("Release Notes", () => { @@ -25,13 +25,23 @@ describe("Release Notes", () => { const issues = [ { number: 1, - title: "Issue 1", + title: "Bug Issue", labels: [{ name: "Type:Bug" }], }, { number: 2, - title: "Issue 2", - labels: [{ name: "Type:Enhancement" }], + title: "Feature Issue", + labels: [{ name: "something" }], + }, + { + number: 3, + title: "Issue Already Fixed", + labels: [{ name: ".Already Fixed" }], + }, + { + number: 4, + title: "Issue That Users Don't Care About", + labels: [{ name: ".CI & Tests" }], }, ] as Issue[]; @@ -45,8 +55,10 @@ describe("Release Notes", () => { expect(notes).toContain("SHA-256 checksum for the v0.2.3 JAR"); expect(notes).toContain("1234567890abcdef"); - expect(notes).toContain("**Enhancements**\n\n- Issue 2 (#2)"); - expect(notes).toContain("**Bug fixes**\n\n- Issue 1 (#1)"); + expect(notes).toContain("**Enhancements**\n\n- Feature Issue (#2)"); + expect(notes).toContain("**Bug fixes**\n\n- Bug Issue (#1)"); + expect(notes).toContain("**Already Fixed**\n\nIssues that we have recently confirmed to have been fixed at some point in the past.\n\n- Issue Already Fixed (#3)"); + expect(notes).toContain("**Under the Hood**\n\n- Issue That Users Don't Care About (#4)"); expect(notes).toContain("metabase/metabase:v0.2.3"); expect(notes).toContain( @@ -64,8 +76,10 @@ describe("Release Notes", () => { expect(notes).toContain("SHA-256 checksum for the v1.2.3 JAR"); expect(notes).toContain("1234567890abcdef"); - expect(notes).toContain("**Enhancements**\n\n- Issue 2 (#2)"); - expect(notes).toContain("**Bug fixes**\n\n- Issue 1 (#1)"); + expect(notes).toContain("**Enhancements**\n\n- Feature Issue (#2)"); + expect(notes).toContain("**Bug fixes**\n\n- Bug Issue (#1)"); + expect(notes).toContain("**Already Fixed**\n\nIssues that we have recently confirmed to have been fixed at some point in the past.\n\n- Issue Already Fixed (#3)");; + expect(notes).toContain("**Under the Hood**\n\n- Issue That Users Don't Care About (#4)"); expect(notes).toContain("metabase/metabase-enterprise:v1.2.3"); expect(notes).toContain( @@ -73,4 +87,106 @@ describe("Release Notes", () => { ); }); }); + + describe('categorizeIssues', () => { + it('should categorize bug issues', () => { + const issue = { + number: 1, + title: "Bug Issue", + labels: [{ name: "Type:Bug" }], + } as Issue; + + const categorizedIssues = categorizeIssues([issue]); + + expect(categorizedIssues.bugFixes).toEqual([issue]); + }); + + it('should categorize already fixed issues', () => { + const issue = { + number: 3, + title: "Already Fixed Issue", + labels: [{ name: ".Already Fixed" }], + } as Issue; + + const categorizedIssues = categorizeIssues([issue]); + + expect(categorizedIssues.alreadyFixedIssues).toEqual([issue]); + }); + + it('should categorize non-user-facing issues', () => { + const issue = { + number: 4, + title: "Non User Facing Issue", + labels: [{ name: ".CI & Tests" }], + } as Issue; + + const categorizedIssues = categorizeIssues([issue]); + + expect(categorizedIssues.underTheHoodIssues).toEqual([issue]); + }); + + it('should categorize all other issues as enhancements', () => { + const issue = { + number: 2, + title: "Big Feature", + labels: [{ name: "something" }], + } as Issue; + + const categorizedIssues = categorizeIssues([issue]); + + expect(categorizedIssues.enhancements).toEqual([issue]); + }); + + it('should prioritize non-user-facing issues above all', () => { + const issue = { + number: 4, + title: "Non User Facing Issue", + labels: [{ name: ".CI & Tests" }, { name: "Type:Bug" }, { name: ".Already Fixed" }, { name: "Ptitard" } ], + } as Issue; + + const categorizedIssues = categorizeIssues([issue]); + + expect(categorizedIssues.underTheHoodIssues).toEqual([issue]); + expect(categorizedIssues.bugFixes).toEqual([]); + expect(categorizedIssues.alreadyFixedIssues).toEqual([]); + expect(categorizedIssues.enhancements).toEqual([]); + }); + + it('should put issues in only one bucket', () => { + const issues = [ + { + number: 1, + title: "Bug Issue", + labels: [{ name: "Type:Bug" }], + }, + { + number: 2, + title: "Big Feature", + labels: [{ name: "something" }], + }, + { + number: 3, + title: "Already Fixed Issue", + labels: [{ name: ".Already Fixed" }], + }, + { + number: 4, + title: "Non User Facing Issue", + labels: [{ name: ".CI & Tests" }], + }, + { + number: 5, + title: "Docs Issue", + labels: [{ name: "Type:Documentation" }], + }, + ] as Issue[]; + + const categorizedIssues = categorizeIssues(issues); + + expect(categorizedIssues.bugFixes).toEqual([issues[0]]); + expect(categorizedIssues.enhancements).toEqual([issues[1]]); + expect(categorizedIssues.alreadyFixedIssues).toEqual([issues[2]]); + expect(categorizedIssues.underTheHoodIssues).toEqual([issues[3], issues[4]]); + }); + }); });