diff --git a/src/changelog.ts b/src/changelog.ts index 3352f74..dd8aaf6 100644 --- a/src/changelog.ts +++ b/src/changelog.ts @@ -107,12 +107,14 @@ type ChangelogChanges = Record & { * @param category - The title of the changelog category. * @param changes - The changes included in this category. * @param repoUrl - The URL of the repository. + * @param useShortPrLink - Whether to use short PR links in the changelog entries. * @returns The stringified category section. */ function stringifyCategory( category: ChangeCategory, changes: Change[], repoUrl: string, + useShortPrLink = false, ) { const categoryHeader = `### ${category}`; if (changes.length === 0) { @@ -122,7 +124,11 @@ function stringifyCategory( .map(({ description, prNumbers }) => { const [firstLine, ...otherLines] = description.split('\n'); const stringifiedPrLinks = prNumbers - .map((prNumber) => `[#${prNumber}](${repoUrl}/pull/${prNumber})`) + .map((prNumber) => + useShortPrLink + ? `#${prNumber}` + : `[#${prNumber}](${repoUrl}/pull/${prNumber})`, + ) .join(', '); const parenthesizedPrLinks = stringifiedPrLinks.length > 0 ? ` (${stringifiedPrLinks})` : ''; @@ -140,6 +146,7 @@ function stringifyCategory( * @param version - The release version. * @param categories - The categories of changes included in this release. * @param repoUrl - The URL of the repository. + * @param useShortPrLink - Whether to use short PR links in the changelog entries. * @param options - Additional release options. * @param options.date - The date of the release. * @param options.status - The status of the release (e.g., "DEPRECATED"). @@ -149,6 +156,7 @@ function stringifyRelease( version: Version | typeof unreleased, categories: ReleaseChanges, repoUrl: string, + useShortPrLink = false, { date, status }: Partial = {}, ) { const releaseHeader = `## [${version}]${date ? ` - ${date}` : ''}${ @@ -158,7 +166,7 @@ function stringifyRelease( .filter((category) => categories[category]) .map((category) => { const changes = categories[category] ?? []; - return stringifyCategory(category, changes, repoUrl); + return stringifyCategory(category, changes, repoUrl, useShortPrLink); }) .join('\n\n'); if (categorizedChanges === '') { @@ -173,21 +181,27 @@ function stringifyRelease( * @param releases - The releases to stringify. * @param changes - The set of changes to include, organized by release. * @param repoUrl - The URL of the repository. + * @param useShortPrLink - Whether to use short PR links in the changelog entries. * @returns The stringified set of release sections. */ function stringifyReleases( releases: ReleaseMetadata[], changes: ChangelogChanges, repoUrl: string, + useShortPrLink = false, ) { const stringifiedUnreleased = stringifyRelease( unreleased, changes[unreleased], repoUrl, + useShortPrLink, ); const stringifiedReleases = releases.map(({ version, date, status }) => { const categories = changes[version]; - return stringifyRelease(version, categories, repoUrl, { date, status }); + return stringifyRelease(version, categories, repoUrl, useShortPrLink, { + date, + status, + }); }); return [stringifiedUnreleased, ...stringifiedReleases].join('\n\n'); @@ -586,15 +600,22 @@ export default class Changelog { * Throws an error if no such release exists. * * @param version - The version of the release to stringify. + * @param useShortPrLink - Whether to use short PR links in the changelog entries. * @returns The stringified release, as it appears in the changelog. */ - getStringifiedRelease(version: Version) { + getStringifiedRelease(version: Version, useShortPrLink = false) { const release = this.getRelease(version); if (!release) { throw new Error(`Specified release version does not exist: '${version}'`); } const releaseChanges = this.getReleaseChanges(version); - return stringifyRelease(version, releaseChanges, this.#repoUrl, release); + return stringifyRelease( + version, + releaseChanges, + this.#repoUrl, + useShortPrLink, + release, + ); } /** @@ -619,13 +640,14 @@ export default class Changelog { /** * The stringified changelog, formatted according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * + * @param useShortPrLink - Whether to use short PR links in the changelog entries. * @returns The stringified changelog. */ - async toString(): Promise { + async toString(useShortPrLink = false): Promise { const changelog = `${changelogTitle} ${changelogDescription} -${stringifyReleases(this.#releases, this.#changes, this.#repoUrl)} +${stringifyReleases(this.#releases, this.#changes, this.#repoUrl, useShortPrLink)} ${stringifyLinkReferenceDefinitions( this.#repoUrl, diff --git a/src/get-new-changes.ts b/src/get-new-changes.ts index b95c3e1..9da145d 100644 --- a/src/get-new-changes.ts +++ b/src/get-new-changes.ts @@ -3,6 +3,7 @@ import { Octokit } from '@octokit/rest'; import { strict as assert } from 'assert'; +import { ConventionalCommitType } from './constants'; import { getOwnerAndRepoFromUrl } from './repo'; import { runCommand, runCommandAndSplit } from './run-command'; @@ -17,6 +18,12 @@ export type AddNewCommitsOptions = { useShortPrLink: boolean; }; +// Get array of all ConventionalCommitType values +const conventionalCommitTypes = Object.values(ConventionalCommitType); + +// Create a regex pattern that matches any of the ConventionalCommitTypes +const typesWithPipe = conventionalCommitTypes.join('|'); + /** * Get all commit hashes included in the given commit range. * @@ -35,6 +42,27 @@ async function getCommitHashesInRange( return await runCommandAndSplit('git', revListArgs); } +/** + * Remove outer backticks if present in the given message. + * + * @param message - The changelog entry message. + * @returns The message without outer backticks. + */ +function removeOuterBackticksIfPresent(message: string) { + return message.replace(/^`(.*)`$/u, '$1'); +} + +/** + * Remove Conventional Commit prefix if it exists in the given message. + * + * @param message - The changelog entry message. + * @returns The message without Conventional Commit prefix. + */ +function removeConventionalCommitPrefixIfPresent(message: string) { + const regex = new RegExp(`^(${typesWithPipe})(\\([^)]*\\))?:\\s*`, 'iu'); + return message.replace(regex, ''); +} + /** * Get commit details for each given commit hash. * @@ -90,6 +118,12 @@ async function getCommits( description = changelogEntry; // This may be string 'null' to indicate no description if (description !== 'null') { + // Remove outer backticks if present. Example: `feat: new feature description` -> feat: new feature description + description = removeOuterBackticksIfPresent(description); + + // Remove Conventional Commit prefix if present. Example: feat: new feature description -> new feature description + description = removeConventionalCommitPrefixIfPresent(description); + // Make description coming from `CHANGELOG entry:` start with an uppercase letter description = description.charAt(0).toUpperCase() + description.slice(1); @@ -101,9 +135,6 @@ async function getCommits( if (description !== 'null') { const prLabels = await getPrLabels(repoUrl, prNumber); - // TODO: eliminate this debug log - console.log(`PR #${prNumber} labels:`, prLabels); - if (prLabels.includes('no-changelog')) { description = 'null'; // Has the no-changelog label, use string 'null' to indicate no description } diff --git a/src/parse-changelog.test.ts b/src/parse-changelog.test.ts index 789ed52..7337df4 100644 --- a/src/parse-changelog.test.ts +++ b/src/parse-changelog.test.ts @@ -4,6 +4,49 @@ import { parseChangelog } from './parse-changelog'; const outdent = _outdent({ trimTrailingNewline: false }); +const repoUrl = + 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository'; + +const COMMON_HEADER = outdent`# 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] +`; + +const COMMON_REFERENCE_LINKS_1 = outdent`[Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/compare/v0.0.2...v1.0.0 +`; +const COMMON_REFERENCE_LINKS_3 = outdent`[Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/compare/v0.0.2...v1.0.0 + [0.0.2]: ${repoUrl}/compare/v0.0.1...v0.0.2 + [0.0.1]: ${repoUrl}/releases/tag/v0.0.1 +`; +const COMMON_REFERENCE_LINKS_4 = outdent`[Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/compare/v0.0.3...v1.0.0 + [0.0.3]: ${repoUrl}/compare/v0.0.2...v0.0.3 + [0.0.2]: ${repoUrl}/compare/v0.0.1...v0.0.2 + [0.0.1]: ${repoUrl}/releases/tag/v0.0.1 +`; + +/** + * Creates a changelog string with the given content, header, and reference links. + * + * @param header - The changelog header (default: COMMON_HEADER). + * @param content - The changelog content to include. + * @param reference - The reference links (default: COMMON_REFERENCE_LINKS). + * @returns The complete changelog string. + */ +function createChangelog( + header: string, + content: string, + reference: string, +): string { + return `${header}\n${content}\n${reference}`; +} + describe('parseChangelog', () => { it('should parse empty changelog', () => { const changelog = parseChangelog({ @@ -37,10 +80,9 @@ describe('parseChangelog', () => { ## [Unreleased] - [Unreleased]:https://github.com/ExampleUsernameOrOrganization/ExampleRepository/ + [Unreleased]:${repoUrl}/ `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([]); @@ -57,10 +99,9 @@ describe('parseChangelog', () => { ## [Unreleased] - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/ + [Unreleased]: ${repoUrl}/ `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([]); @@ -74,10 +115,9 @@ describe('parseChangelog', () => { ## [Unreleased] - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/ + [Unreleased]: ${repoUrl}/ `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([]); @@ -107,13 +147,12 @@ describe('parseChangelog', () => { ### 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 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/compare/v0.0.2...v1.0.0 + [0.0.2]: ${repoUrl}/compare/v0.0.1...v0.0.2 + [0.0.1]: ${repoUrl}/releases/tag/v0.0.1 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([ @@ -189,10 +228,9 @@ describe('parseChangelog', () => { ### Changed - Something - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([ @@ -253,13 +291,12 @@ describe('parseChangelog', () => { ### Changed - Something - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0-rc.1...HEAD - [1.0.0-rc.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2-beta.1...v1.0.0-rc.1 - [0.0.2-beta.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.1-alpha.1...v0.0.2-beta.1 - [0.0.1-alpha.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v0.0.1-alpha.1 + [Unreleased]: ${repoUrl}/compare/v1.0.0-rc.1...HEAD + [1.0.0-rc.1]: ${repoUrl}/compare/v0.0.2-beta.1...v1.0.0-rc.1 + [0.0.2-beta.1]: ${repoUrl}/compare/v0.0.1-alpha.1...v0.0.2-beta.1 + [0.0.1-alpha.1]: ${repoUrl}/releases/tag/v0.0.1-alpha.1 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([ @@ -292,13 +329,12 @@ describe('parseChangelog', () => { ### 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 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/compare/v0.0.2...v1.0.0 + [0.0.2]: ${repoUrl}/compare/v0.0.1...v0.0.2 + [0.0.1]: ${repoUrl}/releases/tag/v0.0.1 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([ @@ -324,11 +360,10 @@ describe('parseChangelog', () => { - Something else Further explanation of changes - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ @@ -357,11 +392,10 @@ describe('parseChangelog', () => { - Something else - Further explanation of changes - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ @@ -391,11 +425,10 @@ describe('parseChangelog', () => { - Further explanation of changes - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ @@ -427,11 +460,10 @@ describe('parseChangelog', () => { ### Fixed - Not including newline between change categories as part of change entry - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ @@ -471,11 +503,10 @@ describe('parseChangelog', () => { ### Changed - Initial release - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ @@ -500,11 +531,10 @@ describe('parseChangelog', () => { ## [1.0.0] - 2020-01-01 ### Changed - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([ @@ -530,13 +560,12 @@ describe('parseChangelog', () => { 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]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/ + [Unreleased]: ${repoUrl}/ `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow('Failed to find Unreleased header'); }); @@ -554,8 +583,7 @@ describe('parseChangelog', () => { expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow('Failed to find Unreleased link reference definition'); }); @@ -574,14 +602,13 @@ describe('parseChangelog', () => { ### Changed - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Unrecognized line: '## 1.0.0 - 2020-01-01'`); }); @@ -600,14 +627,13 @@ describe('parseChangelog', () => { ### Changed - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Malformed release header: '## [1.0.0 - 2020-01-01'`); }); @@ -626,14 +652,13 @@ describe('parseChangelog', () => { ### Changed - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.2.3.4...HEAD - [1.2.3.4]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.2.3.4 + [Unreleased]: ${repoUrl}/compare/v1.2.3.4...HEAD + [1.2.3.4]: ${repoUrl}/releases/tag/v1.2.3.4 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Invalid SemVer version in release header: '## [1.2.3.4]`); }); @@ -652,14 +677,13 @@ describe('parseChangelog', () => { ### Changed - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Unrecognized line: '# [1.0.0] - 2020-01-01'`); }); @@ -677,14 +701,13 @@ describe('parseChangelog', () => { ## [1.0.0] - 2020-01-01 - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Category missing for change: '- Something else'`); }); @@ -703,14 +726,13 @@ describe('parseChangelog', () => { #### Changed - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Unrecognized line: '#### Changed'`); }); @@ -729,14 +751,13 @@ describe('parseChangelog', () => { ### Ch-Ch-Ch-Ch-Changes - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Malformed category header: '### Ch-Ch-Ch-Ch-Changes'`); }); @@ -755,14 +776,13 @@ describe('parseChangelog', () => { ### Invalid - Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Invalid change category: 'Invalid'`); }); @@ -781,14 +801,13 @@ describe('parseChangelog', () => { ### Changed * Something else - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow(`Unrecognized line: '* Something else'`); }); @@ -807,14 +826,13 @@ describe('parseChangelog', () => { ### Changed * Very very very very very very very very very very very very very very very very very very very very long line - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/v1.0.0 + [Unreleased]: ${repoUrl}/compare/v1.0.0...HEAD + [1.0.0]: ${repoUrl}/releases/tag/v1.0.0 `; expect(() => parseChangelog({ changelogContent: brokenChangelog, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }), ).toThrow( `Unrecognized line: '* Very very very very very very very very very very very very very very very ver...'`, @@ -844,13 +862,12 @@ describe('parseChangelog', () => { ### Changed - Something - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/@metamask/test@1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/test@0.0.2...@metamask/test@1.0.0 - [0.0.2]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/test@0.0.1...test@0.0.2 - [0.0.1]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/releases/tag/test@0.0.1 + [Unreleased]: ${repoUrl}/compare/@metamask/test@1.0.0...HEAD + [1.0.0]: ${repoUrl}/compare/test@0.0.2...@metamask/test@1.0.0 + [0.0.2]: ${repoUrl}/compare/test@0.0.1...test@0.0.2 + [0.0.1]: ${repoUrl}/releases/tag/test@0.0.1 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + repoUrl, }); expect(changelog.getReleases()).toStrictEqual([ @@ -903,21 +920,14 @@ describe('parseChangelog', () => { expect(changelog.getUnreleasedChanges()).toStrictEqual({}); }); - describe('when shouldExtractPrLinks is true', () => { + describe('when shouldExtractPrLinks is true and changelog entry matches long pattern', () => { it('should parse changelog with pull request links after changelog entries', () => { - const changelog = parseChangelog({ - changelogContent: outdent` - # 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] - + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something else ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200)) + - Change something else ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200)) ## [0.0.2] ### Fixed @@ -926,14 +936,13 @@ describe('parseChangelog', () => { ## [0.0.1] ### Added - Initial release ([#456](anything goes here actually)) - - [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 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_3, + ); + + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -956,26 +965,20 @@ describe('parseChangelog', () => { }); it('should parse changelog with pull request links at end of first line of multi-line change description', () => { - const changelog = parseChangelog({ - changelogContent: outdent` - # 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] - + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200)) + - Change something ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200)) This is a cool change, you will really like it. - - [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 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -991,26 +994,19 @@ describe('parseChangelog', () => { }); it('should parse changelog with pull request links at end of first line of change description with sub-bullets', () => { - const changelog = parseChangelog({ - changelogContent: outdent` - # 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] - + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200)) + - Change something ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200)) - This is a cool change, you will really like it. - - [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 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -1026,34 +1022,26 @@ describe('parseChangelog', () => { }); it('should preserve links within sub-bullets', () => { - const changelog = parseChangelog({ - changelogContent: outdent` - # 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] - + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - Change something - - This is a cool change, you will really like it. ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100)) - - [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 + - This is a cool change, you will really like it. ([#100](${repoUrl}/pull/100)) `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ Changed: [ { - description: - 'Change something\n - This is a cool change, you will really like it. ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100))', + description: `Change something\n - This is a cool change, you will really like it. ([#100](${repoUrl}/pull/100))`, prNumbers: [], }, ], @@ -1061,25 +1049,18 @@ describe('parseChangelog', () => { }); it('should parse changelog with pull request links somewhere within entry, not just at end', () => { - const changelog = parseChangelog({ - changelogContent: outdent` - # 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] - + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200)). And something else. - - [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 + - Change something ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200)). And something else. `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -1094,25 +1075,70 @@ describe('parseChangelog', () => { }); it('should combine multiple pull request lists', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200)) ([#300](${repoUrl}/pull/300)) + `, + COMMON_REFERENCE_LINKS_1, + ); const changelog = parseChangelog({ - changelogContent: outdent` - # 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). + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); - ## [Unreleased] + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: 'Change something', + prNumbers: ['100', '200', '300'], + }, + ], + }); + }); + it('should de-duplicate pull request links in same list', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200)) ([#300](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/300)) + - Change something ([#100](${repoUrl}/pull/100), [#100](${repoUrl}/pull/100)) + `, + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); + + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: 'Change something', + prNumbers: ['100'], + }, + ], + }); + }); - [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 + it('should de-duplicate pull request links in separate lists', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something ([#100](${repoUrl}/pull/100)) ([#100](${repoUrl}/pull/100)) `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -1120,65 +1146,271 @@ describe('parseChangelog', () => { Changed: [ { description: 'Change something', - prNumbers: ['100', '200', '300'], + prNumbers: ['100'], }, ], }); }); - it('should de-duplicate pull request links in same list', () => { + it('should preserve non-pull request links or malformed link syntax after changelog entries as part of the entry text itself', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something else ([123](${repoUrl})) + + ## [0.0.3] + ### Deprecated + - Deprecate whatever([#123](${repoUrl})) + + ## [0.0.2] + ### Fixed + - Fix something + + ## [0.0.1] + ### Added + - Initial release ([#789](https://example.com) + `, + COMMON_REFERENCE_LINKS_4, + ); const changelog = parseChangelog({ - changelogContent: outdent` - # Changelog - All notable changes to this project will be documented in this file. + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); - 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). + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + // Missing '#' + description: `Change something else ([123](${repoUrl}))`, + prNumbers: [], + }, + ], + }); + expect(changelog.getReleaseChanges('0.0.3')).toStrictEqual({ + Deprecated: [ + { + // Missing space before link + description: `Deprecate whatever([#123](${repoUrl}))`, + prNumbers: [], + }, + ], + }); + expect(changelog.getReleaseChanges('0.0.2')).toStrictEqual({ + Fixed: [ + { + // Missing link + description: 'Fix something', + prNumbers: [], + }, + ], + }); + expect(changelog.getReleaseChanges('0.0.1')).toStrictEqual({ + Added: [ + { + // Incorrect URL + description: 'Initial release ([#789](https://example.com)', + prNumbers: [], + }, + ], + }); + }); + }); - ## [Unreleased] + describe('when shouldExtractPrLinks is true and changelog entry matches short pattern', () => { + it('should parse changelog with pull request links after changelog entries', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something else (#100), (#200) + + ## [0.0.2] + ### Fixed + - Fix something + + ## [0.0.1] + ### Added + - Initial release (#456) + `, + COMMON_REFERENCE_LINKS_3, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); + + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: 'Change something else', + prNumbers: ['100', '200'], + }, + ], + }); + expect(changelog.getReleaseChanges('0.0.1')).toStrictEqual({ + Added: [ + { + description: 'Initial release', + prNumbers: ['456'], + }, + ], + }); + }); + it('should parse changelog with pull request links at end of first line of multi-line change description', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100)) + - Change something (#100), (#200) + This is a cool change, you will really like it. + `, + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); - [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 + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: + 'Change something\nThis is a cool change, you will really like it.', + prNumbers: ['100', '200'], + }, + ], + }); + }); + + it('should parse changelog with pull request links at end of first line of change description with sub-bullets', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something (#100), (#200) + - This is a cool change, you will really like it. `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ Changed: [ { - description: 'Change something', - prNumbers: ['100'], + description: + 'Change something\n - This is a cool change, you will really like it.', + prNumbers: ['100', '200'], }, ], }); }); - it('should de-duplicate pull request links in separate lists', () => { + it('should preserve links within sub-bullets', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something + - This is a cool change, you will really like it. (#100) + `, + COMMON_REFERENCE_LINKS_1, + ); const changelog = parseChangelog({ - changelogContent: outdent` - # Changelog - All notable changes to this project will be documented in this file. + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); + + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: + 'Change something\n - This is a cool change, you will really like it. (#100)', + prNumbers: [], + }, + ], + }); + }); - 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). + it('should parse changelog with pull request links somewhere within entry, not just at end', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something (#100), (#200). And something else. + `, + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); - ## [Unreleased] + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: 'Change something. And something else.', + prNumbers: ['100', '200'], + }, + ], + }); + }); + it('should combine multiple pull request lists', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100)) ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100)) + - Change something (#100), (#200) (#300) + `, + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); + + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: 'Change something', + prNumbers: ['100', '200', '300'], + }, + ], + }); + }); - [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 + it('should de-duplicate pull request links in same list', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something (#100), (#100) `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_1, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -1192,24 +1424,43 @@ describe('parseChangelog', () => { }); }); - it('should preserve non-pull request links or malformed link syntax after changelog entries as part of the entry text itself', () => { + it('should de-duplicate pull request links in separate lists', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` + ## [1.0.0] + ### Changed + - Change something (#100) (#100) + `, + COMMON_REFERENCE_LINKS_1, + ); const changelog = parseChangelog({ - changelogContent: outdent` - # 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). + changelogContent, + repoUrl, + shouldExtractPrLinks: true, + }); - ## [Unreleased] + expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ + Changed: [ + { + description: 'Change something', + prNumbers: ['100'], + }, + ], + }); + }); + it('should preserve non-pull request links or malformed link syntax after changelog entries as part of the entry text itself', () => { + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something else ([123](https://github.com/ExampleUsernameOrOrganization/ExampleRepository)) + - Change something else (123) ## [0.0.3] ### Deprecated - - Deprecate whatever([#123](https://github.com/ExampleUsernameOrOrganization/ExampleRepository)) + - Deprecate whatever(#123) ## [0.0.2] ### Fixed @@ -1217,16 +1468,13 @@ describe('parseChangelog', () => { ## [0.0.1] ### Added - - Initial release ([#789](https://example.com) - - [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v1.0.0...HEAD - [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.3...v1.0.0 - [0.0.3]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/v0.0.2...v0.0.3 - [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 + - Initial release (#789 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_4, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: true, }); @@ -1234,8 +1482,7 @@ describe('parseChangelog', () => { Changed: [ { // Missing '#' - description: - 'Change something else ([123](https://github.com/ExampleUsernameOrOrganization/ExampleRepository))', + description: 'Change something else (123)', prNumbers: [], }, ], @@ -1244,8 +1491,7 @@ describe('parseChangelog', () => { Deprecated: [ { // Missing space before link - description: - 'Deprecate whatever([#123](https://github.com/ExampleUsernameOrOrganization/ExampleRepository))', + description: 'Deprecate whatever(#123)', prNumbers: [], }, ], @@ -1263,7 +1509,7 @@ describe('parseChangelog', () => { Added: [ { // Incorrect URL - description: 'Initial release ([#789](https://example.com)', + description: 'Initial release (#789', prNumbers: [], }, ], @@ -1273,19 +1519,12 @@ describe('parseChangelog', () => { describe('when shouldExtractPrLinks is false', () => { it('should not parse pull request links after changelog entries specially', () => { - const changelog = parseChangelog({ - changelogContent: outdent` - # 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] - + const changelogContent = createChangelog( + COMMON_HEADER, + outdent` ## [1.0.0] ### Changed - - Change something else ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200)) + - Change something else ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200)) ## [0.0.2] ### Fixed @@ -1294,22 +1533,19 @@ describe('parseChangelog', () => { ## [0.0.1] ### Added - Initial release ([#456](anything goes here actually)) - - [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 `, - repoUrl: - 'https://github.com/ExampleUsernameOrOrganization/ExampleRepository', + COMMON_REFERENCE_LINKS_3, + ); + const changelog = parseChangelog({ + changelogContent, + repoUrl, shouldExtractPrLinks: false, }); expect(changelog.getReleaseChanges('1.0.0')).toStrictEqual({ Changed: [ { - description: - 'Change something else ([#100](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/100), [#200](https://github.com/ExampleUsernameOrOrganization/ExampleRepository/pull/200))', + description: `Change something else ([#100](${repoUrl}/pull/100), [#200](${repoUrl}/pull/200))`, prNumbers: [], }, ], diff --git a/src/parse-changelog.ts b/src/parse-changelog.ts index d037672..7823ca5 100644 --- a/src/parse-changelog.ts +++ b/src/parse-changelog.ts @@ -214,53 +214,57 @@ function extractPrLinks(changeEntry: string): { description: string; prNumbers: string[]; } { - const [firstLine, ...otherLines] = changeEntry.split('\n'); - const parentheticalMatches = firstLine.matchAll(/[ ]\((\[.+?\]\(.+?\))\)/gu); + const lines = changeEntry.split('\n'); const prNumbers: string[] = []; - let startIndex = Infinity; - let endIndex = 0; - for (const parentheticalMatch of parentheticalMatches) { - const { index } = parentheticalMatch; - const wholeMatch = parentheticalMatch[0]; - const parts = wholeMatch.split(/,[ ]?/u); + // Only process the first line for PR link extraction + let firstLine = lines[0]; - for (const part of parts) { - const regexp = /\[#(\d+)\]\([^()]+\)/u; - const match = part.match(regexp); - if (match !== null) { - const prNumber = match[1]; - prNumbers.push(prNumber); - } - } + // Example of long match group: " ([#123](...), [#456](...))" + const longGroupMatchPattern = /\s+\(\s*(\[#\d+\]\([^)]+\)\s*,?\s*)+\)/gu; - if (index === undefined) { - throw new Error('Could not find index. This should not happen.'); - } - - if (index < startIndex) { - startIndex = index; - } + // Example of long match: "[#123](...)" + const longMatchPattern = /\[#(\d+)\]\([^)]+\)/gu; - if (index + wholeMatch.length > endIndex) { - endIndex = index + wholeMatch.length; + // Match and extract all long PR links like ([#123](...)) separated by commas + const longGroupMatches = [...firstLine.matchAll(longGroupMatchPattern)]; + for (const longGroupMatch of longGroupMatches) { + const group = longGroupMatch[0]; + const longMatches = [...group.matchAll(longMatchPattern)]; + for (const match of longMatches) { + prNumbers.push(match[1]); } } - const uniquePrNumbers = [...new Set(prNumbers)]; + // Remove valid long PR links (grouped in parentheses, possibly comma-separated) + firstLine = firstLine.replace(longGroupMatchPattern, ''); - if (prNumbers.length > 0) { - const firstLineWithoutPrLinks = - firstLine.slice(0, startIndex) + firstLine.slice(endIndex); + // Example of short match group: " (#123), (#123)" + const shortGroupMatchPattern = /\s+(\(#\d+\)\s*,?\s*)+/gu; - return { - description: [firstLineWithoutPrLinks, ...otherLines].join('\n'), - prNumbers: uniquePrNumbers, - }; + // Example of short match: "(#123)" + const shortMatchPattern = /\(#(\d+)\)/gu; + + // Match and extract all short PR links like (#123) + const shortGroupMatches = [...firstLine.matchAll(shortGroupMatchPattern)]; + for (const shortGroupMatch of shortGroupMatches) { + const group = shortGroupMatch[0]; + const shortMatches = [...group.matchAll(shortMatchPattern)]; + for (const match of shortMatches) { + prNumbers.push(match[1]); + } } + // Remove valid short PR links + firstLine = firstLine.replace(shortGroupMatchPattern, ''); + + // Prepare the cleaned description + const cleanedLines = [firstLine.trim(), ...lines.slice(1)]; + const cleanedDescription = cleanedLines.join('\n'); + + // Return unique PR numbers and the cleaned description return { - description: changeEntry, - prNumbers: [], + description: cleanedDescription.trim(), + prNumbers: [...new Set(prNumbers)], }; } diff --git a/src/update-changelog.ts b/src/update-changelog.ts index 06dad9e..ca7e53d 100644 --- a/src/update-changelog.ts +++ b/src/update-changelog.ts @@ -150,6 +150,7 @@ export async function updateChangelog({ tagPrefix: tagPrefixes[0], formatter, packageRename, + shouldExtractPrLinks: true, // By setting this to true, we ensure we don't re-add a PR to the changelog if it was already added in previous releases }); const mostRecentTag = await getMostRecentTag({ @@ -207,7 +208,7 @@ export async function updateChangelog({ } } - const newChangelogContent = await changelog.toString(); + const newChangelogContent = await changelog.toString(useShortPrLink); const isChangelogUpdated = changelogContent !== newChangelogContent; return isChangelogUpdated ? newChangelogContent : undefined; } @@ -266,5 +267,11 @@ export function getCategory(description: string): ChangeCategory { function checkIfDescriptionIndicatesExcluded(description: string): boolean { const _description = description.toLowerCase(); - return keywordsToIndicateExcluded.some((word) => _description.includes(word)); + const keywordsToIndicateExcludedLowerCase = keywordsToIndicateExcluded.map( + (word) => word.toLowerCase(), + ); + + return keywordsToIndicateExcludedLowerCase.some((word) => + _description.includes(word), + ); }