Skip to content

Commit 2faf01b

Browse files
authored
Add changelog-for-patch script (#11052)
- Add ./scripts/changelog-for-patch.mjs - Extract common functions to ./scripts/utils/changelog - Use changelog-for-patch.mjs from release script
1 parent 9a61bf3 commit 2faf01b

File tree

4 files changed

+181
-91
lines changed

4 files changed

+181
-91
lines changed

scripts/changelog-for-patch.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env node
2+
3+
import path from "node:path";
4+
import minimist from "minimist";
5+
import semver from "semver";
6+
import {
7+
changelogUnreleasedDirPath,
8+
changelogUnreleasedDirs,
9+
getEntries,
10+
printEntries,
11+
replaceVersions,
12+
} from "./utils/changelog.mjs";
13+
14+
const { previousVersion, newVersion } = parseArgv();
15+
16+
const entries = changelogUnreleasedDirs.flatMap((dir) => {
17+
const dirPath = path.join(changelogUnreleasedDirPath, dir.name);
18+
return getEntries(dirPath);
19+
});
20+
21+
console.log(
22+
replaceVersions(
23+
printEntries(entries).join("\n\n"),
24+
previousVersion,
25+
newVersion,
26+
/** isPatch */ true
27+
)
28+
);
29+
30+
function parseArgv() {
31+
const argv = minimist(process.argv.slice(2));
32+
const previousVersion = argv["prev-version"];
33+
const newVersion = argv["new-version"];
34+
if (
35+
!previousVersion ||
36+
!newVersion ||
37+
semver.compare(previousVersion, newVersion) !== -1
38+
) {
39+
throw new Error(
40+
`Invalid argv, prev-version: ${previousVersion}, new-version: ${newVersion}`
41+
);
42+
}
43+
return { previousVersion, newVersion };
44+
}

scripts/draft-blog-post.mjs

Lines changed: 21 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33
import fs from "node:fs";
44
import path from "node:path";
55
import rimraf from "rimraf";
6-
import semver from "semver";
76
import createEsmUtils from "esm-utils";
7+
import {
8+
getEntries,
9+
replaceVersions,
10+
changelogUnreleasedDirPath,
11+
changelogUnreleasedDirs,
12+
printEntries,
13+
} from "./utils/changelog.mjs";
814

915
const { __dirname, require } = createEsmUtils(import.meta);
10-
const changelogUnreleasedDir = path.join(__dirname, "../changelog_unreleased");
1116
const blogDir = path.join(__dirname, "../website/blog");
1217
const introTemplateFile = path.join(
13-
changelogUnreleasedDir,
18+
changelogUnreleasedDirPath,
1419
"BLOG_POST_INTRO_TEMPLATE.md"
1520
);
16-
const introFile = path.join(changelogUnreleasedDir, "blog-post-intro.md");
21+
const introFile = path.join(changelogUnreleasedDirPath, "blog-post-intro.md");
1722
if (!fs.existsSync(introFile)) {
1823
fs.copyFileSync(introTemplateFile, introFile);
1924
}
@@ -50,46 +55,15 @@ const categoriesByDir = new Map(
5055
categories.map((category) => [category.dir, category])
5156
);
5257

53-
const dirs = fs
54-
.readdirSync(changelogUnreleasedDir, { withFileTypes: true })
55-
.filter((entry) => entry.isDirectory());
56-
57-
for (const dir of dirs) {
58-
const dirPath = path.join(changelogUnreleasedDir, dir.name);
58+
for (const dir of changelogUnreleasedDirs) {
59+
const dirPath = path.join(changelogUnreleasedDirPath, dir.name);
5960
const category = categoriesByDir.get(dir.name);
6061

6162
if (!category) {
6263
throw new Error("Unknown category: " + dir.name);
6364
}
6465

65-
category.entries = fs
66-
.readdirSync(dirPath)
67-
.filter((fileName) => /^\d+\.md$/.test(fileName))
68-
.map((fileName) => {
69-
const [title, ...rest] = fs
70-
.readFileSync(path.join(dirPath, fileName), "utf8")
71-
.trim()
72-
.split("\n");
73-
74-
const improvement = title.match(/\[IMPROVEMENT(:(\d+))?]/);
75-
76-
const section = title.includes("[HIGHLIGHT]")
77-
? "highlight"
78-
: title.includes("[BREAKING]")
79-
? "breaking"
80-
: improvement
81-
? "improvement"
82-
: undefined;
83-
84-
const order =
85-
section === "improvement" && improvement[2] !== undefined
86-
? Number(improvement[2])
87-
: undefined;
88-
89-
const content = [processTitle(title), ...rest].join("\n");
90-
91-
return { fileName, section, order, content };
92-
});
66+
category.entries = getEntries(dirPath);
9367
}
9468

9569
rimraf.sync(postGlob);
@@ -100,54 +74,35 @@ fs.writeFileSync(
10074
[
10175
fs.readFileSync(introFile, "utf8").trim(),
10276
"<!--truncate-->",
103-
...printEntries({
77+
...printEntriesWithTitle({
10478
title: "Highlights",
10579
filter: (entry) => entry.section === "highlight",
10680
}),
107-
...printEntries({
81+
...printEntriesWithTitle({
10882
title: "Breaking Changes",
10983
filter: (entry) => entry.section === "breaking",
11084
}),
111-
...printEntries({
85+
...printEntriesWithTitle({
11286
title: "Formatting Improvements",
11387
filter: (entry) => entry.section === "improvement",
11488
}),
115-
...printEntries({
89+
...printEntriesWithTitle({
11690
title: "Other Changes",
11791
filter: (entry) => !entry.section,
11892
}),
119-
].join("\n\n") + "\n"
93+
].join("\n\n") + "\n",
94+
previousVersion,
95+
version
12096
)
12197
);
12298

123-
function processTitle(title) {
124-
return title
125-
.replace(/\[(BREAKING|HIGHLIGHT|IMPROVEMENT(:\d+)?)]/g, "")
126-
.replace(/\s+/g, " ")
127-
.replace(/^#{4} [a-z]/, (s) => s.toUpperCase())
128-
.replace(/(?<![[`])@([\w-]+)/g, "[@$1](https://github.com/$1)")
129-
.replace(
130-
/(?<![[`])#(\d{4,})/g,
131-
"[#$1](https://github.com/prettier/prettier/pull/$1)"
132-
);
133-
}
134-
135-
function printEntries({ title, filter }) {
99+
function printEntriesWithTitle({ title, filter }) {
136100
const result = [];
137101

138102
for (const { entries = [], title } of categories) {
139103
const filteredEntries = entries.filter(filter);
140104
if (filteredEntries.length > 0) {
141-
filteredEntries.sort((a, b) => {
142-
if (a.order !== undefined) {
143-
return b.order === undefined ? 1 : a.order - b.order;
144-
}
145-
return a.fileName.localeCompare(b.fileName, "en", { numeric: true });
146-
});
147-
result.push(
148-
"### " + title,
149-
...filteredEntries.map((entry) => entry.content)
150-
);
105+
result.push("###" + title, ...printEntries(filteredEntries));
151106
}
152107
}
153108

@@ -157,13 +112,3 @@ function printEntries({ title, filter }) {
157112

158113
return result;
159114
}
160-
161-
function formatVersion(version) {
162-
return `${semver.major(version)}.${semver.minor(version)}`;
163-
}
164-
165-
function replaceVersions(data) {
166-
return data
167-
.replace(/prettier stable/gi, `Prettier ${formatVersion(previousVersion)}`)
168-
.replace(/prettier main/gi, `Prettier ${formatVersion(version)}`);
169-
}

scripts/release/steps/update-changelog.js

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const fs = require("fs");
4+
const execa = require("execa");
45
const chalk = require("chalk");
56
const { outdent, string: outdentString } = require("outdent");
67
const semver = require("semver");
@@ -18,18 +19,29 @@ function getBlogPostInfo(version) {
1819
};
1920
}
2021

21-
function writeChangelog({ version, previousVersion, releaseNotes }) {
22+
function writeChangelog({ version, previousVersion, body }) {
2223
const changelog = fs.readFileSync("CHANGELOG.md", "utf-8");
2324
const newEntry = outdent`
2425
# ${version}
2526
2627
[diff](https://github.com/prettier/prettier/compare/${previousVersion}...${version})
2728
28-
${releaseNotes}
29+
${body}
2930
`;
3031
fs.writeFileSync("CHANGELOG.md", newEntry + "\n\n" + changelog);
3132
}
3233

34+
async function getChangelogForPatch({ version, previousVersion }) {
35+
const { stdout: changelog } = await execa("node", [
36+
"scripts/changelog-for-patch.mjs",
37+
"--prev-version",
38+
previousVersion,
39+
"--new-version",
40+
version,
41+
]);
42+
return changelog;
43+
}
44+
3345
module.exports = async function ({ version, previousVersion }) {
3446
const semverDiff = semver.diff(version, previousVersion);
3547

@@ -38,7 +50,7 @@ module.exports = async function ({ version, previousVersion }) {
3850
writeChangelog({
3951
version,
4052
previousVersion,
41-
releaseNotes: `🔗 [Release Notes](https://prettier.io/${blogPost.path})`,
53+
body: `🔗 [Release Notes](https://prettier.io/${blogPost.path})`,
4254
});
4355
if (fs.existsSync(blogPost.file)) {
4456
// Everything is fine, this step is finished
@@ -52,18 +64,16 @@ module.exports = async function ({ version, previousVersion }) {
5264
`)
5365
);
5466
} else {
55-
console.log(
56-
outdentString(chalk`
57-
{yellow.bold A manual step is necessary.}
58-
59-
You can copy the entries from {bold changelog_unreleased/*/*.md} to {bold CHANGELOG.md}
60-
and update it accordingly.
61-
62-
You don't need to commit the file, the script will take care of that.
63-
64-
When you're finished, press ENTER to continue.
65-
`)
66-
);
67+
const body = await getChangelogForPatch({
68+
version,
69+
previousVersion,
70+
});
71+
writeChangelog({
72+
version,
73+
previousVersion,
74+
body,
75+
});
76+
console.log("Press ENTER to continue.");
6777
}
6878

6979
await waitForEnter();

scripts/utils/changelog.mjs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import createEsmUtils from "esm-utils";
4+
import semver from "semver";
5+
6+
const { __dirname } = createEsmUtils(import.meta);
7+
8+
export const changelogUnreleasedDirPath = path.join(
9+
__dirname,
10+
"../../changelog_unreleased"
11+
);
12+
13+
export const changelogUnreleasedDirs = fs
14+
.readdirSync(changelogUnreleasedDirPath, {
15+
withFileTypes: true,
16+
})
17+
.filter((entry) => entry.isDirectory());
18+
19+
export function getEntries(dirPath) {
20+
const fileNames = fs
21+
.readdirSync(dirPath)
22+
.filter((fileName) => path.extname(fileName) === ".md");
23+
const entries = fileNames.map((fileName) => {
24+
const [title, ...rest] = fs
25+
.readFileSync(path.join(dirPath, fileName), "utf8")
26+
.trim()
27+
.split("\n");
28+
29+
const improvement = title.match(/\[IMPROVEMENT(:(\d+))?]/);
30+
31+
const section = title.includes("[HIGHLIGHT]")
32+
? "highlight"
33+
: title.includes("[BREAKING]")
34+
? "breaking"
35+
: improvement
36+
? "improvement"
37+
: undefined;
38+
39+
const order =
40+
section === "improvement" && improvement[2] !== undefined
41+
? Number(improvement[2])
42+
: undefined;
43+
44+
const content = [processTitle(title), ...rest].join("\n");
45+
46+
return { fileName, section, order, content };
47+
});
48+
return entries;
49+
}
50+
51+
export function printEntries(entries) {
52+
const result = [];
53+
if (entries.length > 0) {
54+
entries.sort((a, b) => {
55+
if (a.order !== undefined) {
56+
return b.order === undefined ? 1 : a.order - b.order;
57+
}
58+
return a.fileName.localeCompare(b.fileName, "en", { numeric: true });
59+
});
60+
result.push(...entries.map((entry) => entry.content));
61+
}
62+
return result;
63+
}
64+
65+
export function replaceVersions(data, prevVer, newVer, isPatch = false) {
66+
return data
67+
.replace(
68+
/prettier stable/gi,
69+
`Prettier ${isPatch ? prevVer : formatVersion(prevVer)}`
70+
)
71+
.replace(
72+
/prettier main/gi,
73+
`Prettier ${isPatch ? newVer : formatVersion(newVer)}`
74+
);
75+
}
76+
77+
function formatVersion(version) {
78+
return `${semver.major(version)}.${semver.minor(version)}`;
79+
}
80+
81+
function processTitle(title) {
82+
return title
83+
.replace(/\[(BREAKING|HIGHLIGHT|IMPROVEMENT(:\d+)?)]/g, "")
84+
.replace(/\s+/g, " ")
85+
.replace(/^#{4} [a-z]/, (s) => s.toUpperCase())
86+
.replace(/(?<![[`])@([\w-]+)/g, "[@$1](https://github.com/$1)")
87+
.replace(
88+
/(?<![[`])#(\d{4,})/g,
89+
"[#$1](https://github.com/prettier/prettier/pull/$1)"
90+
);
91+
}

0 commit comments

Comments
 (0)