diff --git a/README.md b/README.md index 740edf3..f4a7f6a 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ or ## CLI Usage +### Update + To update the 'Unreleased' section of the changelog: `npx @metamask/auto-changelog update` @@ -22,6 +24,16 @@ To update the current release section of the changelog: `npx @metamask/auto-changelog update --rc` +### Validate + +To validate the changelog: + +`npx @metamask/auto-changelog validate` + +To validate the changelog in a release candidate environment: + +`npx @metamask/auto-changelog validate --rc` + ## API Usage Each supported command is a separate named export. @@ -46,6 +58,30 @@ const updatedChangelog = updateChangelog({ await fs.writeFile('CHANEGLOG.md', updatedChangelog); ``` +### `validateChangelog` + +This command validates the changelog + +```javascript +const fs = require('fs').promises; +const { validateChangelog } = require('@metamask/auto-changelog'); + +const oldChangelog = await fs.readFile('CHANEGLOG.md', { + encoding: 'utf8', +}); +try { + validateChangelog({ + changelogContent: oldChangelog, + currentVersion: '1.0.0', + repoUrl: 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }); + // changelog is valid! +} catch (error) { + // changelog is invalid +} +``` + ## Testing Run `yarn test` to run the tests once. diff --git a/package.json b/package.json index 63bbdbb..1a68f96 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "cross-spawn": "^7.0.3", + "diff": "^5.0.0", "semver": "^7.3.5", "yargs": "^16.2.0" }, diff --git a/src/changelog.js b/src/changelog.js index 5d71617..5394540 100644 --- a/src/changelog.js +++ b/src/changelog.js @@ -64,9 +64,12 @@ function getTagUrl(repoUrl, tag) { } function stringifyLinkReferenceDefinitions(repoUrl, releases) { - const orderedReleases = releases + const releasesOrderedByVersion = releases .map(({ version }) => version) - .sort((a, b) => semver.gt(a, b)); + .sort((a, b) => { + return semver.gt(a, b) ? -1 : 1; + }); + const orderedReleases = releases.map(({ version }) => version); const hasReleases = orderedReleases.length > 0; // The "Unreleased" section represents all changes made since the *highest* @@ -81,7 +84,7 @@ function stringifyLinkReferenceDefinitions(repoUrl, releases) { // the link definition. const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${ hasReleases - ? getCompareUrl(repoUrl, `v${orderedReleases[0]}`, 'HEAD') + ? getCompareUrl(repoUrl, `v${releasesOrderedByVersion[0]}`, 'HEAD') : withTrailingSlash(repoUrl) }`; diff --git a/src/cli.js b/src/cli.js index 7f9a707..0f940b0 100644 --- a/src/cli.js +++ b/src/cli.js @@ -6,6 +6,11 @@ const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const { updateChangelog } = require('./updateChangelog'); +const { generateDiff } = require('./generateDiff'); +const { + validateChangelog, + ChangelogFormattingError, +} = require('./validateChangelog'); const { unreleased } = require('./constants'); const updateEpilog = `New commits will be added to the "${unreleased}" section (or \ @@ -16,11 +21,62 @@ 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.`; +const validateEpilog = `This does not ensure that the changelog is complete, \ +or that each change is in the correct section. It just ensures that the \ +formatting is correct. Verification of the contents is left for manual review.`; + // eslint-disable-next-line node/no-process-env const npmPackageVersion = process.env.npm_package_version; // eslint-disable-next-line node/no-process-env const npmPackageRepositoryUrl = process.env.npm_package_repository_url; +const changelogFilename = 'CHANGELOG.md'; + +async function readChangelog() { + return await fs.readFile(changelogFilename, { + encoding: 'utf8', + }); +} + +async function saveChangelog(newChangelogContent) { + await fs.writeFile(changelogFilename, newChangelogContent); +} + +async function update({ isReleaseCandidate }) { + const changelogContent = await readChangelog(); + + const newChangelogContent = await updateChangelog({ + changelogContent, + currentVersion: npmPackageVersion, + repoUrl: npmPackageRepositoryUrl, + isReleaseCandidate, + }); + + await saveChangelog(newChangelogContent); + console.log('CHANGELOG updated'); +} + +async function validate({ isReleaseCandidate }) { + const changelogContent = await readChangelog(); + + try { + validateChangelog({ + changelogContent, + currentVersion: npmPackageVersion, + repoUrl: npmPackageRepositoryUrl, + isReleaseCandidate, + }); + } catch (error) { + if (error instanceof ChangelogFormattingError) { + const { validChangelog, invalidChangelog } = error.data; + const diff = generateDiff(validChangelog, invalidChangelog); + console.error(`Changelog not well-formatted.\nDiff:\n${diff}`); + process.exit(1); + } + throw error; + } +} + async function main() { const { argv } = yargs(hideBin(process.argv)) .command( @@ -35,6 +91,18 @@ async function main() { }) .epilog(updateEpilog), ) + .command( + 'validate', + 'Validate the changelog, ensuring that it is well-formatted.\nUsage: $0 validate [options]', + (_yargs) => + _yargs + .option('rc', { + default: false, + description: `Verify that the current version has a release header in the changelog`, + type: 'boolean', + }) + .epilog(validateEpilog), + ) .strict() .demandCommand() .help('help') @@ -52,23 +120,11 @@ async function main() { ); } - const isReleaseCandidate = argv.rc; - - const changelogFilename = 'CHANGELOG.md'; - const changelogContent = await fs.readFile(changelogFilename, { - encoding: 'utf8', - }); - - const newChangelogContent = await updateChangelog({ - changelogContent, - currentVersion: npmPackageVersion, - repoUrl: npmPackageRepositoryUrl, - isReleaseCandidate, - }); - - await fs.writeFile(changelogFilename, newChangelogContent); - - console.log('CHANGELOG updated'); + if (argv._ && argv._[0] === 'update') { + await update({ isReleaseCandidate: argv.rc }); + } else if (argv._ && argv._[0] === 'validate') { + await validate({ isReleaseCandidate: argv.rc }); + } } main().catch((error) => { diff --git a/src/generateDiff.js b/src/generateDiff.js new file mode 100644 index 0000000..6820c63 --- /dev/null +++ b/src/generateDiff.js @@ -0,0 +1,41 @@ +const diff = require('diff'); + +/** + * Generates a diff between two multi-line strings. The resulting diff shows + * any changes using '-' and '+' to indicate the "old" and "new" version + * respectively, and includes 2 lines of unchanged content around each changed + * section where possible. + * @param {string} before - The string representing the base for the comparison. + * @param {string} after - The string representing the changes being compared. + * @returns {string} The genereated text diff + */ +function generateDiff(before, after) { + const changes = diff.diffLines(before, after); + const diffLines = []; + const preceedingContext = []; + for (const { added, removed, value } of changes) { + const lines = value.split('\n'); + // remove trailing newline + lines.pop(); + if (added || removed) { + if (preceedingContext.length) { + diffLines.push(...preceedingContext); + preceedingContext.splice(0, preceedingContext.length); + } + diffLines.push(...lines.map((line) => `${added ? '+' : '-'}${line}`)); + } else { + // If a changed line has been included already, add up to 2 lines of context + if (diffLines.length) { + diffLines.push(...lines.slice(0, 2).map((line) => ` ${line}`)); + lines.splice(0, 2); + } + // stash last 2 lines for context in case there is another change + if (lines.length) { + preceedingContext.push(...lines.slice(-2)); + } + } + } + return diffLines.join('\n'); +} + +module.exports = { generateDiff }; diff --git a/src/index.js b/src/index.js index 3958448..1024e96 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,11 @@ const { updateChangelog } = require('./updateChangelog'); +const { + ChangelogFormattingError, + validateChangelog, +} = require('./validateChangelog'); module.exports = { + ChangelogFormattingError, updateChangelog, + validateChangelog, }; diff --git a/src/validateChangelog.js b/src/validateChangelog.js new file mode 100644 index 0000000..f372d06 --- /dev/null +++ b/src/validateChangelog.js @@ -0,0 +1,74 @@ +const { parseChangelog } = require('./parseChangelog'); + +/** + * @typedef {import('./constants.js').Version} Version + */ + +/** + * Represents a formatting error in a changelog. + */ +class ChangelogFormattingError extends Error { + /** + * @param {Object} options + * @param {string} options.validChangelog - The string contents of the well- + * formatted changelog. + * @param {string} options.invalidChangelog - The string contents of the + * malformed changelog. + */ + constructor({ validChangelog, invalidChangelog }) { + super('Changelog is not well-formatted'); + this.data = { + validChangelog, + invalidChangelog, + }; + } +} + +/** + * Validates that a changelog is well-formatted. + * @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, this + * command will also ensure the current version is represented in the + * changelog with a release header, and that there are no unreleased changes + * present. + */ +function validateChangelog({ + changelogContent, + currentVersion, + repoUrl, + isReleaseCandidate, +}) { + const changelog = parseChangelog({ changelogContent, repoUrl }); + + // Ensure release header exists, if necessary + if ( + isReleaseCandidate && + !changelog + .getReleases() + .find((release) => release.version === currentVersion) + ) { + throw new Error( + `Current version missing from changelog: '${currentVersion}'`, + ); + } + + const hasUnreleasedChanges = changelog.getUnreleasedChanges().length !== 0; + if (isReleaseCandidate && hasUnreleasedChanges) { + throw new Error('Unreleased changes present in the changelog'); + } + + const validChangelog = changelog.toString(); + if (validChangelog !== changelogContent) { + throw new ChangelogFormattingError({ + validChangelog, + invalidChangelog: changelogContent, + }); + } +} + +module.exports = { validateChangelog, ChangelogFormattingError }; diff --git a/src/validateChangelog.test.js b/src/validateChangelog.test.js new file mode 100644 index 0000000..1f0efa3 --- /dev/null +++ b/src/validateChangelog.test.js @@ -0,0 +1,445 @@ +const { validateChangelog } = require('./validateChangelog'); + +const emptyChangelog = `# Changelog +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). + +## [Unreleased] + +[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/ +`; + +const changelogWithReleases = `# Changelog +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). + +## [Unreleased] + +## [1.0.0] - 2020-01-01 +### Changed +- Something else + +## [0.0.2] - 2020-01-01 +### Fixed +- Something + +## [0.0.1] - 2020-01-01 +### Changed +- Something + +[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v1.0.0 +[0.0.2]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.1...v0.0.2 +[0.0.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v0.0.1 +`; + +const branchingChangelog = `# Changelog +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). + +## [Unreleased] + +## [0.0.3] - 2020-01-01 +### Fixed +- Security fix + +## [1.0.0] - 2020-01-01 +### Changed +- Something else + +## [0.0.2] - 2020-01-01 +### Fixed +- Something + +## [0.0.1] - 2020-01-01 +### Changed +- Something + +[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD +[0.0.3]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v0.0.3 +[1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v1.0.0 +[0.0.2]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.1...v0.0.2 +[0.0.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v0.0.1 +`; + +describe('validateChangelog', () => { + it('should throw for an empty string', () => { + expect(() => + validateChangelog({ + changelogContent: '', + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Failed to find Unreleased header'); + }); + + it('should not throw for any empty valid changelog', () => { + expect(() => + validateChangelog({ + changelogContent: emptyChangelog, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).not.toThrow(); + }); + + it('should not throw for a valid changelog with multiple releases', () => { + expect(() => + validateChangelog({ + changelogContent: changelogWithReleases, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).not.toThrow(); + }); + + it('should throw when the title is different', () => { + const changelogWithDifferentTitle = changelogWithReleases.replace( + '# Changelog', + '# Custom Title', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithDifferentTitle, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw when the changelog description is different', () => { + const changelogWithDifferentDescription = changelogWithReleases.replace( + 'All notable changes', + 'A random assortment of changes', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithDifferentDescription, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw when there are whitespace changes', () => { + const changelogWithExtraWhitespace = `${changelogWithReleases}\n`; + expect(() => + validateChangelog({ + changelogContent: changelogWithExtraWhitespace, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw when a release header is malformed', () => { + const changelogWithMalformedReleaseHeader = changelogWithReleases.replace( + '[1.0.0] - 2020-01-01', + '1.0.0 - 2020-01-01', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithMalformedReleaseHeader, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow(`Unrecognized line: '## 1.0.0 - 2020-01-01'`); + }); + + it('should throw when there are extraneous header contents', () => { + const changelogWithExtraHeaderContents = changelogWithReleases.replace( + '[1.0.0] - 2020-01-01', + '[1.0.0] - 2020-01-01 [extra contents]', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithExtraHeaderContents, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw when a change category is unrecognized', () => { + const changelogWithUnrecognizedChangeCategory = changelogWithReleases.replace( + '### Changed', + '### Updated', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithUnrecognizedChangeCategory, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow(`Unrecognized category: 'Updated'`); + }); + + it('should throw when the Unreleased section is missing', () => { + const changelogWithoutUnreleased = changelogWithReleases.replace( + /## \[Unreleased\]\n\n/u, + '', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithoutUnreleased, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Failed to find Unreleased header'); + }); + + it('should throw if the wrong repo URL is used', () => { + expect(() => + validateChangelog({ + changelogContent: changelogWithReleases, + currentVersion: '1.0.0', + repoUrl: 'https://github.com/DifferentOrganization/DifferentRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if a comparison release link is missing', () => { + const changelogWithoutReleaseLink = changelogWithReleases.replace( + '[1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v1.0.0\n', + '', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithoutReleaseLink, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if the first release link is missing', () => { + const changelogWithoutFirstReleaseLink = changelogWithReleases.replace( + '[0.0.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v0.0.1\n', + '', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithoutFirstReleaseLink, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if release links are in a different order than the release headers', () => { + const thirdReleaseLink = + '[1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v1.0.0'; + const secondReleaseLink = + '[0.0.2]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.1...v0.0.2'; + const changelogWithoutFirstReleaseLink = changelogWithReleases.replace( + `${thirdReleaseLink}\n${secondReleaseLink}`, + `${secondReleaseLink}\n${thirdReleaseLink}`, + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithoutFirstReleaseLink, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should not throw for changelog with branching releases', () => { + expect(() => + validateChangelog({ + changelogContent: branchingChangelog, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).not.toThrow(); + }); + + it(`should throw if the highest version isn't compared with the Unreleased changes`, () => { + const changelogWithInvalidUnreleasedComparison = branchingChangelog.replace( + '[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD', + '[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.3...HEAD', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithInvalidUnreleasedComparison, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if there are decreasing comparisons', () => { + const changelogWithDecreasingComparison = branchingChangelog.replace( + '[0.0.3]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v0.0.3', + '[0.0.3]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...v0.0.3', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithDecreasingComparison, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if the unreleased link points at anything other than the bare repository when there are no releases', () => { + const changelogWithIncorrectUnreleasedLink = emptyChangelog.replace( + '[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/', + '[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v0.0.1', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithIncorrectUnreleasedLink, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if the bare unreleased link is missing a trailing slash', () => { + const changelogWithoutUnreleasedLinkTrailingSlash = emptyChangelog.replace( + '[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/', + '[Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithoutUnreleasedLinkTrailingSlash, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow('Changelog is not well-formatted'); + }); + + it('should throw if a change category is missing', () => { + const changelogWithoutChangeCategory = changelogWithReleases.replace( + '### Changed\n', + '', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithoutChangeCategory, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow("Category missing for change: '- Something else'"); + }); + + it("should throw if a change isn't prefixed by '- '", () => { + const changelogWithInvalidChangePrefix = changelogWithReleases.replace( + '- Something', + 'Something', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithInvalidChangePrefix, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).toThrow(`Unrecognized line: 'Something else'`); + }); + + it('should not throw if the current version release header is missing', () => { + expect(() => + validateChangelog({ + changelogContent: changelogWithReleases, + currentVersion: '1.0.1', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).not.toThrow(); + }); + + it('should not throw if there are unreleased changes', () => { + const changelogWithUnreleasedChanges = changelogWithReleases.replace( + '## [Unreleased]', + '## [Unreleased]\n### Changed\n- More changes', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithUnreleasedChanges, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: false, + }), + ).not.toThrow(); + }); + + describe('isReleaseCandidate', () => { + it('should throw if the current version release header is missing', () => { + expect(() => + validateChangelog({ + changelogContent: changelogWithReleases, + currentVersion: '1.0.1', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: true, + }), + ).toThrow(`Current version missing from changelog: '1.0.1'`); + }); + + it('should throw if there are unreleased changes', () => { + const changelogWithUnreleasedChanges = changelogWithReleases.replace( + '## [Unreleased]', + '## [Unreleased]\n### Changed\n- More changes', + ); + expect(() => + validateChangelog({ + changelogContent: changelogWithUnreleasedChanges, + currentVersion: '1.0.0', + repoUrl: + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + isReleaseCandidate: true, + }), + ).toThrow('Unreleased changes present in the changelog'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 78f4ea1..630a44e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1384,6 +1384,11 @@ diff-sequences@^26.6.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"