Add initial support for having GPUI release notes (#64203)

Release Notes:

- N/A
This commit is contained in:
Finn Evers authored and GitHub committed 2026-09-15 12:43:19 +00:00
1 parent 280beb8920
commit 3b54ef7ab4
7 files changed
+176 -24

No files matched your search

+4
View File
@@ -26,6 +26,10 @@
"tab_size": 2,
"formatter": "prettier",
},
"TypeScript": {
"tab_size": 2,
"formatter": "prettier",
},
"CSS": {
"tab_size": 2,
"formatter": "prettier",
+44 -4
View File
@@ -1,4 +1,6 @@
import { danger, message, warn, fail, schedule } from "danger";
import { releaseNotesSection, releaseNotesEntries } from "../lib/release-notes";
const { prHygiene } = require("danger-plugin-pr-hygiene");
prHygiene({
@@ -12,10 +14,9 @@ prHygiene({
},
});
const RELEASE_NOTES_PATTERN = /Release Notes:(\r?\n)+- /gm;
const body = danger.github.pr.body;
const hasReleaseNotes = RELEASE_NOTES_PATTERN.test(body);
const releaseNotes = releaseNotesSection(body);
const hasReleaseNotes = releaseNotes !== "";
if (!hasReleaseNotes) {
warn(
@@ -36,14 +37,53 @@ if (!hasReleaseNotes) {
"",
"- N/A",
"```",
"",
"If your change touches a `gpui`-related crate, you must also add an entry for the GPUI release notes:",
"```",
"Release Notes:",
"",
"- Added/Fixed/Improved ...",
"- [GPUI] Added/Fixed/Improved ...",
"```",
"",
'If the change is not user-facing for GPUI users, use "- [GPUI] N/A" for that entry.',
].join("\n"),
);
}
const GPUI_RELEASE_NOTES_PATTERN = /^- \[GPUI\]/im;
const gpuiCrates = danger.git.fileMatch("crates/gpui*/**");
if (gpuiCrates.edited || gpuiCrates.deleted) {
if (!releaseNotesEntries(releaseNotes).some((entry) => GPUI_RELEASE_NOTES_PATTERN.test(entry))) {
const { edited, deleted } = gpuiCrates.getKeyedPaths();
const touchedGpuiCratesStr = [...edited, ...deleted]
.map((file) => "`" + file.split("/")[1] + "`")
.filter((crate, index, self) => self.indexOf(crate) === index)
.join(", ");
fail(
[
`This PR modifies GPUI crates (${touchedGpuiCratesStr}), which requires a GPUI release notes entry.`,
"",
'Please add at least one entry prefixed with `[GPUI]` to the "Release Notes" section:',
"```",
"Release Notes:",
"",
"- Added/Fixed/Improved ...",
"- [GPUI] Added/Fixed/Improved ...",
"```",
"",
'If the change is not user-facing for GPUI users, use "- [GPUI] N/A" for that entry.',
].join("\n"),
);
}
}
const ISSUE_LINK_PATTERN =
/(?:- )?(?<!(?:Close[sd]?|Fixe[sd]|Resolve[sd]|Implement[sed]|Follow-up of|Part of):?\s+)https:\/\/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/gi;
const bodyWithoutReleaseNotes = hasReleaseNotes ? body.split(/Release Notes:/)[0] : body;
const bodyWithoutReleaseNotes = hasReleaseNotes ? body.slice(0, body.length - releaseNotes.length) : body;
const includesIssueUrl = ISSUE_LINK_PATTERN.test(bodyWithoutReleaseNotes);
if (includesIssueUrl) {
+4 -2
View File
@@ -4,10 +4,12 @@
"private": true,
"main": "index.js",
"scripts": {
"danger": "danger"
"danger": "tsc --noEmit && danger"
},
"devDependencies": {
"@types/node": "24.13.4",
"danger": "13.0.8",
"danger-plugin-pr-hygiene": "0.7.1"
"danger-plugin-pr-hygiene": "0.7.1",
"typescript": "5.9.3"
}
}
+25
View File
@@ -8,12 +8,18 @@ importers:
.:
devDependencies:
'@types/node':
specifier: 24.13.4
version: 24.13.4
danger:
specifier: 13.0.8
version: 13.0.8
danger-plugin-pr-hygiene:
specifier: 0.7.1
version: 0.7.1
typescript:
specifier: 5.9.3
version: 5.9.3
packages:
@@ -81,6 +87,9 @@ packages:
'@octokit/types@13.10.0':
resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==}
'@types/node@24.13.4':
resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@@ -400,6 +409,14 @@ packages:
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
universal-user-agent@6.0.1:
resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==}
@@ -500,6 +517,10 @@ snapshots:
dependencies:
'@octokit/openapi-types': 24.2.0
'@types/node@24.13.4':
dependencies:
undici-types: 7.18.2
agent-base@7.1.4: {}
ansi-styles@4.3.0:
@@ -824,6 +845,10 @@ snapshots:
tr46@0.0.3: {}
typescript@5.9.3: {}
undici-types@7.18.2: {}
universal-user-agent@6.0.1: {}
webidl-conversions@3.0.1: {}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"strict": true,
"allowJs": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["dangerfile.ts"]
}
+61 -18
View File
@@ -1,21 +1,52 @@
#!/usr/bin/env node --redirect-warnings=/dev/null
const { execFileSync } = require("child_process");
const { parseArgs } = require("util");
const { releaseNotesSection, releaseNotesEntries } = require("./lib/release-notes");
main();
const GPUI_PREFIX_PATTERN = /^- \[GPUI\]\s*/i;
async function main() {
let version = process.argv[2];
let channel = process.argv[3];
let parts = version.split(".");
const USAGE = [
"Usage: draft-release-notes <version> {stable|preview} [--gpui]",
"",
"By default, release notes entries prefixed with `- [GPUI]` are excluded.",
"With --gpui, only those entries are included, with the prefix removed.",
].join("\n");
if (
process.argv.length != 4 ||
parts.length != 3 ||
parts.find((part) => isNaN(part)) != null ||
(channel != "stable" && channel != "preview")
) {
console.log("Usage: draft-release-notes <version> {stable|preview}");
function parseArguments() {
let values, positionals;
try {
({ values, positionals } = parseArgs({
allowPositionals: true,
options: { gpui: { type: "boolean", default: false } },
}));
} catch (e) {
return { error: e.message };
}
let [version, channel, ...rest] = positionals;
if (version == null || channel == null || rest.length > 0) {
return { error: "Expected exactly two positional arguments: <version> {stable|preview}" };
}
let parts = version.split(".").map((part) => Number(part));
if (parts.length != 3 || parts.some((part) => !Number.isInteger(part))) {
return { error: `Invalid version "${version}", expected <major>.<minor>.<patch>` };
}
if (channel != "stable" && channel != "preview") {
return { error: `Invalid channel "${channel}", expected "stable" or "preview"` };
}
return { version, channel, parts, gpui: values.gpui };
}
function main() {
let { error, version, channel, parts, gpui } = parseArguments();
if (error) {
console.error(error);
console.error("");
console.error(USAGE);
process.exit(1);
}
@@ -61,7 +92,8 @@ async function main() {
for (const commit of newCommits) {
let link = "https://github.com/zed-industries/zed/pull/" + commit.pr;
let notes = commit.releaseNotes;
let filteredNotes = filterReleaseNotes(commit.releaseNotes, gpui);
let notes = filteredNotes;
if (commit.pr == "") {
link = "https://github.com/zed-industries/zed/commits/" + commit.hash;
} else if (!notes.includes("zed-industries/zed/issues")) {
@@ -70,7 +102,7 @@ async function main() {
if (commit.releaseNotes == "") {
missing.push("- MISSING " + commit.firstLine + " " + link);
} else if (commit.releaseNotes.startsWith("- N/A")) {
} else if (filteredNotes == "" || filteredNotes.startsWith("- N/A")) {
skipped.push("- N/A " + commit.firstLine + " " + link);
} else {
releaseNotes.push(notes);
@@ -85,6 +117,16 @@ async function main() {
}
}
// Entries prefixed with `- [GPUI]` describe changes for GPUI users. They are kept
// (with the prefix removed) only when drafting GPUI release notes, and dropped otherwise.
function filterReleaseNotes(releaseNotes, gpui) {
return releaseNotes
.split("\n")
.filter((line) => GPUI_PREFIX_PATTERN.test(line) == gpui)
.map((line) => (gpui ? line.replace(GPUI_PREFIX_PATTERN, "- ") : line))
.join("\n");
}
function getCommits(oldTag, newTag) {
const pullRequestNumbers = execFileSync(
"git",
@@ -98,10 +140,7 @@ function getCommits(oldTag, newTag) {
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, " ");
let releaseNotes = releaseNotesEntries(releaseNotesSection(commit)).join("\n");
if (releaseNotes.includes("<public_issue_number_if_exists>")) {
releaseNotes = "";
@@ -118,3 +157,7 @@ function getCommits(oldTag, newTag) {
return pullRequestNumbers;
}
if (require.main === module) {
main();
}
+27
View File
@@ -0,0 +1,27 @@
function releaseNotesSection(body) {
// Inline mentions and bullet text must not override the final release notes heading.
const headings = [...body.matchAll(/^Release Notes:(?:\r?\n)+(?=- )/gim)];
const lastHeading = headings[headings.length - 1];
return lastHeading ? body.slice(lastHeading.index + lastHeading[0].length) : "";
}
function releaseNotesEntries(section) {
const entries = [];
// Blank lines can separate bullets, but a non-bullet paragraph ends the notes
// so commit trailers and other trailing text are not published as release notes.
for (const paragraph of section.split(/\r?\n(?:[ \t]*\r?\n)+/)) {
if (!paragraph.startsWith("- ")) {
break;
}
entries.push(
...paragraph
.trimEnd()
.split(/\r?\n(?=- )/)
.map((entry) => entry.replace(/\r?\n/g, " ")),
);
}
return entries;
}
module.exports = { releaseNotesSection, releaseNotesEntries };