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 (!ensureTag(`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  if (!ensureTag(tag) || !ensureTag(priorTag)) {
 38    console.log("Could not draft release notes, missing a tag:", tag, priorTag);
 39    process.exit(0);
 40  }
 41
 42  const newCommits = getCommits(priorTag, tag);
 43
 44  let releaseNotes = [];
 45  let missing = [];
 46  let skipped = [];
 47
 48  for (const commit of newCommits) {
 49    let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
 50    let notes = commit.releaseNotes;
 51    if (commit.pr == "") {
 52      link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
 53    } else if (!notes.includes("zed-industries/zed/issues")) {
 54      notes = notes + " ([#" + commit.pr + "](" + link + "))";
 55    }
 56
 57    if (commit.releaseNotes == "") {
 58      missing.push("- MISSING " + commit.firstLine + " " + link);
 59    } else if (commit.releaseNotes.startsWith("- N/A")) {
 60      skipped.push("- N/A " + commit.firstLine + " " + link);
 61    } else {
 62      releaseNotes.push(notes);
 63    }
 64  }
 65
 66  console.log(releaseNotes.join("\n") + "\n");
 67  console.log("<!-- ");
 68  console.log(missing.join("\n"));
 69  console.log(skipped.join("\n"));
 70  console.log("-->");
 71}
 72
 73function getCommits(oldTag, newTag) {
 74  const pullRequestNumbers = execFileSync(
 75    "git",
 76    ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
 77    { encoding: "utf8" },
 78  )
 79    .replace(/\r\n/g, "\n")
 80    .split("DIVIDER\n")
 81    .filter((commit) => commit.length > 0)
 82    .map((commit) => {
 83      let [hash, firstLine] = commit.split("\n")[0].split("|||");
 84      let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
 85      let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
 86      let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
 87        .split("\n\n")[0]
 88        .trim()
 89        .replace(/\n(?![\n-])/g, " ");
 90
 91      if (releaseNotes.includes("<public_issue_number_if_exists>")) {
 92        releaseNotes = "";
 93      }
 94
 95      return {
 96        hash,
 97        pr,
 98        cherryPick,
 99        releaseNotes,
100        firstLine,
101      };
102    });
103
104  return pullRequestNumbers;
105}
106
107function ensureTag(tag) {
108  try {
109    execFileSync("git", ["rev-parse", "--verify", tag]);
110    return true;
111  } catch (e) {
112    try {
113      execFileSync("git"[("fetch", "origin", "--shallow-exclude", tag)]);
114      execFileSync("git"[("fetch", "origin", "--deepen", "1")]);
115      return true;
116    } catch (e) {
117      return false;
118    }
119  }
120}