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}