Skip to content

package rename changelog validation #157

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 144 additions & 38 deletions src/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,19 @@ function getTagUrl(repoUrl: string, tag: string) {
* @param repoUrl - The URL for the GitHub repository.
* @param tagPrefix - The prefix used in tags before the version number.
* @param releases - The releases to generate link definitions for.
* @param [versionBeforePkgRename] - A version string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @param [tagPrefixBeforePkgRename] - A tag prefix string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @returns The stringified release link definitions.
*/
function stringifyLinkReferenceDefinitions(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
versionBeforePkgRename?: string,
tagPrefixBeforePkgRename?: string,
) {
// A list of release versions in descending SemVer order
const descendingSemverVersions = releases
.map(({ version }) => version)
.sort((a: Version, b: Version) => {
return semver.gt(a, b) ? -1 : 1;
});
const latestSemverVersion = descendingSemverVersions[0];
// A list of release versions in chronological order
const chronologicalVersions = releases.map(({ version }) => version);
const hasReleases = chronologicalVersions.length > 0;

// 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.
Expand All @@ -192,42 +187,137 @@ function stringifyLinkReferenceDefinitions(
//
// If there have not been any releases yet, the repo URL is used directly as
// the link definition.
const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${
hasReleases
? getCompareUrl(repoUrl, `${tagPrefix}${latestSemverVersion}`, 'HEAD')
: withTrailingSlash(repoUrl)
}`;
const unreleasedLinkReferenceDefinition =
getUnreleasedLinkReferenceDefinition(
repoUrl,
tagPrefix,
releases,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
);

// 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 }) => {
let diffUrl;
if (version === chronologicalVersions[chronologicalVersions.length - 1]) {
diffUrl = getTagUrl(repoUrl, `${tagPrefix}${version}`);
// by default tag prefix from new package will be used
const releaseLinkReferenceDefinitions = getReleaseLinkReferenceDefinitions(
repoUrl,
tagPrefix,
releases,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
).join('\n');
return `${unreleasedLinkReferenceDefinition}\n${releaseLinkReferenceDefinitions}${
releases.length > 0 ? '\n' : ''
}`;
}

/**
* Get a string of unreleased link reference definition.
*
* @param repoUrl - The URL for the GitHub repository.
* @param tagPrefix - The prefix used in tags before the version number.
* @param releases - The releases to generate link definitions for.
* @param [versionBeforePkgRename] - A version string of the package before being renamed.
* @param [tagPrefixBeforePkgRename] - A tag prefix string of the package before being renamed.
* @returns A unreleased link reference definition string.
*/
function getUnreleasedLinkReferenceDefinition(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
versionBeforePkgRename?: string,
tagPrefixBeforePkgRename?: string,
): string {
// A list of release versions in descending SemVer order
const descendingSemverVersions = releases
.map(({ version }) => version)
.sort((a: Version, b: Version) => {
return semver.gt(a, b) ? -1 : 1;
});
const latestSemverVersion = descendingSemverVersions[0];
const hasReleases = descendingSemverVersions.length > 0;
// if there is a package renamed, the tag prefix before the rename will be considered for compare
// [Unreleased]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/[email protected]
const tagPrefixToCompare =
tagPrefixBeforePkgRename && versionBeforePkgRename === latestSemverVersion
? tagPrefixBeforePkgRename
: tagPrefix;

return `[${unreleased}]: ${
hasReleases
? getCompareUrl(
repoUrl,
`${tagPrefixToCompare}${latestSemverVersion}`,
'HEAD',
)
: withTrailingSlash(repoUrl)
}`;
}

/**
* Get a list of release link reference definitions.
*
* @param repoUrl - The URL for the GitHub repository.
* @param tagPrefix - The prefix used in tags before the version number.
* @param releases - The releases to generate link definitions for.
* @param [versionBeforePkgRename] - A version string of the package before being renamed.
* @param [tagPrefixBeforePkgRename] - A tag prefix string of the package before being renamed.
* @returns A list of release link reference definitions.
*/
function getReleaseLinkReferenceDefinitions(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
versionBeforePkgRename?: string,
tagPrefixBeforePkgRename?: string,
): string[] {
const chronologicalVersions = releases.map(({ version }) => version);
let tagPrefixToCompare = tagPrefix;
const releaseLinkReferenceDefinitions = releases.map(({ version }) => {
let diffUrl;
// once the version matches with original version, rest of the lines in changelog will be assumed as migrated tags
if (tagPrefixBeforePkgRename && versionBeforePkgRename === version) {
tagPrefixToCompare = tagPrefixBeforePkgRename;
}
if (version === chronologicalVersions[chronologicalVersions.length - 1]) {
diffUrl = getTagUrl(repoUrl, `${tagPrefixToCompare}${version}`);
} else {
const versionIndex = chronologicalVersions.indexOf(version);
const previousVersion = chronologicalVersions
.slice(versionIndex)
.find((releaseVersion: Version) => {
return semver.gt(version, releaseVersion);
});
// when there is a package renamed below if will fix the validation for renamed package's first release
// In the below example `test` package has been renamed to `@metamask/test`
// [1.0.0]: https://github.com/ExampleUsernameOrOrganization/ExampleRepository/compare/[email protected]...@metamask/[email protected]
if (
tagPrefixBeforePkgRename &&
versionBeforePkgRename === previousVersion
) {
diffUrl = previousVersion
? getCompareUrl(
repoUrl,
`${tagPrefixBeforePkgRename}${previousVersion}`,
`${tagPrefixToCompare}${version}`,
)
: getTagUrl(repoUrl, `${tagPrefixToCompare}${version}`);
} else {
const versionIndex = chronologicalVersions.indexOf(version);
const previousVersion = chronologicalVersions
.slice(versionIndex)
.find((releaseVersion: Version) => {
return semver.gt(version, releaseVersion);
});
diffUrl = previousVersion
? getCompareUrl(
repoUrl,
`${tagPrefix}${previousVersion}`,
`${tagPrefix}${version}`,
`${tagPrefixToCompare}${previousVersion}`,
`${tagPrefixToCompare}${version}`,
)
: getTagUrl(repoUrl, `${tagPrefix}${version}`);
: getTagUrl(repoUrl, `${tagPrefixToCompare}${version}`);
}
return `[${version}]: ${diffUrl}`;
})
.join('\n');
return `${unreleasedLinkReferenceDefinition}\n${releaseLinkReferenceDefinitions}${
releases.length > 0 ? '\n' : ''
}`;
}
return `[${version}]: ${diffUrl}`;
});

return releaseLinkReferenceDefinitions;
}

type AddReleaseOptions = {
Expand Down Expand Up @@ -264,28 +354,42 @@ export default class Changelog {

#formatter: Formatter;

readonly #versionBeforePkgRename: string | undefined;

readonly #tagPrefixBeforePkgRename: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consistent typing for optional properties.

Suggested change
readonly #versionBeforePkgRename: string | undefined;
readonly #tagPrefixBeforePkgRename: string | undefined;
readonly #versionBeforePkgRename?: string;
readonly #tagPrefixBeforePkgRename?: string;

Copy link
Contributor

Choose a reason for hiding this comment

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

An optional property and a property that can be undefined isn't the same thing in TypeScript. An optional property may or may not be set, but it looks like we do set this property in the constructor — it just might be undefined. So this one seems right to me, but does that make sense to you?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I like | undefined better than ?: here, since undefined is explicitly being passed in with these two variables, and not making them optional will prevent the code from breaking if exactOptionalPropertyTypes is turned on.

My concern is around consistent typing for the other properties in these files that are defined as optional. exactOptionalPropertyTypes would enforce that consistency.


/**
* Construct an empty changelog.
*
* @param options - Changelog options.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
* @param options.formatter - A function that formats the changelog string.
* @param [options.tagPrefix] - The prefix used in tags before the version number.
* @param [options.formatter] - A function that formats the changelog string.
* @param [options.versionBeforePkgRename] - A version string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @param [options.tagPrefixBeforePkgRename] - A tag prefix string of the package before being renamed.
* An optional, which is required only in case of package renamed.
*/
constructor({
repoUrl,
tagPrefix = 'v',
formatter = (changelog) => changelog,
versionBeforePkgRename = undefined,
tagPrefixBeforePkgRename = undefined,
}: {
repoUrl: string;
tagPrefix?: string;
formatter?: Formatter;
versionBeforePkgRename?: string | undefined;
tagPrefixBeforePkgRename?: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consistent typing for optional values. With exactOptionalPropertyTypes disabled, the | undefined is redundant.

Suggested change
versionBeforePkgRename?: string | undefined;
tagPrefixBeforePkgRename?: string | undefined;
versionBeforePkgRename?: string;
tagPrefixBeforePkgRename?: string;

}) {
this.#releases = [];
this.#changes = { [unreleased]: {} };
this.#repoUrl = repoUrl;
this.#tagPrefix = tagPrefix;
this.#formatter = formatter;
this.#versionBeforePkgRename = versionBeforePkgRename;
this.#tagPrefixBeforePkgRename = tagPrefixBeforePkgRename;
}

/**
Expand Down Expand Up @@ -467,6 +571,8 @@ ${stringifyLinkReferenceDefinitions(
this.#repoUrl,
this.#tagPrefix,
this.#releases,
this.#versionBeforePkgRename,
this.#tagPrefixBeforePkgRename,
)}`;

return this.#formatter(changelog);
Expand Down
22 changes: 22 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ type ValidateOptions = {
tagPrefix: string;
fix: boolean;
formatter: Formatter;
versionBeforePkgRename?: string; // used in case of package renamed
tagPrefixBeforePkgRename?: string; // used in case of package renamed
};

/**
Expand All @@ -155,6 +157,10 @@ type ValidateOptions = {
* @param options.tagPrefix - The prefix used in tags before the version number.
* @param options.fix - Whether to attempt to fix the changelog or not.
* @param options.formatter - A custom Markdown formatter to use.
* @param [options.versionBeforePkgRename] - A version of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @param [options.tagPrefixBeforePkgRename] - A tag prefix of the package before being renamed.
* An optional, which is required only in case of package renamed.
*/
async function validate({
changelogPath,
Expand All @@ -164,6 +170,8 @@ async function validate({
tagPrefix,
fix,
formatter,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
}: ValidateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -175,6 +183,8 @@ async function validate({
isReleaseCandidate,
tagPrefix,
formatter,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
});
return undefined;
} catch (error) {
Expand Down Expand Up @@ -257,6 +267,14 @@ function configureCommonCommandOptions(_yargs: Argv) {
default: 'v',
description: 'The prefix used in tags before the version number.',
type: 'string',
})
.option('versionBeforePkgRename', {
description: 'A version of the package before being renamed.',
type: 'string',
})
.option('tagPrefixBeforePkgRename', {
description: 'A tag prefix of the package before being renamed.',
type: 'string',
});
}

Expand Down Expand Up @@ -332,6 +350,8 @@ async function main() {
tagPrefix,
fix,
prettier: usePrettier,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
} = argv;
let { currentVersion } = argv;

Expand Down Expand Up @@ -455,6 +475,8 @@ async function main() {
tagPrefix,
fix,
formatter,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
});
} else if (command === 'init') {
await init({
Expand Down
20 changes: 17 additions & 3 deletions src/parse-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,37 @@ function isValidChangeCategory(category: string): category is ChangeCategory {
* @param options - Options.
* @param options.changelogContent - The changelog to parse.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
* @param options.formatter - A custom Markdown formatter to use.
* @param [options.tagPrefix] - The prefix used in tags before the version number.
* @param [options.formatter] - A custom Markdown formatter to use.
* @param [options.versionBeforePkgRename] - A version string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @param [options.tagPrefixBeforePkgRename] - A tag prefix string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @returns A changelog instance that reflects the changelog text provided.
*/
export function parseChangelog({
changelogContent,
repoUrl,
tagPrefix = 'v',
formatter = undefined,
versionBeforePkgRename = undefined,
tagPrefixBeforePkgRename = undefined,
}: {
changelogContent: string;
repoUrl: string;
tagPrefix?: string;
formatter?: Formatter;
versionBeforePkgRename?: string;
tagPrefixBeforePkgRename?: string;
}) {
const changelogLines = changelogContent.split('\n');
const changelog = new Changelog({ repoUrl, tagPrefix, formatter });
const changelog = new Changelog({
repoUrl,
tagPrefix,
formatter,
versionBeforePkgRename,
tagPrefixBeforePkgRename,
});

const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`);
if (unreleasedHeaderIndex === -1) {
Expand Down
Loading