diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9266c5f116296ea07ebbffbc37091506e461a23..4c4864fa32ce49c58669acb487f4e8d43226142d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -205,6 +205,7 @@ jobs: echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" exit 1 fi + script/draft-release-notes "$version" "$channel" > target/release-notes.md - name: Generate license file run: script/generate-licenses @@ -248,7 +249,7 @@ jobs: target/aarch64-apple-darwin/release/Zed-aarch64.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg target/release/Zed.dmg - body: "" + body_file: target/release-notes.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/script/draft-release-notes b/script/draft-release-notes new file mode 100755 index 0000000000000000000000000000000000000000..b3760441067266a49b2853d9f0096976fdee98d4 --- /dev/null +++ b/script/draft-release-notes @@ -0,0 +1,109 @@ +#!/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 (!tagExists("v${priorVersion}")) { + console.log("Copy the release notes from preview."); + process.exit(0); + } + + let [tag, priorTag] = [`v${version}${suffix}`, `v${priorVersion}${suffix}`]; + + 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 tagExists(tag) { + try { + execFileSync("git", ["rev-parse", "--verify", tag]); + return true; + } catch (e) { + return false; + } +}