Have the CI server draft the release notes (#10700)

Conrad Irwin created

While I don't expect these to be useful for our weekly minor releases, I
hope that this will save a step for people doing mid-week patches.

Release Notes:

- N/A

Change summary

.github/workflows/ci.yml             |   3 
docs/src/developing_zed__releases.md |  26 ++++--
script/draft-release-notes           | 109 ++++++++++++++++++++++++++++++
3 files changed, 127 insertions(+), 11 deletions(-)

Detailed changes

.github/workflows/ci.yml 🔗

@@ -205,6 +205,7 @@ jobs:
             echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
             exit 1
           fi
+          script/draft-release-notes "$version" "$channel" > target/release-notes.md
 
       - name: Generate license file
         run: script/generate-licenses
@@ -248,7 +249,7 @@ jobs:
             target/aarch64-apple-darwin/release/Zed-aarch64.dmg
             target/x86_64-apple-darwin/release/Zed-x86_64.dmg
             target/release/Zed.dmg
-          body: ""
+          body_file: target/release-notes.md
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 

docs/src/developing_zed__releases.md 🔗

@@ -17,25 +17,31 @@ You will need write access to the Zed repository to do this:
 - Run `./script/bump-zed-minor-versions` and push the tags
   and branches as instructed.
 - Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
-- Copy the release notes from the previous Preview release(s) to the current Stable release.
-- Write new release notes for Preview. `/script/get-preview-channel-changes` can help with this, but you'll need to edit and format the output to make it good.
-- Download the artifacts for each release and test that you can run them locally.
-- Publish the releases.
+- While you're waiting:
+  - Start creating the new release notes for preview. You can start with the output of `./script/get-preview-channel-changes`.
+  - Start drafting the release tweets.
+- Once the builds are ready:
+  - Copy the release notes from the previous Preview release(s) to the current Stable release.
+  - Download the artifacts for each release and test that you can run them locally.
+  - Publish the releases on GitHub.
+  - Tweet the tweets (Credentials are in 1password).
 
 ## Patch release process
 
-If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to the appropriate branch.
+If your PR fixes a panic or a crash, you should cherry-pick it to the current stable and preview branches. If your PR fixes a regression in recently released code, you should cherry-pick it to preview.
 
 You will need write access to the Zed repository to do this:
 
-- Cherry pick them onto the correct branch. You can either do this manually, or leave a comment of the form `/cherry-pick v0.XXX.x` on the PR, and the GitHub bot should do it for you.
-- Run `./script/trigger-release {preview|stable}`
+- Send a PR containing your change to `main` as normal.
+- Leave a comment on the PR `/cherry-pick v0.XXX.x`. Once your PR is merged, the Github bot will send a PR to the branch.
+  - In case of a merge conflict, you will have to cherry-pick manually and push the change to the `v0.XXX.x` branch.
+- After the commits are cherry-picked onto the branch, run `./script/trigger-release {preview|stable}`. This will bump the version numbers, create a new release tag, and kick off a release build.
 - Wait for the builds to appear at https://github.com/zed-industries/zed/releases (typically takes around 30 minutes)
-- Add release notes using the `Release notes:` section of each cherry-picked PR.
+- Proof-read and edit the release notes as needed.
 - Download the artifacts for each release and test that you can run them locally.
 - Publish the release.
 
 ## Nightly release process
 
-- Merge your changes to main
-- Run `./script/trigger-release {nightly}`
+In addition to the public releases, we also have a nightly build that we encourage employees to use.
+Nightly is released by cron once a day, and can be shipped as often as you'd like. There are no release notes or announcements, so you can just merge your changes to main and run `./script/trigger-release nightly`.

script/draft-release-notes 🔗

@@ -0,0 +1,109 @@
+#!/usr/bin/env node --redirect-warnings=/dev/null
+
+const { execFileSync } = require("child_process");
+
+main();
+
+async function main() {
+  let version = process.argv[2];
+  let channel = process.argv[3];
+  let parts = version.split(".");
+
+  if (
+    process.argv.length != 4 ||
+    parts.length != 3 ||
+    parts.find((part) => isNaN(part)) != null ||
+    (channel != "stable" && channel != "preview")
+  ) {
+    console.log("Usage: draft-release-notes <version> {stable|preview}");
+    process.exit(1);
+  }
+
+  let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
+  let suffix = "";
+
+  if (channel == "preview") {
+    suffix = "-pre";
+    if (parts[2] == 0) {
+      priorVersion = [parts[0], parts[1] - 1, 0].join(".");
+    }
+  } else if (!tagExists("v${priorVersion}")) {
+    console.log("Copy the release notes from preview.");
+    process.exit(0);
+  }
+
+  let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
+
+  const newCommits = getCommits(priorTag, tag);
+
+  let releaseNotes = [];
+  let missing = [];
+  let skipped = [];
+
+  for (const commit of newCommits) {
+    let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
+    let notes = commit.releaseNotes;
+    if (commit.pr == "") {
+      link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
+    } else if (!notes.includes("zed-industries/zed/issues")) {
+      notes = notes + " ([#" + commit.pr + "](" + link + "))";
+    }
+
+    if (commit.releaseNotes == "") {
+      missing.push("- MISSING " + commit.firstLine + " " + link);
+    } else if (commit.releaseNotes.startsWith("- N/A")) {
+      skipped.push("- N/A " + commit.firstLine + " " + link);
+    } else {
+      releaseNotes.push(notes);
+    }
+  }
+
+  console.log(releaseNotes.join("\n") + "\n");
+  console.log("<!-- ");
+  console.log(missing.join("\n"));
+  console.log(skipped.join("\n"));
+  console.log("-->");
+}
+
+function getCommits(oldTag, newTag) {
+  const pullRequestNumbers = execFileSync(
+    "git",
+    ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
+    { encoding: "utf8" },
+  )
+    .replace(/\r\n/g, "\n")
+    .split("DIVIDER\n")
+    .filter((commit) => commit.length > 0)
+    .map((commit) => {
+      let [hash, firstLine] = commit.split("\n")[0].split("|||");
+      let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
+      let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
+      let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
+        .split("\n\n")[0]
+        .trim()
+        .replace(/\n(?![\n-])/g, " ");
+
+      if (releaseNotes.includes("<public_issue_number_if_exists>")) {
+        releaseNotes = "";
+      }
+
+      return {
+        hash,
+        pr,
+        cherryPick,
+        releaseNotes,
+        firstLine,
+      };
+    });
+
+  return pullRequestNumbers;
+}
+
+function tagExists(tag) {
+  try {
+    execFileSync("git", ["rev-parse", "--verify", tag]);
+    return true;
+  } catch (e) {
+    return false;
+  }
+}