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}
68
69function getCommits(oldTag, newTag) {
70 const pullRequestNumbers = execFileSync(
71 "git",
72 ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"],
73 { encoding: "utf8" },
74 )
75 .replace(/\r\n/g, "\n")
76 .split("DIVIDER\n")
77 .filter((commit) => commit.length > 0)
78 .map((commit) => {
79 let [hash, firstLine] = commit.split("\n")[0].split("|||");
80 let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || "";
81 let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || "";
82 let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "")
83 .split("\n\n")[0]
84 .trim()
85 .replace(/\n(?![\n-])/g, " ");
86
87 if (releaseNotes.includes("<public_issue_number_if_exists>")) {
88 releaseNotes = "";
89 }
90
91 return {
92 hash,
93 pr,
94 cherryPick,
95 releaseNotes,
96 firstLine,
97 };
98 });
99
100 return pullRequestNumbers;
101}
102
103function ensureTag(tag) {
104 try {
105 execFileSync("git", ["rev-parse", "--verify", tag]);
106 return true;
107 } catch (e) {
108 try {
109 execFileSync("git"[("fetch", "origin", "--shallow-exclude", tag)]);
110 execFileSync("git"[("fetch", "origin", "--deepen", "1")]);
111 return true;
112 } catch (e) {
113 return false;
114 }
115 }
116}