Add initial support for having GPUI release notes (#64203)
Release Notes: - N/A
This commit is contained in:
1 parent
280beb8920
commit
3b54ef7ab4
7 files changed
+176
-24
No files matched your search
@@ -26,6 +26,10 @@
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier",
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier",
|
||||
},
|
||||
"CSS": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier",
|
||||
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
Generated
+25
@@ -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: {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["dangerfile.ts"]
|
||||
}
|
||||
+61
-18
@@ -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();
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user