From a6e7fc3b1824ba06c11d6fe56e1e000286e7abe7 Mon Sep 17 00:00:00 2001 From: Marika Marszalkowski Date: Fri, 13 Sep 2024 13:52:44 +0200 Subject: [PATCH] chore: improve changeset handling --- .github/actions/merge-changelogs/index.js | 275 ++++++++++++---------- build-packages/merge-changelogs/index.ts | 136 ++++++----- scripts/write-changelog.ts | 1 + 3 files changed, 217 insertions(+), 195 deletions(-) diff --git a/.github/actions/merge-changelogs/index.js b/.github/actions/merge-changelogs/index.js index fc2d178b78..548011672c 100644 --- a/.github/actions/merge-changelogs/index.js +++ b/.github/actions/merge-changelogs/index.js @@ -1,129 +1,6 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ -/***/ 7587: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - -Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.validMessageTypes = void 0; -exports.mergeChangelogs = mergeChangelogs; -/* eslint-disable jsdoc/require-jsdoc */ -const promises_1 = __nccwpck_require__(3977); -const node_path_1 = __nccwpck_require__(9411); -const core_1 = __nccwpck_require__(7117); -const get_packages_1 = __nccwpck_require__(512); -exports.validMessageTypes = [ - 'Known Issue', - 'Compatibility Note', - 'New Functionality', - 'Improvement', - 'Fixed Issue', - 'Updated Dependencies' -]; -function getPackageName(changelog) { - return changelog.split('\n')[0].split('/')[1]; -} -function splitByVersion(changelog) { - return changelog - .split('\n## ') - .slice(1) - .map(h2 => { - const [version, ...content] = h2.split('\n'); - return { - version, - content: content.join('\n').trim() - }; - }); -} -function assertGroups(groups, packageName, version) { - // TODO: improve type checking, currently you have to match a long string, allow shortcuts - if (!exports.validMessageTypes.includes(groups?.type)) { - (0, core_1.error)(groups?.type - ? `Error: Type [${groups?.type}] is not valid (${groups?.commit})` - : `Error: No type was provided for "${groups?.summary} (${groups?.commit})"`); - throw new Error(`Incorrect or missing type in CHANGELOG.md in ${packageName} for v${version}!`); - } - if (typeof groups?.summary !== 'string' || groups?.summary.trim() === '') { - (0, core_1.error)(`Error: Empty or missing summary in CHANGELOG.md in ${packageName} for v${version}! (${groups?.commit})`); - throw new Error(`Empty or missing summary in CHANGELOG.md in ${packageName} for v${version}!`); - } -} -function parseContent(content, version, packageName) { - // Explanation: https://regex101.com/r/ikvIaa/2 - const contentRegex = /- ((?.*):) (\[(?.*?)\])? ?(?[^]*?)(?=(\n- |\n### |$))/g; - return [...content.matchAll(contentRegex)].map(({ groups }) => { - assertGroups(groups, packageName, version); - return { - version, - summary: groups.summary.trim(), - packageNames: [packageName], - // TODO: add link to commit - commit: groups.commit ? `(${groups.commit})` : '', - type: groups.type - }; - }); -} -function parseChangelog(changelog) { - const packageName = getPackageName(changelog); - const [latest] = splitByVersion(changelog); - return parseContent(latest.content, latest.version, packageName).flat(); -} -function formatMessagesOfType(messages, type) { - if (!messages.some(msg => msg.type === type)) { - return ''; - } - const formatted = messages - .filter(msg => msg.type === type) - .map(msg => `- [${msg.packageNames.join(', ')}] ${msg.summary} ${msg.commit}${msg.dependencies || ''}`) - .join('\n'); - // TODO: this is ugly, e.g. there is no plural for "New Functionality" => use type to title mapping instead - const pluralizedType = type.slice(-1) === 'y' ? type.slice(0, -1) + 'ies' : type + 's'; - return `\n\n## ${pluralizedType}\n\n${formatted}`; -} -function mergeMessages(parsedMessages) { - return parsedMessages.reduce((prev, curr) => { - const sameMessage = prev.find(msg => msg.summary === curr.summary && - msg.dependencies === curr.dependencies && - msg.version === curr.version && - msg.type === curr.type); - if (sameMessage) { - sameMessage.packageNames.push(curr.packageNames[0]); - return prev; - } - return [...prev, curr]; - }, []); -} -async function formatChangelog(messages) { - if (!messages.length) { - throw new Error('No messages found in changelogs'); - } - return (formatMessagesOfType(messages, 'Known Issue') + - formatMessagesOfType(messages, 'Compatibility Note') + - formatMessagesOfType(messages, 'New Functionality') + - formatMessagesOfType(messages, 'Improvement') + - formatMessagesOfType(messages, 'Fixed Issue') + - formatMessagesOfType(messages, 'Updated Dependencies') + - '\n\n'); -} -async function mergeChangelogs() { - const { packages } = await (0, get_packages_1.getPackages)(process.cwd()); - const pathsToPublicLogs = packages - .filter(({ packageJson }) => !packageJson.private) - .map(({ relativeDir }) => (0, node_path_1.resolve)(relativeDir, 'CHANGELOG.md')); - (0, core_1.info)(`changelogs to merge: ${pathsToPublicLogs.join(', ')}`); - const changelogs = await Promise.all(pathsToPublicLogs.map(async (file) => (0, promises_1.readFile)(file, { encoding: 'utf8' }))); - const newChangelog = await formatChangelog(mergeMessages(changelogs.map(log => parseChangelog(log)).flat())); - (0, core_1.setOutput)('changelog', newChangelog); -} -mergeChangelogs().catch(error => { - (0, core_1.setFailed)(error.message); -}); - - -/***/ }), - /***/ 3145: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { @@ -41922,12 +41799,150 @@ module.exports = parseParams /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; /******/ /************************************************************************/ -/******/ -/******/ // startup -/******/ // Load entry module and return exports -/******/ // This entry module is referenced by other modules so it can't be inlined -/******/ var __webpack_exports__ = __nccwpck_require__(7587); -/******/ module.exports = __webpack_exports__; -/******/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be in strict mode. +(() => { +"use strict"; +var exports = __webpack_exports__; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.mergeChangelogs = mergeChangelogs; +/* eslint-disable jsdoc/require-jsdoc */ +const promises_1 = __nccwpck_require__(3977); +const node_path_1 = __nccwpck_require__(9411); +const core_1 = __nccwpck_require__(7117); +const get_packages_1 = __nccwpck_require__(512); +const messageTypes = [ + { + name: 'compat', + title: 'Compatibility Notes', + alternatives: ['compatibility', 'compatibility note'] + }, + { + name: 'feat', + title: 'New Features', + alternatives: ['new', 'new functionality'] + }, + { + name: 'fix', + title: 'Fixed Issues', + alternatives: ['bug', 'bug fix', 'fixed issue'] + }, + { + name: 'impr', + title: 'Improvements', + alternatives: ['improvement', 'improv'] + }, + { + name: 'dep', + title: 'Updated Dependencies', + alternatives: ['dependency', 'dependency update'] + } +]; +function getPackageName(changelog) { + return changelog.split('\n')[0].split('/')[1]; +} +function splitByVersion(changelog) { + return changelog + .split('\n## ') + .slice(1) + .map(h2 => { + const [version, ...content] = h2.split('\n'); + return { + version, + content: content.join('\n').trim() + }; + }); +} +function getMessageType(matchedType) { + if (!matchedType) { + throw new Error(`Missing message type`); + } + const type = messageTypes.find(({ name, alternatives }) => [name, ...alternatives].includes(matchedType)); + if (!type) { + throw new Error(`Invalid message type: ${matchedType}`); + } + return type; +} +function getSummary(matchedSummary) { + const summary = matchedSummary?.trim(); + if (!summary) { + throw new Error(`Empty or missing summary`); + } + return summary; +} +function parseContent(content, version, packageName) { + // Explanation: https://regex101.com/r/ikvIaa/2 + const contentRegex = /- ((?.*):) (\[(?.*?)\])? ?(?[^]*?)(?=(\n- |\n### |$))/g; + (0, core_1.info)(`parsing content for ${packageName} v${version}`); + return [...content.matchAll(contentRegex)].map(({ groups }) => { + const type = getMessageType(groups?.type); + const summary = getSummary(groups?.summary); + return { + version, + summary, + packageNames: [packageName], + // TODO: add link to commit + commit: groups.commit ? `(${groups.commit})` : '', + type + }; + }); +} +function parseChangelog(changelog) { + const packageName = getPackageName(changelog); + const [latest] = splitByVersion(changelog); + return parseContent(latest.content, latest.version, packageName).flat(); +} +function formatMessagesOfType(messages, type) { + if (!messages.some(msg => msg.type.name === type.name)) { + return ''; + } + const formattedMessages = messages + .filter(msg => msg.type.name === type.name) + .map(msg => `- [${msg.packageNames.join(', ')}] ${msg.summary} ${msg.commit}${msg.dependencies || ''}`) + .join('\n'); + return `## ${type.title}\n\n${formattedMessages}`; +} +function mergeMessages(parsedMessages) { + return parsedMessages.reduce((prev, curr) => { + const sameMessage = prev.find(msg => msg.summary === curr.summary && + msg.dependencies === curr.dependencies && + msg.version === curr.version && + msg.type.name === curr.type.name); + if (sameMessage) { + sameMessage.packageNames.push(curr.packageNames[0]); + return prev; + } + return [...prev, curr]; + }, []); +} +async function formatChangelog(messages) { + if (!messages.length) { + throw new Error('No messages found in changelogs'); + } + return messageTypes + .map(type => formatMessagesOfType(messages, type)) + .join('\n\n'); +} +async function getPublicChangelogs() { + const { packages } = await (0, get_packages_1.getPackages)(process.cwd()); + const pathsToPublicLogs = packages + .filter(({ packageJson }) => !packageJson.private) + .map(({ relativeDir }) => (0, node_path_1.resolve)(relativeDir, 'CHANGELOG.md')); + (0, core_1.info)(`changelogs to merge: ${pathsToPublicLogs.join(', ')}`); + return Promise.all(pathsToPublicLogs.map(async (file) => (0, promises_1.readFile)(file, { encoding: 'utf8' }))); +} +async function mergeChangelogs() { + const changelogs = await getPublicChangelogs(); + const mergedChangelog = await formatChangelog(mergeMessages(changelogs.map(log => parseChangelog(log)).flat())); + (0, core_1.setOutput)('changelog', mergedChangelog); +} +mergeChangelogs().catch(error => { + (0, core_1.setFailed)(error.message); +}); + +})(); + +module.exports = __webpack_exports__; /******/ })() ; \ No newline at end of file diff --git a/build-packages/merge-changelogs/index.ts b/build-packages/merge-changelogs/index.ts index b31bf984c3..fe99779d68 100644 --- a/build-packages/merge-changelogs/index.ts +++ b/build-packages/merge-changelogs/index.ts @@ -1,22 +1,43 @@ /* eslint-disable jsdoc/require-jsdoc */ import { readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; -import { setOutput, error, setFailed, info } from '@actions/core'; +import { setOutput, setFailed, info } from '@actions/core'; import { getPackages } from '@manypkg/get-packages'; -export const validMessageTypes = [ - 'Known Issue', - 'Compatibility Note', - 'New Functionality', - 'Improvement', - 'Fixed Issue', - 'Updated Dependencies' -] as const; +const messageTypes = [ + { + name: 'compat', + title: 'Compatibility Notes', + alternatives: ['compatibility', 'compatibility note'] + }, + { + name: 'feat', + title: 'New Features', + alternatives: ['new', 'new functionality'] + }, + { + name: 'fix', + title: 'Fixed Issues', + alternatives: ['bug', 'bug fix', 'fixed issue'] + }, + { + name: 'impr', + title: 'Improvements', + alternatives: ['improvement', 'improv'] + }, + { + name: 'dep', + title: 'Updated Dependencies', + alternatives: ['dependency', 'dependency update'] + } +]; + +type MessageType = (typeof messageTypes)[number]; interface Change { packageNames: string[]; commit: string; - type: (typeof validMessageTypes)[number]; + type: MessageType; version: string; summary: string; dependencies?: string; @@ -44,34 +65,25 @@ function splitByVersion(changelog: string): ContentByVersion[] { }); } -function assertGroups( - groups: RegExpMatchArray['groups'] | undefined, - packageName: string, - version: string -): asserts groups is { - summary: string; - commit?: string; - type: (typeof validMessageTypes)[number]; -} { - // TODO: improve type checking, currently you have to match a long string, allow shortcuts - if (!validMessageTypes.includes(groups?.type as any)) { - error( - groups?.type - ? `Error: Type [${groups?.type}] is not valid (${groups?.commit})` - : `Error: No type was provided for "${groups?.summary} (${groups?.commit})"` - ); - throw new Error( - `Incorrect or missing type in CHANGELOG.md in ${packageName} for v${version}!` - ); +function getMessageType(matchedType: string | undefined): MessageType { + if (!matchedType) { + throw new Error(`Missing message type`); } - if (typeof groups?.summary !== 'string' || groups?.summary.trim() === '') { - error( - `Error: Empty or missing summary in CHANGELOG.md in ${packageName} for v${version}! (${groups?.commit})` - ); - throw new Error( - `Empty or missing summary in CHANGELOG.md in ${packageName} for v${version}!` - ); + const type = messageTypes.find(({ name, alternatives }) => + [name, ...alternatives].includes(matchedType) + ); + if (!type) { + throw new Error(`Invalid message type: ${matchedType}`); + } + return type; +} + +function getSummary(matchedSummary: string | undefined): string { + const summary = matchedSummary?.trim(); + if (!summary) { + throw new Error(`Empty or missing summary`); } + return summary; } function parseContent( @@ -83,15 +95,18 @@ function parseContent( const contentRegex = /- ((?.*):) (\[(?.*?)\])? ?(?[^]*?)(?=(\n- |\n### |$))/g; + info(`parsing content for ${packageName} v${version}`); + return [...content.matchAll(contentRegex)].map(({ groups }) => { - assertGroups(groups, packageName, version); + const type = getMessageType(groups?.type); + const summary = getSummary(groups?.summary); return { version, - summary: groups.summary.trim(), + summary, packageNames: [packageName], // TODO: add link to commit commit: groups.commit ? `(${groups.commit})` : '', - type: groups.type + type }; }); } @@ -102,15 +117,13 @@ function parseChangelog(changelog: string): Change[] { return parseContent(latest.content, latest.version, packageName).flat(); } -function formatMessagesOfType( - messages: Change[], - type: (typeof validMessageTypes)[number] -): string { - if (!messages.some(msg => msg.type === type)) { +function formatMessagesOfType(messages: Change[], type: MessageType): string { + if (!messages.some(msg => msg.type.name === type.name)) { return ''; } - const formatted = messages - .filter(msg => msg.type === type) + + const formattedMessages = messages + .filter(msg => msg.type.name === type.name) .map( msg => `- [${msg.packageNames.join(', ')}] ${msg.summary} ${msg.commit}${ @@ -119,10 +132,7 @@ function formatMessagesOfType( ) .join('\n'); - // TODO: this is ugly, e.g. there is no plural for "New Functionality" => use type to title mapping instead - const pluralizedType = - type.slice(-1) === 'y' ? type.slice(0, -1) + 'ies' : type + 's'; - return `\n\n## ${pluralizedType}\n\n${formatted}`; + return `## ${type.title}\n\n${formattedMessages}`; } function mergeMessages(parsedMessages: Change[]): Change[] { @@ -132,7 +142,7 @@ function mergeMessages(parsedMessages: Change[]): Change[] { msg.summary === curr.summary && msg.dependencies === curr.dependencies && msg.version === curr.version && - msg.type === curr.type + msg.type.name === curr.type.name ); if (sameMessage) { sameMessage.packageNames.push(curr.packageNames[0]); @@ -146,19 +156,12 @@ async function formatChangelog(messages: Change[]): Promise { if (!messages.length) { throw new Error('No messages found in changelogs'); } - - return ( - formatMessagesOfType(messages, 'Known Issue') + - formatMessagesOfType(messages, 'Compatibility Note') + - formatMessagesOfType(messages, 'New Functionality') + - formatMessagesOfType(messages, 'Improvement') + - formatMessagesOfType(messages, 'Fixed Issue') + - formatMessagesOfType(messages, 'Updated Dependencies') + - '\n\n' - ); + return messageTypes + .map(type => formatMessagesOfType(messages, type)) + .join('\n\n'); } -export async function mergeChangelogs(): Promise { +async function getPublicChangelogs() { const { packages } = await getPackages(process.cwd()); const pathsToPublicLogs = packages .filter(({ packageJson }) => !packageJson.private) @@ -166,14 +169,17 @@ export async function mergeChangelogs(): Promise { info(`changelogs to merge: ${pathsToPublicLogs.join(', ')}`); - const changelogs = await Promise.all( + return Promise.all( pathsToPublicLogs.map(async file => readFile(file, { encoding: 'utf8' })) ); +} - const newChangelog = await formatChangelog( +export async function mergeChangelogs(): Promise { + const changelogs = await getPublicChangelogs(); + const mergedChangelog = await formatChangelog( mergeMessages(changelogs.map(log => parseChangelog(log)).flat()) ); - setOutput('changelog', newChangelog); + setOutput('changelog', mergedChangelog); } mergeChangelogs().catch(error => { diff --git a/scripts/write-changelog.ts b/scripts/write-changelog.ts index 8411763213..f7efad58dc 100644 --- a/scripts/write-changelog.ts +++ b/scripts/write-changelog.ts @@ -15,6 +15,7 @@ async function writeChangelog(): Promise { `# ${process.env.VERSION}` + '\n' + process.env.CHANGELOG + + '\n\n' + unifiedChangelog.split('\n').slice(30).join('\n'), { encoding: 'utf8' } );