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}