draft-release-notes

  1#!/usr/bin/env node --redirect-warnings=/dev/null
  2
  3const { execFileSync } = require("child_process");
  4
  5main();
  6
  7async function main() {
  8  let version = process.argv[2];
  9  let channel = process.argv[3];
 10  let parts = version.split(".");
 11
 12  if (
 13    process.argv.length != 4 ||
 14    parts.length != 3 ||
 15    parts.find((part) => isNaN(part)) != null ||
 16    (channel != "stable" && channel != "preview")
 17  ) {
 18    console.log("Usage: draft-release-notes <version> {stable|preview}");
 19    process.exit(1);
 20  }
 21
 22  // currently we can only draft notes for patch releases.
 23  if (parts[2] === 0) {
 24    process.exit(0);
 25  }
 26
 27  let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
 28  let suffix = channel == "preview" ? "-pre" : "";
 29  let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
 30
 31  try {
 32    execFileSync("rm", ["-rf", "target/shallow_clone"]);
 33    execFileSync("git", [
 34      "clone",
 35      "https://github.com/zed-industries/zed",
 36      "target/shallow_clone",
 37      "--filter=tree:0",
 38      "--no-checkout",
 39      "--branch",
 40      tag,
 41      "--depth",
 42      100,
 43    ]);
 44    execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", tag]);
 45    try {
 46      execFileSync("git", ["-C", "target/shallow_clone", "rev-parse", "--verify", priorTag]);
 47    } catch (e) {
 48      console.error(`Prior tag ${priorTag} not found`);
 49      process.exit(0);
 50    }
 51  } catch (e) {
 52    console.error(e.stderr.toString());
 53    process.exit(1);
 54  }
 55
 56  const newCommits = getCommits(priorTag, tag);
 57
 58  let releaseNotes = [];
 59  let missing = [];
 60  let skipped = [];
 61
 62  for (const commit of newCommits) {
 63    let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
 64    let notes = commit.releaseNotes;
 65    if (commit.pr == "") {
 66      link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
 67    } else if (!notes.includes("zed-industries/zed/issues")) {
 68      notes = notes + " ([#" + commit.pr + "](" + link + "))";
 69    }
 70
 71    if (commit.releaseNotes == "") {
 72      missing.push("- MISSING " + commit.firstLine + " " + link);
 73    } else if (commit.releaseNotes.startsWith("- N/A")) {
 74      skipped.push("- N/A " + commit.firstLine + " " + link);
 75    } else {
 76      releaseNotes.push(notes);
 77    }
 78  }
 79
 80  console.log(releaseNotes.join("\n") + "\n");
 81}
 82
 83function getCommits(oldTag, newTag) {
 84  const pullRequestNumbers = execFileSync(
 85    "git",
 86    ["-C", "target/shallow_clone", "log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
 87    { encoding: "utf8" },
 88  )
 89    .replace(/\r\n/g, "\n")
 90    .split("DIVIDER\n")
 91    .filter((commit) => commit.length > 0)
 92    .map((commit) => {
 93      let [hash, firstLine] = commit.split("\n")[0].split("|||");
 94      let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
 95      let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
 96      let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
 97        .split("\n\n")[0]
 98        .trim()
 99        .replace(/\n(?![\n-])/g, " ");
100
101      if (releaseNotes.includes("<public_issue_number_if_exists>")) {
102        releaseNotes = "";
103      }
104
105      return {
106        hash,
107        pr,
108        cherryPick,
109        releaseNotes,
110        firstLine,
111      };
112    });
113
114  return pullRequestNumbers;
115}