Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a script that generates changelog for patch release #11052

Merged
merged 1 commit into from Jun 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions scripts/changelog-for-patch.mjs
@@ -0,0 +1,44 @@
#!/usr/bin/env node

import path from "node:path";
import minimist from "minimist";
import semver from "semver";
import {
changelogUnreleasedDirPath,
changelogUnreleasedDirs,
getEntries,
printEntries,
replaceVersions,
} from "./utils/changelog.mjs";

const { previousVersion, newVersion } = parseArgv();

const entries = changelogUnreleasedDirs.flatMap((dir) => {
const dirPath = path.join(changelogUnreleasedDirPath, dir.name);
return getEntries(dirPath);
});

console.log(
replaceVersions(
printEntries(entries).join("\n\n"),
previousVersion,
newVersion,
/** isPatch */ true
)
);

function parseArgv() {
const argv = minimist(process.argv.slice(2));
const previousVersion = argv["prev-version"];
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't previous version always require("prettier").version?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's correct in most cases. However, the release script actually get the previous version in a different way(ref:

const { stdout: previousVersion } = await runGit([
"describe",
"--tags",
"--abbrev=0",
]);
). So I think it is safer to be able to specify it as an argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fisker What do you think? I want to merge this for next patch version.

const newVersion = argv["new-version"];
if (
!previousVersion ||
!newVersion ||
semver.compare(previousVersion, newVersion) !== -1
) {
throw new Error(
`Invalid argv, prev-version: ${previousVersion}, new-version: ${newVersion}`
);
}
return { previousVersion, newVersion };
}
97 changes: 21 additions & 76 deletions scripts/draft-blog-post.mjs
Expand Up @@ -3,17 +3,22 @@
import fs from "node:fs";
import path from "node:path";
import rimraf from "rimraf";
import semver from "semver";
import createEsmUtils from "esm-utils";
import {
getEntries,
replaceVersions,
changelogUnreleasedDirPath,
changelogUnreleasedDirs,
printEntries,
} from "./utils/changelog.mjs";

const { __dirname, require } = createEsmUtils(import.meta);
const changelogUnreleasedDir = path.join(__dirname, "../changelog_unreleased");
const blogDir = path.join(__dirname, "../website/blog");
const introTemplateFile = path.join(
changelogUnreleasedDir,
changelogUnreleasedDirPath,
"BLOG_POST_INTRO_TEMPLATE.md"
);
const introFile = path.join(changelogUnreleasedDir, "blog-post-intro.md");
const introFile = path.join(changelogUnreleasedDirPath, "blog-post-intro.md");
if (!fs.existsSync(introFile)) {
fs.copyFileSync(introTemplateFile, introFile);
}
Expand Down Expand Up @@ -50,46 +55,15 @@ const categoriesByDir = new Map(
categories.map((category) => [category.dir, category])
);

const dirs = fs
.readdirSync(changelogUnreleasedDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory());

for (const dir of dirs) {
const dirPath = path.join(changelogUnreleasedDir, dir.name);
for (const dir of changelogUnreleasedDirs) {
const dirPath = path.join(changelogUnreleasedDirPath, dir.name);
const category = categoriesByDir.get(dir.name);

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

category.entries = fs
.readdirSync(dirPath)
.filter((fileName) => /^\d+\.md$/.test(fileName))
.map((fileName) => {
const [title, ...rest] = fs
.readFileSync(path.join(dirPath, fileName), "utf8")
.trim()
.split("\n");

const improvement = title.match(/\[IMPROVEMENT(:(\d+))?]/);

const section = title.includes("[HIGHLIGHT]")
? "highlight"
: title.includes("[BREAKING]")
? "breaking"
: improvement
? "improvement"
: undefined;

const order =
section === "improvement" && improvement[2] !== undefined
? Number(improvement[2])
: undefined;

const content = [processTitle(title), ...rest].join("\n");

return { fileName, section, order, content };
});
category.entries = getEntries(dirPath);
}

rimraf.sync(postGlob);
Expand All @@ -100,54 +74,35 @@ fs.writeFileSync(
[
fs.readFileSync(introFile, "utf8").trim(),
"<!--truncate-->",
...printEntries({
...printEntriesWithTitle({
title: "Highlights",
filter: (entry) => entry.section === "highlight",
}),
...printEntries({
...printEntriesWithTitle({
title: "Breaking Changes",
filter: (entry) => entry.section === "breaking",
}),
...printEntries({
...printEntriesWithTitle({
title: "Formatting Improvements",
filter: (entry) => entry.section === "improvement",
}),
...printEntries({
...printEntriesWithTitle({
title: "Other Changes",
filter: (entry) => !entry.section,
}),
].join("\n\n") + "\n"
].join("\n\n") + "\n",
previousVersion,
version
)
);

function processTitle(title) {
return title
.replace(/\[(BREAKING|HIGHLIGHT|IMPROVEMENT(:\d+)?)]/g, "")
.replace(/\s+/g, " ")
.replace(/^#{4} [a-z]/, (s) => s.toUpperCase())
.replace(/(?<![[`])@([\w-]+)/g, "[@$1](https://github.com/$1)")
.replace(
/(?<![[`])#(\d{4,})/g,
"[#$1](https://github.com/prettier/prettier/pull/$1)"
);
}

function printEntries({ title, filter }) {
function printEntriesWithTitle({ title, filter }) {
const result = [];

for (const { entries = [], title } of categories) {
const filteredEntries = entries.filter(filter);
if (filteredEntries.length > 0) {
filteredEntries.sort((a, b) => {
if (a.order !== undefined) {
return b.order === undefined ? 1 : a.order - b.order;
}
return a.fileName.localeCompare(b.fileName, "en", { numeric: true });
});
result.push(
"### " + title,
...filteredEntries.map((entry) => entry.content)
);
result.push("###" + title, ...printEntries(filteredEntries));
}
}

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

return result;
}

function formatVersion(version) {
return `${semver.major(version)}.${semver.minor(version)}`;
}

function replaceVersions(data) {
return data
.replace(/prettier stable/gi, `Prettier ${formatVersion(previousVersion)}`)
.replace(/prettier main/gi, `Prettier ${formatVersion(version)}`);
}
40 changes: 25 additions & 15 deletions scripts/release/steps/update-changelog.js
@@ -1,6 +1,7 @@
"use strict";

const fs = require("fs");
const execa = require("execa");
const chalk = require("chalk");
const { outdent, string: outdentString } = require("outdent");
const semver = require("semver");
Expand All @@ -18,18 +19,29 @@ function getBlogPostInfo(version) {
};
}

function writeChangelog({ version, previousVersion, releaseNotes }) {
function writeChangelog({ version, previousVersion, body }) {
const changelog = fs.readFileSync("CHANGELOG.md", "utf-8");
const newEntry = outdent`
# ${version}
[diff](https://github.com/prettier/prettier/compare/${previousVersion}...${version})
${releaseNotes}
${body}
`;
fs.writeFileSync("CHANGELOG.md", newEntry + "\n\n" + changelog);
}

async function getChangelogForPatch({ version, previousVersion }) {
const { stdout: changelog } = await execa("node", [
"scripts/changelog-for-patch.mjs",
"--prev-version",
previousVersion,
"--new-version",
version,
]);
return changelog;
}

module.exports = async function ({ version, previousVersion }) {
const semverDiff = semver.diff(version, previousVersion);

Expand All @@ -38,7 +50,7 @@ module.exports = async function ({ version, previousVersion }) {
writeChangelog({
version,
previousVersion,
releaseNotes: `🔗 [Release Notes](https://prettier.io/${blogPost.path})`,
body: `🔗 [Release Notes](https://prettier.io/${blogPost.path})`,
});
if (fs.existsSync(blogPost.file)) {
// Everything is fine, this step is finished
Expand All @@ -52,18 +64,16 @@ module.exports = async function ({ version, previousVersion }) {
`)
);
} else {
console.log(
outdentString(chalk`
{yellow.bold A manual step is necessary.}
You can copy the entries from {bold changelog_unreleased/*/*.md} to {bold CHANGELOG.md}
and update it accordingly.
You don't need to commit the file, the script will take care of that.
When you're finished, press ENTER to continue.
`)
);
const body = await getChangelogForPatch({
version,
previousVersion,
});
writeChangelog({
version,
previousVersion,
body,
});
console.log("Press ENTER to continue.");
}

await waitForEnter();
Expand Down
91 changes: 91 additions & 0 deletions scripts/utils/changelog.mjs
@@ -0,0 +1,91 @@
import fs from "node:fs";
import path from "node:path";
import createEsmUtils from "esm-utils";
import semver from "semver";

const { __dirname } = createEsmUtils(import.meta);

export const changelogUnreleasedDirPath = path.join(
__dirname,
"../../changelog_unreleased"
);

export const changelogUnreleasedDirs = fs
.readdirSync(changelogUnreleasedDirPath, {
withFileTypes: true,
})
.filter((entry) => entry.isDirectory());

export function getEntries(dirPath) {
const fileNames = fs
.readdirSync(dirPath)
.filter((fileName) => path.extname(fileName) === ".md");
const entries = fileNames.map((fileName) => {
const [title, ...rest] = fs
.readFileSync(path.join(dirPath, fileName), "utf8")
.trim()
.split("\n");

const improvement = title.match(/\[IMPROVEMENT(:(\d+))?]/);

const section = title.includes("[HIGHLIGHT]")
? "highlight"
: title.includes("[BREAKING]")
? "breaking"
: improvement
? "improvement"
: undefined;

const order =
section === "improvement" && improvement[2] !== undefined
? Number(improvement[2])
: undefined;

const content = [processTitle(title), ...rest].join("\n");

return { fileName, section, order, content };
});
return entries;
}

export function printEntries(entries) {
const result = [];
if (entries.length > 0) {
entries.sort((a, b) => {
if (a.order !== undefined) {
return b.order === undefined ? 1 : a.order - b.order;
}
return a.fileName.localeCompare(b.fileName, "en", { numeric: true });
});
result.push(...entries.map((entry) => entry.content));
}
return result;
}

export function replaceVersions(data, prevVer, newVer, isPatch = false) {
return data
.replace(
/prettier stable/gi,
`Prettier ${isPatch ? prevVer : formatVersion(prevVer)}`
)
.replace(
/prettier main/gi,
`Prettier ${isPatch ? newVer : formatVersion(newVer)}`
);
}

function formatVersion(version) {
return `${semver.major(version)}.${semver.minor(version)}`;
}

function processTitle(title) {
return title
.replace(/\[(BREAKING|HIGHLIGHT|IMPROVEMENT(:\d+)?)]/g, "")
.replace(/\s+/g, " ")
.replace(/^#{4} [a-z]/, (s) => s.toUpperCase())
.replace(/(?<![[`])@([\w-]+)/g, "[@$1](https://github.com/$1)")
.replace(
/(?<![[`])#(\d{4,})/g,
"[#$1](https://github.com/prettier/prettier/pull/$1)"
);
}