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  let priorVersion = [parts[0], parts[1], parts[2] - 1].join(".");
 23  let suffix = "";
 24
 25  if (channel == "preview") {
 26    suffix = "-pre";
 27    if (parts[2] == 0) {
 28      priorVersion = [parts[0], parts[1] - 1, 0].join(".");
 29    }
 30  } else if (!tagExists("v${priorVersion}")) {
 31    console.log("Copy the release notes from preview.");
 32    process.exit(0);
 33  }
 34
 35  let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`];
 36
 37  const newCommits = getCommits(priorTag, tag);
 38
 39  let releaseNotes = [];
 40  let missing = [];
 41  let skipped = [];
 42
 43  for (const commit of newCommits) {
 44    let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
 45    let notes = commit.releaseNotes;
 46    if (commit.pr == "") {
 47      link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
 48    } else if (!notes.includes("zed-industries/zed/issues")) {
 49      notes = notes + " ([#" + commit.pr + "](" + link + "))";
 50    }
 51
 52    if (commit.releaseNotes == "") {
 53      missing.push("- MISSING " + commit.firstLine + " " + link);
 54    } else if (commit.releaseNotes.startsWith("- N/A")) {
 55      skipped.push("- N/A " + commit.firstLine + " " + link);
 56    } else {
 57      releaseNotes.push(notes);
 58    }
 59  }
 60
 61  console.log(releaseNotes.join("\n") + "\n");
 62  console.log("<!-- ");
 63  console.log(missing.join("\n"));
 64  console.log(skipped.join("\n"));
 65  console.log("-->");
 66}
 67
 68function getCommits(oldTag, newTag) {
 69  const pullRequestNumbers = execFileSync(
 70    "git",
 71    ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
 72    { encoding: "utf8" },
 73  )
 74    .replace(/\r\n/g, "\n")
 75    .split("DIVIDER\n")
 76    .filter((commit) => commit.length > 0)
 77    .map((commit) => {
 78      let [hash, firstLine] = commit.split("\n")[0].split("|||");
 79      let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
 80      let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
 81      let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
 82        .split("\n\n")[0]
 83        .trim()
 84        .replace(/\n(?![\n-])/g, " ");
 85
 86      if (releaseNotes.includes("<public_issue_number_if_exists>")) {
 87        releaseNotes = "";
 88      }
 89
 90      return {
 91        hash,
 92        pr,
 93        cherryPick,
 94        releaseNotes,
 95        firstLine,
 96      };
 97    });
 98
 99  return pullRequestNumbers;
100}
101
102function tagExists(tag) {
103  try {
104    execFileSync("git", ["rev-parse", "--verify", tag]);
105    return true;
106  } catch (e) {
107    return false;
108  }
109}