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