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 all 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
167 changes: 131 additions & 36 deletions src/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
unreleased,
Version,
} from './constants';
import { PackageRename } from './shared-types';

const changelogTitle = '# Changelog';
const changelogDescription = `All notable changes to this project will be documented in this file.
Expand Down Expand Up @@ -165,24 +166,50 @@ 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 packageRename - The package rename properties
* 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[],
packageRename?: PackageRename,
) {
// 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,
packageRename,
);

const releaseLinkReferenceDefinitions = getReleaseLinkReferenceDefinitions(
repoUrl,
tagPrefix,
releases,
packageRename,
).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 packageRename - The package rename properties.
* @returns A unreleased link reference definition string.
*/
function getUnreleasedLinkReferenceDefinition(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
packageRename?: PackageRename,
): 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 @@ -193,42 +220,102 @@ 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/[email protected]
const tagPrefixToCompare =
packageRename && packageRename.versionBeforeRename === latestSemverVersion
? packageRename.tagPrefixBeforeRename
: 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 packageRename - The package rename properties.
* @returns A list of release link reference definitions.
*/
function getReleaseLinkReferenceDefinitions(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
packageRename?: PackageRename,
): 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}`);
const chronologicalVersions = releases.map(({ version }) => version);
let tagPrefixToCompare = tagPrefix;
const releaseLinkReferenceDefinitions = releases.map(({ version }) => {
let diffUrl;
// once the version matches with versionBeforeRename, rest of the lines in changelog will be assumed as migrated tags
if (packageRename && packageRename.versionBeforeRename === version) {
tagPrefixToCompare = packageRename.tagPrefixBeforeRename;
}

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 (
packageRename &&
packageRename.versionBeforeRename === previousVersion
) {
// The package was renamed at this version
// (the tag prefix holds the new name).
diffUrl = getCompareUrl(
repoUrl,
`${packageRename.tagPrefixBeforeRename}${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 @@ -265,28 +352,35 @@ export default class Changelog {

#formatter: Formatter;

readonly #packageRename: PackageRename | 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.packageRename - The package rename properties.
* An optional, which is required only in case of package renamed.
*/
constructor({
repoUrl,
tagPrefix = 'v',
formatter = (changelog) => changelog,
packageRename,
}: {
repoUrl: string;
tagPrefix?: string;
formatter?: Formatter;
packageRename?: PackageRename;
}) {
this.#releases = [];
this.#changes = { [unreleased]: {} };
this.#repoUrl = repoUrl;
this.#tagPrefix = tagPrefix;
this.#formatter = formatter;
this.#packageRename = packageRename;
}

/**
Expand Down Expand Up @@ -468,6 +562,7 @@ ${stringifyLinkReferenceDefinitions(
this.#repoUrl,
this.#tagPrefix,
this.#releases,
this.#packageRename,
)}`;

return this.#formatter(changelog);
Expand Down
36 changes: 36 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { unreleased, Version } from './constants';
import { generateDiff } from './generate-diff';
import { createEmptyChangelog } from './init';
import { getRepositoryUrl } from './repo';
import { PackageRename } from './shared-types';
import { updateChangelog } from './update-changelog';
import {
ChangelogFormattingError,
Expand Down Expand Up @@ -142,6 +143,10 @@ type ValidateOptions = {
tagPrefix: string;
fix: boolean;
formatter: Formatter;
/**
* The package rename properties, used in case of package is renamed
*/
packageRename?: PackageRename;
};

/**
Expand All @@ -155,6 +160,8 @@ 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.packageRename - The package rename properties.
* An optional, which is required only in case of package renamed.
*/
async function validate({
changelogPath,
Expand All @@ -164,6 +171,7 @@ async function validate({
tagPrefix,
fix,
formatter,
packageRename,
}: ValidateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -175,6 +183,7 @@ async function validate({
isReleaseCandidate,
tagPrefix,
formatter,
packageRename,
});
return undefined;
} catch (error) {
Expand Down Expand Up @@ -257,6 +266,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 +349,8 @@ async function main() {
tagPrefix,
fix,
prettier: usePrettier,
versionBeforePackageRename,
tagPrefixBeforePackageRename,
} = argv;
let { currentVersion } = argv;

Expand Down Expand Up @@ -408,6 +427,15 @@ async function main() {
return exitWithError(`Invalid repo URL: '${repoUrl}'`);
}

if (
(versionBeforePackageRename && !tagPrefixBeforePackageRename) ||
(!versionBeforePackageRename && tagPrefixBeforePackageRename)
) {
return exitWithError(
'--version-before-package-rename and --tag-prefix-before-package-rename must be given together or not at all.',
);
}

let changelogPath = changelogFilename;
if (!path.isAbsolute(changelogFilename) && projectRootDirectory) {
changelogPath = path.resolve(projectRootDirectory, changelogFilename);
Expand Down Expand Up @@ -447,6 +475,13 @@ async function main() {
formatter,
});
} else if (command === 'validate') {
let packageRename: PackageRename | undefined;
if (versionBeforePackageRename && tagPrefixBeforePackageRename) {
packageRename = {
versionBeforeRename: versionBeforePackageRename,
tagPrefixBeforeRename: tagPrefixBeforePackageRename,
};
}
await validate({
changelogPath,
currentVersion,
Expand All @@ -455,6 +490,7 @@ async function main() {
tagPrefix,
fix,
formatter,
packageRename,
});
} else if (command === 'init') {
await init({
Expand Down
Loading