From d549cd6414b367e780ba4a9ea6219665ca9ad17b Mon Sep 17 00:00:00 2001
From: Ryan Laurie <30528226+iethree@users.noreply.github.com>
Date: Tue, 2 Jul 2024 00:29:05 -0600
Subject: [PATCH] Create new release note sections (#44967)

* Create new release note sections

* add explanatory text
---
 release/src/constants.ts               |   5 +
 release/src/release-notes.ts           |  69 +++++++++++--
 release/src/release-notes.unit.spec.ts | 132 +++++++++++++++++++++++--
 3 files changed, 191 insertions(+), 15 deletions(-)
 create mode 100644 release/src/constants.ts

diff --git a/release/src/constants.ts b/release/src/constants.ts
new file mode 100644
index 00000000000..116d986bdbc
--- /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 fcfae35e0f3..bf247f17bbd 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 9beaf6e3871..5efd34f43e8 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]]);
+    });
+  });
 });
-- 
GitLab