diff --git a/development/auto-changelog.js b/development/auto-changelog.js index 8a6d8f08a..455bf5beb 100755 --- a/development/auto-changelog.js +++ b/development/auto-changelog.js @@ -1,25 +1,27 @@ #!/usr/bin/env node const fs = require('fs').promises; -const assert = require('assert').strict; -const path = require('path'); -const { escapeRegExp } = require('lodash'); -const { version } = require('../app/manifest/_base.json'); -const runCommand = require('./lib/runCommand'); -const URL = 'https://github.com/MetaMask/metamask-extension'; +const path = require('path'); +const { version } = require('../app/manifest/_base.json'); +const { updateChangelog } = require('./lib/changelog/updateChangelog'); +const { unreleased } = require('./lib/changelog/constants'); + +const REPO_URL = 'https://github.com/MetaMask/metamask-extension'; const command = 'yarn update-changelog'; const helpText = `Usage: ${command} [--rc] [-h|--help] Update CHANGELOG.md with any changes made since the most recent release. + Options: --rc Add new changes to the current release header, rather than to the - 'Unreleased' section. + '${unreleased}' section. -h, --help Display this help and exit. -New commits will be added to the "Unreleased" section (or to the section for the +New commits will be added to the "${unreleased}" section (or to the section for the current release if the '--rc' flag is used) in reverse chronological order. Any commits for PRs that are represented already in the changelog will be ignored. + If the '--rc' flag is used and the section for the current release does not yet exist, it will be created. `; @@ -42,158 +44,19 @@ async function main() { } } - await runCommand('git', ['fetch', '--tags']); - - const [mostRecentTagCommitHash] = await runCommand('git', [ - 'rev-list', - '--tags', - '--max-count=1', - ]); - const [mostRecentTag] = await runCommand('git', [ - 'describe', - '--tags', - mostRecentTagCommitHash, - ]); - assert.equal(mostRecentTag[0], 'v', 'Most recent tag should start with v'); - - const commitsSinceLastRelease = await runCommand('git', [ - 'rev-list', - `${mostRecentTag}..HEAD`, - ]); - - const commitEntries = []; - for (const commit of commitsSinceLastRelease) { - const [subject] = await runCommand('git', [ - 'show', - '-s', - '--format=%s', - commit, - ]); - - let prNumber; - let description = subject; - - // Squash & Merge: the commit subject is parsed as ` (#)` - if (subject.match(/\(#\d+\)/u)) { - const matchResults = subject.match(/\(#(\d+)\)/u); - prNumber = matchResults[1]; - description = subject.match(/^(.+)\s\(#\d+\)/u)[1]; - // Merge: the PR ID is parsed from the git subject (which is of the form `Merge pull request - // # from `, and the description is assumed to be the first line of the body. - // If no body is found, the description is set to the commit subject - } else if (subject.match(/#\d+\sfrom/u)) { - const matchResults = subject.match(/#(\d+)\sfrom/u); - prNumber = matchResults[1]; - const [firstLineOfBody] = await runCommand('git', [ - 'show', - '-s', - '--format=%b', - commit, - ]); - description = firstLineOfBody || subject; - } - // Otherwise: - // Normal commits: The commit subject is the description, and the PR ID is omitted. - - commitEntries.push({ prNumber, description }); - } - const changelogFilename = path.resolve(__dirname, '..', 'CHANGELOG.md'); - const changelog = await fs.readFile(changelogFilename, { encoding: 'utf8' }); - const changelogLines = changelog.split('\n'); + const changelogContent = await fs.readFile(changelogFilename, { + encoding: 'utf8', + }); - const prNumbersWithChangelogEntries = []; - for (const line of changelogLines) { - const matchResults = line.match(/- \[#(\d+)\]/u); - if (matchResults === null) { - continue; - } - const prNumber = matchResults[1]; - prNumbersWithChangelogEntries.push(prNumber); - } + const newChangelogContent = await updateChangelog({ + changelogContent, + currentVersion: version, + repoUrl: REPO_URL, + isReleaseCandidate, + }); - const changelogEntries = []; - for (const { prNumber, description } of commitEntries) { - if (prNumbersWithChangelogEntries.includes(prNumber)) { - continue; - } - - let changelogEntry; - if (prNumber) { - const prefix = `[#${prNumber}](${URL}/pull/${prNumber})`; - changelogEntry = `- ${prefix}: ${description}`; - } else { - changelogEntry = `- ${description}`; - } - changelogEntries.push(changelogEntry); - } - - if (changelogEntries.length === 0) { - console.log('CHANGELOG required no updates'); - return; - } - - const versionHeader = `## [${version}]`; - const escapedVersionHeader = escapeRegExp(versionHeader); - const currentDevelopBranchHeader = '## [Unreleased]'; - const currentReleaseHeaderPattern = isReleaseCandidate - ? // This ensures this doesn't match on a version with a suffix - // e.g. v9.0.0 should not match on the header v9.0.0-beta.0 - `${escapedVersionHeader}$|${escapedVersionHeader}\\s` - : escapeRegExp(currentDevelopBranchHeader); - - let releaseHeaderIndex = changelogLines.findIndex((line) => - line.match(new RegExp(currentReleaseHeaderPattern, 'u')), - ); - if (releaseHeaderIndex === -1) { - if (!isReleaseCandidate) { - throw new Error( - `Failed to find release header '${currentDevelopBranchHeader}'`, - ); - } - - // Add release header if not found - const firstReleaseHeaderIndex = changelogLines.findIndex((line) => - line.match(/## \[\d+\.\d+\.\d+\]/u), - ); - const [, previousVersion] = changelogLines[firstReleaseHeaderIndex].match( - /## \[(\d+\.\d+\.\d+)\]/u, - ); - changelogLines.splice(firstReleaseHeaderIndex, 0, versionHeader, ''); - releaseHeaderIndex = firstReleaseHeaderIndex; - - // Update release link reference definitions - // A link reference definition is added for the new release, and the - // "Unreleased" header is updated to point at the range of commits merged - // after the most recent release. - const unreleasedLinkIndex = changelogLines.findIndex((line) => - line.match(/\[Unreleased\]:/u), - ); - changelogLines.splice( - unreleasedLinkIndex, - 1, - `[Unreleased]: ${URL}/compare/v${version}...HEAD`, - `[${version}]: ${URL}/compare/v${previousVersion}...v${version}`, - ); - } - - // Ensure "Uncategorized" header is present - const uncategorizedHeaderIndex = releaseHeaderIndex + 1; - const uncategorizedHeader = '### Uncategorized'; - if (changelogLines[uncategorizedHeaderIndex] !== '### Uncategorized') { - const hasEmptyLine = changelogLines[uncategorizedHeaderIndex] === ''; - changelogLines.splice( - uncategorizedHeaderIndex, - 0, - uncategorizedHeader, - // Ensure an empty line follows the new header - ...(hasEmptyLine ? [] : ['']), - ); - } - - changelogLines.splice(uncategorizedHeaderIndex + 1, 0, ...changelogEntries); - const updatedChangelog = changelogLines.join('\n'); - await fs.writeFile(changelogFilename, updatedChangelog); + await fs.writeFile(changelogFilename, newChangelogContent); console.log('CHANGELOG updated'); } diff --git a/development/lib/changelog/changelog.js b/development/lib/changelog/changelog.js new file mode 100644 index 000000000..07c0cd01c --- /dev/null +++ b/development/lib/changelog/changelog.js @@ -0,0 +1,275 @@ +const semver = require('semver'); + +const { orderedChangeCategories, unreleased } = require('./constants'); + +const changelogTitle = '# Changelog'; +const changelogDescription = `All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).`; + +// Stringification helpers + +function stringifyCategory(category, changes) { + const categoryHeader = `### ${category}`; + if (changes.length === 0) { + return categoryHeader; + } + const changeDescriptions = changes + .map((description) => `- ${description}`) + .join('\n'); + return `${categoryHeader}\n${changeDescriptions}`; +} + +function stringifyRelease(version, categories, { date, status } = {}) { + const releaseHeader = `## [${version}]${date ? ` - ${date}` : ''}${ + status ? ` [${status}]` : '' + }`; + const categorizedChanges = orderedChangeCategories + .filter((category) => categories[category]) + .map((category) => { + const changes = categories[category]; + return stringifyCategory(category, changes); + }) + .join('\n\n'); + if (categorizedChanges === '') { + return releaseHeader; + } + return `${releaseHeader}\n${categorizedChanges}`; +} + +function stringifyReleases(releases, changes) { + const stringifiedUnreleased = stringifyRelease( + unreleased, + changes[unreleased], + ); + const stringifiedReleases = releases.map(({ version, date, status }) => { + const categories = changes[version]; + return stringifyRelease(version, categories, { date, status }); + }); + + return [stringifiedUnreleased, ...stringifiedReleases].join('\n\n'); +} + +function withTrailingSlash(url) { + return url.endsWith('/') ? url : `${url}/`; +} + +function getCompareUrl(repoUrl, firstRef, secondRef) { + return `${withTrailingSlash(repoUrl)}compare/${firstRef}...${secondRef}`; +} + +function getTagUrl(repoUrl, tag) { + return `${withTrailingSlash(repoUrl)}releases/tag/${tag}`; +} + +function stringifyLinkReferenceDefinitions(repoUrl, releases) { + const orderedReleases = releases + .map(({ version }) => version) + .sort((a, b) => semver.gt(a, b)); + + // The "Unreleased" section represents all changes made since the *highest* + // release, not the most recent release. This is to accomodate patch releases + // of older versions that don't represent the latest set of changes. + // + // For example, if a library has a v2.0.0 but the v1.0.0 release needed a + // security update, the v1.0.1 release would then be the most recent, but the + // range of unreleased changes would remain `v2.0.0...HEAD`. + const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${getCompareUrl( + repoUrl, + `v${orderedReleases[0]}`, + 'HEAD', + )}`; + + // The "previous" release that should be used for comparison is not always + // the most recent release chronologically. The _highest_ version that is + // lower than the current release is used as the previous release, so that + // patch releases on older releases can be accomodated. + const releaseLinkReferenceDefinitions = releases + .map(({ version }) => { + if (version === orderedReleases[orderedReleases.length - 1]) { + return `[${version}]: ${getTagUrl(repoUrl, `v${version}`)}`; + } + const versionIndex = orderedReleases.indexOf(version); + const previousVersion = orderedReleases + .slice(versionIndex) + .find((releaseVersion) => { + return semver.gt(version, releaseVersion); + }); + return `[${version}]: ${getCompareUrl( + repoUrl, + `v${previousVersion}`, + `v${version}`, + )}`; + }) + .join('\n'); + return `${unreleasedLinkReferenceDefinition}\n${releaseLinkReferenceDefinitions}${ + releases.length > 0 ? '\n' : '' + }`; +} + +/** + * @typedef {import('./constants.js').Unreleased} Unreleased + * @typedef {import('./constants.js').ChangeCategories ChangeCategories} + */ +/** + * @typedef {import('./constants.js').Version} Version + */ +/** + * Release metadata. + * @typedef {Object} ReleaseMetadata + * @property {string} date - An ISO-8601 formatted date, representing the + * release date. + * @property {string} status -The status of the release (e.g. 'WITHDRAWN', 'DEPRECATED') + * @property {Version} version - The version of the current release. + */ + +/** + * Category changes. A list of changes in a single category. + * @typedef {Array} CategoryChanges + */ + +/** + * Release changes, organized by category + * @typedef {Record} ReleaseChanges + */ + +/** + * Changelog changes, organized by release and by category. + * @typedef {Record} ChangelogChanges + */ + +/** + * A changelog that complies with the ["keep a changelog" v1.1.0 guidelines]{@link https://keepachangelog.com/en/1.0.0/}. + * + * This changelog starts out completely empty, and allows new releases and + * changes to be added such that the changelog remains compliant at all times. + * This can be used to help validate the contents of a changelog, normalize + * formatting, update a changelog, or build one from scratch. + */ +class Changelog { + /** + * Construct an empty changelog + * + * @param {Object} options + * @param {string} options.repoUrl - The GitHub repository URL for the current project + */ + constructor({ repoUrl }) { + this._releases = []; + this._changes = { [unreleased]: {} }; + this._repoUrl = repoUrl; + } + + /** + * Add a release to the changelog + * + * @param {Object} options + * @param {boolean} [options.addToStart] - Determines whether the release is + * added to the top or bottom of the changelog. This defaults to 'true' + * because new releases should be added to the top of the changelog. This + * should be set to 'false' when parsing a changelog top-to-bottom. + * @param {string} [options.date] - An ISO-8601 formatted date, representing the + * release date. + * @param {string} [options.status] - The status of the release (e.g. + * 'WITHDRAWN', 'DEPRECATED') + * @param {Version} options.version - The version of the current release, + * which should be a [semver]{@link https://semver.org/spec/v2.0.0.html}- + * compatible version. + */ + addRelease({ addToStart = true, date, status, version }) { + if (!version) { + throw new Error('Version required'); + } else if (semver.valid(version) === null) { + throw new Error(`Not a valid semver version: '${version}'`); + } else if (this._changes[version]) { + throw new Error(`Release already exists: '${version}'`); + } + + this._changes[version] = {}; + const newRelease = { version, date, status }; + if (addToStart) { + this._releases.unshift(newRelease); + } else { + this._releases.push(newRelease); + } + } + + /** + * Add a change to the changelog + * + * @param {Object} options + * @param {boolean} [options.addToStart] - Determines whether the change is + * added to the top or bottom of the list of changes in this category. This + * defaults to 'true' because changes should be in reverse-chronological + * order. This should be set to 'false' when parsing a changelog top-to- + * bottom. + * @param {string} options.category - The category of the change. + * @param {string} options.description - The description of the change. + * @param {Version} [options.version] - The version this change was released + * in. If this is not given, the change is assumed to be unreleased. + */ + addChange({ addToStart = true, category, description, version }) { + if (!category) { + throw new Error('Category required'); + } else if (!orderedChangeCategories.includes(category)) { + throw new Error(`Unrecognized category: '${category}'`); + } else if (!description) { + throw new Error('Description required'); + } else if (version !== undefined && !this._changes[version]) { + throw new Error(`Specified release version does not exist: '${version}'`); + } + + const release = version + ? this._changes[version] + : this._changes[unreleased]; + + if (!release[category]) { + release[category] = []; + } + if (addToStart) { + release[category].unshift(description); + } else { + release[category].push(description); + } + } + + /** + * Gets the metadata for all releases. + * @returns {Array} The metadata for each release. + */ + getReleases() { + return this._releases; + } + + /** + * Gets the changes in the given release, organized by category. + * @param {Version} version - The version of the release being retrieved. + * @returns {ReleaseChanges} The changes included in the given released. + */ + getReleaseChanges(version) { + return this._changes[version]; + } + + /** + * Gets all changes that have not yet been released + * @returns {ReleaseChanges} The changes that have not yet been released. + */ + getUnreleasedChanges() { + return this._changes[unreleased]; + } + + /** + * The stringified changelog, formatted according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + * @returns {string} The stringified changelog. + */ + toString() { + return `${changelogTitle} +${changelogDescription} + +${stringifyReleases(this._releases, this._changes)} + +${stringifyLinkReferenceDefinitions(this._repoUrl, this._releases)}`; + } +} + +module.exports = Changelog; diff --git a/development/lib/changelog/constants.js b/development/lib/changelog/constants.js new file mode 100644 index 000000000..c2b8ae008 --- /dev/null +++ b/development/lib/changelog/constants.js @@ -0,0 +1,68 @@ +/** + * Version string + * @typedef {string} Version - A [SemVer]{@link https://semver.org/spec/v2.0.0.html}- + * compatible version string. + */ + +/** + * Change categories. + * + * Most of these categories are from [Keep a Changelog]{@link https://keepachangelog.com/en/1.0.0/}. + * The "Uncategorized" category was added because we have many changes from + * older releases that would be difficult to categorize. + * + * @typedef {Record} ChangeCategories + * @property {'Added'} Added - for new features. + * @property {'Changed'} Changed - for changes in existing functionality. + * @property {'Deprecated'} Deprecated - for soon-to-be removed features. + * @property {'Fixed'} Fixed - for any bug fixes. + * @property {'Removed'} Removed - for now removed features. + * @property {'Security'} Security - in case of vulnerabilities. + * @property {'Uncategorized'} Uncategorized - for any changes that have not + * yet been categorized. + */ + +/** + * @type {ChangeCategories} + */ +const changeCategories = { + Added: 'Added', + Changed: 'Changed', + Deprecated: 'Deprecated', + Fixed: 'Fixed', + Removed: 'Removed', + Security: 'Security', + Uncategorized: 'Uncategorized', +}; + +/** + * Change categories in the order in which they should be listed in the + * changelog. + * + * @type {Array} + */ +const orderedChangeCategories = [ + 'Uncategorized', + 'Added', + 'Changed', + 'Deprecated', + 'Removed', + 'Fixed', + 'Security', +]; + +/** + * The header for the section of the changelog listing unreleased changes. + * @typedef {'Unreleased'} Unreleased + */ + +/** + * @type {Unreleased} + */ +const unreleased = 'Unreleased'; + +module.exports = { + changeCategories, + orderedChangeCategories, + unreleased, +}; diff --git a/development/lib/changelog/parseChangelog.js b/development/lib/changelog/parseChangelog.js new file mode 100644 index 000000000..228da3635 --- /dev/null +++ b/development/lib/changelog/parseChangelog.js @@ -0,0 +1,84 @@ +const Changelog = require('./changelog'); +const { unreleased } = require('./constants'); + +function truncated(line) { + return line.length > 80 ? `${line.slice(0, 80)}...` : line; +} + +/** + * Constructs a Changelog instance that represents the given changelog, which + * is parsed for release and change informatino. + * @param {Object} options + * @param {string} options.changelogContent - The changelog to parse + * @param {string} options.repoUrl - The GitHub repository URL for the current + * project. + * @returns {Changelog} A changelog instance that reflects the changelog text + * provided. + */ +function parseChangelog({ changelogContent, repoUrl }) { + const changelogLines = changelogContent.split('\n'); + const changelog = new Changelog({ repoUrl }); + + const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`); + if (unreleasedHeaderIndex === -1) { + throw new Error(`Failed to find ${unreleased} header`); + } + const unreleasedLinkReferenceDefinition = changelogLines.findIndex((line) => { + return line.startsWith(`[${unreleased}]: `); + }); + if (unreleasedLinkReferenceDefinition === -1) { + throw new Error(`Failed to find ${unreleased} link reference definition`); + } + + const contentfulChangelogLines = changelogLines + .slice(unreleasedHeaderIndex + 1, unreleasedLinkReferenceDefinition) + .filter((line) => line !== ''); + + let mostRecentRelease; + let mostRecentCategory; + for (const line of contentfulChangelogLines) { + if (line.startsWith('## [')) { + const results = line.match( + /^## \[(\d+\.\d+\.\d+)\](?: - (\d\d\d\d-\d\d-\d\d))?(?: \[(\w+)\])?/u, + ); + if (results === null) { + throw new Error(`Malformed release header: '${truncated(line)}'`); + } + mostRecentRelease = results[1]; + mostRecentCategory = undefined; + const date = results[2]; + const status = results[3]; + changelog.addRelease({ + addToStart: false, + date, + status, + version: mostRecentRelease, + }); + } else if (line.startsWith('### ')) { + const results = line.match(/^### (\w+)$\b/u); + if (results === null) { + throw new Error(`Malformed category header: '${truncated(line)}'`); + } + mostRecentCategory = results[1]; + } else if (line.startsWith('- ')) { + if (mostRecentCategory === undefined) { + throw new Error(`Category missing for change: '${truncated(line)}'`); + } + const description = line.slice(2); + changelog.addChange({ + addToStart: false, + category: mostRecentCategory, + description, + version: mostRecentRelease, + }); + } else if (mostRecentRelease === null) { + continue; + } else { + throw new Error(`Unrecognized line: '${truncated(line)}'`); + } + } + + return changelog; +} + +module.exports = { parseChangelog }; diff --git a/development/lib/changelog/updateChangelog.js b/development/lib/changelog/updateChangelog.js new file mode 100644 index 000000000..2198b8f0a --- /dev/null +++ b/development/lib/changelog/updateChangelog.js @@ -0,0 +1,163 @@ +const assert = require('assert').strict; +const runCommand = require('../runCommand'); +const { parseChangelog } = require('./parseChangelog'); +const { changeCategories } = require('./constants'); + +async function getMostRecentTag() { + const [mostRecentTagCommitHash] = await runCommand('git', [ + 'rev-list', + '--tags', + '--max-count=1', + ]); + const [mostRecentTag] = await runCommand('git', [ + 'describe', + '--tags', + mostRecentTagCommitHash, + ]); + assert.equal(mostRecentTag[0], 'v', 'Most recent tag should start with v'); + return mostRecentTag; +} + +async function getCommits(commitHashes) { + const commits = []; + for (const commitHash of commitHashes) { + const [subject] = await runCommand('git', [ + 'show', + '-s', + '--format=%s', + commitHash, + ]); + + let prNumber; + let description = subject; + + // Squash & Merge: the commit subject is parsed as ` (#)` + if (subject.match(/\(#\d+\)/u)) { + const matchResults = subject.match(/\(#(\d+)\)/u); + prNumber = matchResults[1]; + description = subject.match(/^(.+)\s\(#\d+\)/u)[1]; + // Merge: the PR ID is parsed from the git subject (which is of the form `Merge pull request + // # from `, and the description is assumed to be the first line of the body. + // If no body is found, the description is set to the commit subject + } else if (subject.match(/#\d+\sfrom/u)) { + const matchResults = subject.match(/#(\d+)\sfrom/u); + prNumber = matchResults[1]; + const [firstLineOfBody] = await runCommand('git', [ + 'show', + '-s', + '--format=%b', + commitHash, + ]); + description = firstLineOfBody || subject; + } + // Otherwise: + // Normal commits: The commit subject is the description, and the PR ID is omitted. + + commits.push({ prNumber, description }); + } + return commits; +} + +function getAllChangeDescriptions(changelog) { + const releases = changelog.getReleases(); + const changeDescriptions = Object.values( + changelog.getUnreleasedChanges(), + ).flat(); + for (const release of releases) { + changeDescriptions.push( + ...Object.values(changelog.getReleaseChanges(release.version)).flat(), + ); + } + return changeDescriptions; +} + +function getAllLoggedPrNumbers(changelog) { + const changeDescriptions = getAllChangeDescriptions(changelog); + + const prNumbersWithChangelogEntries = []; + for (const description of changeDescriptions) { + const matchResults = description.match(/^\[#(\d+)\]/u); + if (matchResults === null) { + continue; + } + const prNumber = matchResults[1]; + prNumbersWithChangelogEntries.push(prNumber); + } + + return prNumbersWithChangelogEntries; +} + +/** + * @typedef {import('./constants.js').Version} Version + */ + +/** + * Update a changelog with any commits made since the last release. Commits for + * PRs that are already included in the changelog are omitted. + * @param {Object} options + * @param {string} options.changelogContent - The current changelog + * @param {Version} options.currentVersion - The current version + * @param {string} options.repoUrl - The GitHub repository URL for the current + * project. + * @param {boolean} options.isReleaseCandidate - Denotes whether the current + * project is in the midst of release preparation or not. If this is set, any + * new changes are listed under the current release header. Otherwise, they + * are listed under the 'Unreleased' section. + * @returns + */ +async function updateChangelog({ + changelogContent, + currentVersion, + repoUrl, + isReleaseCandidate, +}) { + const changelog = parseChangelog({ changelogContent, repoUrl }); + + // Ensure we have all tags on remote + await runCommand('git', ['fetch', '--tags']); + const mostRecentTag = await getMostRecentTag(); + const commitsHashesSinceLastRelease = await runCommand('git', [ + 'rev-list', + `${mostRecentTag}..HEAD`, + ]); + const commits = await getCommits(commitsHashesSinceLastRelease); + + const loggedPrNumbers = getAllLoggedPrNumbers(changelog); + const newCommits = commits.filter( + ({ prNumber }) => !loggedPrNumbers.includes(prNumber), + ); + + if (newCommits.length === 0) { + return undefined; + } + + // Ensure release header exists, if necessary + if ( + isReleaseCandidate && + !changelog + .getReleases() + .find((release) => release.version === currentVersion) + ) { + changelog.addRelease({ currentVersion }); + } + + const newChangeEntries = newCommits.map(({ prNumber, description }) => { + if (prNumber) { + const prefix = `[#${prNumber}](${repoUrl}/pull/${prNumber})`; + return `${prefix}: ${description}`; + } + return description; + }); + + for (const description of newChangeEntries.reverse()) { + changelog.addChange({ + version: isReleaseCandidate ? currentVersion : undefined, + category: changeCategories.Uncategorized, + description, + }); + } + + return changelog.toString(); +} + +module.exports = { updateChangelog }; diff --git a/package.json b/package.json index 2fe6ed9d6..30c09015a 100644 --- a/package.json +++ b/package.json @@ -280,6 +280,7 @@ "sass": "^1.32.4", "sass-loader": "^10.1.1", "selenium-webdriver": "4.0.0-alpha.7", + "semver": "^7.3.5", "serve-handler": "^6.1.2", "sinon": "^9.0.0", "source-map": "^0.7.2", diff --git a/yarn.lock b/yarn.lock index b47f81af3..28b7220d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23334,6 +23334,13 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: dependencies: lru-cache "^6.0.0" +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + semver@~5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"