#!/usr/bin/env node --redirect-warnings=/dev/null const { execFileSync } = require("child_process"); main(); async function main() { let version = process.argv[2]; let channel = process.argv[3]; let parts = version.split("."); if ( process.argv.length != 4 || parts.length != 3 || parts.find((part) => isNaN(part)) != null || (channel != "stable" && channel != "preview") ) { console.log("Usage: draft-release-notes {stable|preview}"); process.exit(1); } let priorVersion = [parts[0], parts[1], parts[2] - 1].join("."); let suffix = ""; if (channel == "preview") { suffix = "-pre"; if (parts[2] == 0) { priorVersion = [parts[0], parts[1] - 1, 0].join("."); } } else if (!ensureTag(`v${priorVersion}`)) { console.log("Copy the release notes from preview."); process.exit(0); } let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`]; if (!ensureTag(tag) || !ensureTag(priorTag)) { console.log("Could not draft release notes, missing a tag:", tag, priorTag); process.exit(0); } const newCommits = getCommits(priorTag, tag); let releaseNotes = []; let missing = []; let skipped = []; for (const commit of newCommits) { let link = "https://github.com/zed-industries/zed/pull/" + commit.pr; let notes = commit.releaseNotes; if (commit.pr == "") { link = "https://github.com/zed-industries/zed/commits/" + commit.hash; } else if (!notes.includes("zed-industries/zed/issues")) { notes = notes + " ([#" + commit.pr + "](" + link + "))"; } if (commit.releaseNotes == "") { missing.push("- MISSING " + commit.firstLine + " " + link); } else if (commit.releaseNotes.startsWith("- N/A")) { skipped.push("- N/A " + commit.firstLine + " " + link); } else { releaseNotes.push(notes); } } console.log(releaseNotes.join("\n") + "\n"); console.log(""); } function getCommits(oldTag, newTag) { const pullRequestNumbers = execFileSync( "git", ["log", `${oldTag}..${newTag}`, "--format=DIVIDER\n%H|||%B"], { encoding: "utf8" }, ) .replace(/\r\n/g, "\n") .split("DIVIDER\n") .filter((commit) => commit.length > 0) .map((commit) => { let [hash, firstLine] = commit.split("\n")[0].split("|||"); let cherryPick = firstLine.match(/\(cherry-pick #([0-9]+)\)/)?.[1] || ""; let pr = firstLine.match(/\(#(\d+)\)$/)?.[1] || ""; let releaseNotes = (commit.split(/Release notes:.*\n/i)[1] || "") .split("\n\n")[0] .trim() .replace(/\n(?![\n-])/g, " "); if (releaseNotes.includes("")) { releaseNotes = ""; } return { hash, pr, cherryPick, releaseNotes, firstLine, }; }); return pullRequestNumbers; } function ensureTag(tag) { try { execFileSync("git", ["rev-parse", "--verify", tag]); return true; } catch (e) { try { execFileSync("git"[("fetch", "origin", "--shallow-exclude", tag)]); execFileSync("git"[("fetch", "origin", "--deepen", "1")]); return true; } catch (e) { return false; } } }