Skip to content
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

package rename changelog validation #157

Merged
merged 17 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
192 changes: 154 additions & 38 deletions src/changelog.ts
MajorLift marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,57 @@ 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 [versionBeforePackageRename] - A version string of the package before being renamed.
mcmire marked this conversation as resolved.
Show resolved Hide resolved
* An optional, which is required only in case of package renamed.
* @param [tagPrefixBeforePackageRename] - 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[],
versionBeforePackageRename?: string,
mcmire marked this conversation as resolved.
Show resolved Hide resolved
tagPrefixBeforePackageRename?: 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;
const unreleasedLinkReferenceDefinition =
getUnreleasedLinkReferenceDefinition(
repoUrl,
tagPrefix,
releases,
versionBeforePackageRename,
tagPrefixBeforePackageRename,
);

const releaseLinkReferenceDefinitions = getReleaseLinkReferenceDefinitions(
repoUrl,
tagPrefix,
releases,
versionBeforePackageRename,
tagPrefixBeforePackageRename,
).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 [versionBeforePackageRename] - A version string of the package before being renamed.
* @param [tagPrefixBeforePackageRename] - 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[],
versionBeforePackageRename?: string,
tagPrefixBeforePackageRename?: string,
): string {
// 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 +225,109 @@ function stringifyLinkReferenceDefinitions(
//
// If there have not been any releases yet, the repo URL is used directly as
// the link definition.
const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${

// 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/test@0.0.2...HEAD
const tagPrefixToCompare =
tagPrefixBeforePackageRename &&
versionBeforePackageRename === latestSemverVersion
? tagPrefixBeforePackageRename
: tagPrefix;

return `[${unreleased}]: ${
hasReleases
? getCompareUrl(repoUrl, `${tagPrefix}${latestSemverVersion}`, 'HEAD')
? 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 [versionBeforePackageRename] - A version string of the package before being renamed.
* @param [tagPrefixBeforePackageRename] - 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[],
versionBeforePackageRename?: string,
tagPrefixBeforePackageRename?: string,
): string[] {
// 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
mcmire marked this conversation as resolved.
Show resolved Hide resolved
const chronologicalVersions = releases.map(({ version }) => version);
let tagPrefixToCompare = tagPrefix;
const releaseLinkReferenceDefinitions = releases.map(({ version }) => {
let diffUrl;
// once the version matches with versionBeforePackageRename, rest of the lines in changelog will be assumed as migrated tags
if (
tagPrefixBeforePackageRename &&
versionBeforePackageRename === version
) {
tagPrefixToCompare = tagPrefixBeforePackageRename;
}

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);
});

if (previousVersion) {
if (
tagPrefixBeforePackageRename &&
versionBeforePackageRename === previousVersion
) {
// The package was renamed at this version
// (the tag prefix holds the new name).
diffUrl = getCompareUrl(
repoUrl,
`${tagPrefixBeforePackageRename}${previousVersion}`,
`${tagPrefix}${version}`,
);
} else {
// If the package was ever renamed, it was not renamed at this version,
// so use either the old tag prefix or the new tag prefix.
// If the package was never renamed, use the tag prefix as it is.
diffUrl = getCompareUrl(
repoUrl,
`${tagPrefixToCompare}${previousVersion}`,
`${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}`,
)
: getTagUrl(repoUrl, `${tagPrefix}${version}`);
// This is the smallest release.
diffUrl = 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 +364,42 @@ export default class Changelog {

#formatter: Formatter;

readonly #versionBeforePackageRename: string | undefined;

readonly #tagPrefixBeforePackageRename: string | undefined;

/**
* 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.versionBeforePackageRename] - A version string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @param [options.tagPrefixBeforePackageRename] - 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,
versionBeforePackageRename = undefined,
tagPrefixBeforePackageRename = undefined,
}: {
repoUrl: string;
tagPrefix?: string;
formatter?: Formatter;
versionBeforePackageRename?: string | undefined;
tagPrefixBeforePackageRename?: string | undefined;
}) {
this.#releases = [];
this.#changes = { [unreleased]: {} };
this.#repoUrl = repoUrl;
this.#tagPrefix = tagPrefix;
this.#formatter = formatter;
this.#versionBeforePackageRename = versionBeforePackageRename;
this.#tagPrefixBeforePackageRename = tagPrefixBeforePackageRename;
}

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

return this.#formatter(changelog);
Expand Down
28 changes: 28 additions & 0 deletions src/cli.ts
MajorLift marked this conversation as resolved.
Show resolved Hide resolved
MajorLift marked this conversation as resolved.
Show resolved Hide resolved
mcmire marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ type ValidateOptions = {
tagPrefix: string;
fix: boolean;
formatter: Formatter;
/**
* Used in case of package renamed
*/
versionBeforePackageRename?: string;
/**
* Used in case of package renamed
*/
tagPrefixBeforePackageRename?: string;
};

/**
Expand All @@ -155,6 +163,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.versionBeforePackageRename] - A version of the package before being renamed.
* An optional, which is required only in case of package renamed.
mcmire marked this conversation as resolved.
Show resolved Hide resolved
* @param [options.tagPrefixBeforePackageRename] - 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 +176,8 @@ async function validate({
tagPrefix,
fix,
formatter,
versionBeforePackageRename,
tagPrefixBeforePackageRename,
}: ValidateOptions) {
const changelogContent = await readChangelog(changelogPath);

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

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

Expand Down Expand Up @@ -455,6 +481,8 @@ async function main() {
tagPrefix,
fix,
formatter,
versionBeforePackageRename,
tagPrefixBeforePackageRename,
});
} 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.versionBeforePackageRename] - A version string of the package before being renamed.
* An optional, which is required only in case of package renamed.
* @param [options.tagPrefixBeforePackageRename] - 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,
versionBeforePackageRename = undefined,
tagPrefixBeforePackageRename = undefined,
}: {
changelogContent: string;
repoUrl: string;
tagPrefix?: string;
formatter?: Formatter;
versionBeforePackageRename?: string;
tagPrefixBeforePackageRename?: string;
}) {
const changelogLines = changelogContent.split('\n');
const changelog = new Changelog({ repoUrl, tagPrefix, formatter });
const changelog = new Changelog({
mcmire marked this conversation as resolved.
Show resolved Hide resolved
repoUrl,
tagPrefix,
formatter,
versionBeforePackageRename,
tagPrefixBeforePackageRename,
});

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