Skip to content

Commit

Permalink
Support alternate tag prefixes (#120)
Browse files Browse the repository at this point in the history
We should support alternate prefixes in version tags. At the moment the
project assumes that the prefix v is always used. But some projects use
no prefix, or include the package as a prefix to the version (e.g. in
monorepos).

All public-facing functions now support a tag prefix option. It is
optional, so this is a non-breaking change. The `updateChangelog`
method accepts an array of tag prefixes instead of a single one,
so that we can better support the first release of a monorepo where
you might want to compare against a backup tag if the first isn't
found.

Fixes #116
  • Loading branch information
Gudahtt authored Nov 3, 2022
1 parent 53a6d42 commit 5cc4d22
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 23 deletions.
12 changes: 11 additions & 1 deletion src/changelog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

describe('Changelog', () => {
it('should allow creating an empty changelog', () => {
const changelog = new Changelog({ repoUrl: 'fake://metamask.io' });
const changelog = new Changelog({
repoUrl: 'fake://metamask.io',
});
expect(changelog.toString()).toStrictEqual(emptyChangelog);
});

it('should allow creating an empty changelog with a custom tag prefix', () => {
const changelog = new Changelog({
repoUrl: 'fake://metamask.io',
tagPrefix: 'example@v',
});
expect(changelog.toString()).toStrictEqual(emptyChangelog);
});
});
32 changes: 26 additions & 6 deletions src/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,13 @@ function getTagUrl(repoUrl: string, tag: string) {
* previous release.
*
* @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.
* @returns The stringified release link definitions.
*/
function stringifyLinkReferenceDefinitions(
repoUrl: string,
tagPrefix: string,
releases: ReleaseMetadata[],
) {
// A list of release versions in descending SemVer order
Expand All @@ -187,7 +189,7 @@ function stringifyLinkReferenceDefinitions(
// the link definition.
const unreleasedLinkReferenceDefinition = `[${unreleased}]: ${
hasReleases
? getCompareUrl(repoUrl, `v${latestSemverVersion}`, 'HEAD')
? getCompareUrl(repoUrl, `${tagPrefix}${latestSemverVersion}`, 'HEAD')
: withTrailingSlash(repoUrl)
}`;

Expand All @@ -199,7 +201,7 @@ function stringifyLinkReferenceDefinitions(
.map(({ version }) => {
let diffUrl;
if (version === chronologicalVersions[chronologicalVersions.length - 1]) {
diffUrl = getTagUrl(repoUrl, `v${version}`);
diffUrl = getTagUrl(repoUrl, `${tagPrefix}${version}`);
} else {
const versionIndex = chronologicalVersions.indexOf(version);
const previousVersion = chronologicalVersions
Expand All @@ -208,8 +210,12 @@ function stringifyLinkReferenceDefinitions(
return semver.gt(version, releaseVersion);
});
diffUrl = previousVersion
? getCompareUrl(repoUrl, `v${previousVersion}`, `v${version}`)
: getTagUrl(repoUrl, `v${version}`);
? getCompareUrl(
repoUrl,
`${tagPrefix}${previousVersion}`,
`${tagPrefix}${version}`,
)
: getTagUrl(repoUrl, `${tagPrefix}${version}`);
}
return `[${version}]: ${diffUrl}`;
})
Expand Down Expand Up @@ -249,16 +255,26 @@ export default class Changelog {

private _repoUrl: string;

private _tagPrefix: string;

/**
* 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.
*/
constructor({ repoUrl }: { repoUrl: string }) {
constructor({
repoUrl,
tagPrefix = 'v',
}: {
repoUrl: string;
tagPrefix?: string;
}) {
this._releases = [];
this._changes = { [unreleased]: {} };
this._repoUrl = repoUrl;
this._tagPrefix = tagPrefix;
}

/**
Expand Down Expand Up @@ -436,6 +452,10 @@ ${changelogDescription}
${stringifyReleases(this._releases, this._changes)}
${stringifyLinkReferenceDefinitions(this._repoUrl, this._releases)}`;
${stringifyLinkReferenceDefinitions(
this._repoUrl,
this._tagPrefix,
this._releases,
)}`;
}
}
23 changes: 21 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ type UpdateOptions = {
repoUrl: string;
isReleaseCandidate: boolean;
projectRootDirectory?: string;
tagPrefix: string;
};

/**
Expand All @@ -108,13 +109,15 @@ type UpdateOptions = {
* @param options.isReleaseCandidate - Whether the current branch is a release candidate or not.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.projectRootDirectory - The root project directory.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
async function update({
changelogPath,
currentVersion,
isReleaseCandidate,
repoUrl,
projectRootDirectory,
tagPrefix,
}: UpdateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -124,6 +127,7 @@ async function update({
repoUrl,
isReleaseCandidate,
projectRootDirectory,
tagPrefixes: [tagPrefix],
});

if (newChangelogContent) {
Expand All @@ -139,6 +143,7 @@ type ValidateOptions = {
currentVersion?: Version;
isReleaseCandidate: boolean;
repoUrl: string;
tagPrefix: string;
};

/**
Expand All @@ -149,12 +154,14 @@ type ValidateOptions = {
* @param options.currentVersion - The current project version.
* @param options.isReleaseCandidate - Whether the current branch is a release candidate or not.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
async function validate({
changelogPath,
currentVersion,
isReleaseCandidate,
repoUrl,
tagPrefix,
}: ValidateOptions) {
const changelogContent = await readChangelog(changelogPath);

Expand All @@ -164,6 +171,7 @@ async function validate({
currentVersion,
repoUrl,
isReleaseCandidate,
tagPrefix,
});
} catch (error) {
if (error instanceof ChangelogFormattingError) {
Expand All @@ -182,6 +190,7 @@ async function validate({
type InitOptions = {
changelogPath: string;
repoUrl: string;
tagPrefix: string;
};

/**
Expand All @@ -190,9 +199,10 @@ type InitOptions = {
* @param options - Initialization options.
* @param options.changelogPath - The path to the changelog file.
* @param options.repoUrl - The GitHub repository URL for the current project.
* @param options.tagPrefix - The prefix used in tags before the version number.
*/
async function init({ changelogPath, repoUrl }: InitOptions) {
const changelogContent = await createEmptyChangelog({ repoUrl });
async function init({ changelogPath, repoUrl, tagPrefix }: InitOptions) {
const changelogContent = await createEmptyChangelog({ repoUrl, tagPrefix });
await saveChangelog(changelogPath, changelogContent);
}

Expand Down Expand Up @@ -222,6 +232,11 @@ function configureCommonCommandOptions(_yargs: Argv) {
.option('root', {
description: rootDescription,
type: 'string',
})
.option('tagPrefix', {
default: 'v',
description: 'The prefix used in tags before the version number.',
type: 'string',
});
}

Expand Down Expand Up @@ -282,6 +297,7 @@ async function main() {
rc: isReleaseCandidate,
repo: repoUrl,
root: projectRootDirectory,
tagPrefix,
} = argv;

if (isReleaseCandidate && !currentVersion) {
Expand Down Expand Up @@ -358,18 +374,21 @@ async function main() {
isReleaseCandidate,
repoUrl,
projectRootDirectory,
tagPrefix,
});
} else if (command === 'validate') {
await validate({
changelogPath,
currentVersion,
isReleaseCandidate,
repoUrl,
tagPrefix,
});
} else if (command === 'init') {
await init({
changelogPath,
repoUrl,
tagPrefix,
});
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ describe('createEmptyChangelog', () => {
emptyChangelog,
);
});

it('creates an empty changelog with a custom tag prefix', () => {
expect(
createEmptyChangelog({ repoUrl: exampleRepoUrl, tagPrefix: 'foo' }),
).toStrictEqual(emptyChangelog);
});
});
11 changes: 9 additions & 2 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import Changelog from './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.
* @returns The initial changelog text.
*/
export function createEmptyChangelog({ repoUrl }: { repoUrl: string }) {
const changelog = new Changelog({ repoUrl });
export function createEmptyChangelog({
repoUrl,
tagPrefix = 'v',
}: {
repoUrl: string;
tagPrefix?: string;
}) {
const changelog = new Changelog({ repoUrl, tagPrefix });
return changelog.toString();
}
5 changes: 4 additions & 1 deletion src/parse-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,20 @@ 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.
* @returns A changelog instance that reflects the changelog text provided.
*/
export function parseChangelog({
changelogContent,
repoUrl,
tagPrefix = 'v',
}: {
changelogContent: string;
repoUrl: string;
tagPrefix?: string;
}) {
const changelogLines = changelogContent.split('\n');
const changelog = new Changelog({ repoUrl });
const changelog = new Changelog({ repoUrl, tagPrefix });

const unreleasedHeaderIndex = changelogLines.indexOf(`## [${unreleased}]`);
if (unreleasedHeaderIndex === -1) {
Expand Down
51 changes: 41 additions & 10 deletions src/update-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,41 @@ import { ChangeCategory, Version } from './constants';
import type Changelog from './changelog';

/**
* Get the most recent tag.
* Get the most recent tag for a project.
*
* @param options - Options.
* @param options.tagPrefixes - A list of tag prefixes to look for, where the first is the intended
* prefix and each subsequent prefix is a fallback in case the previous tag prefixes are not found.
* @returns The most recent tag.
*/
async function getMostRecentTag() {
const revListArgs = ['rev-list', '--tags', '--max-count=1', '--date-order'];
const results = await runCommand('git', revListArgs);
if (results.length === 0) {
async function getMostRecentTag({
tagPrefixes,
}: {
tagPrefixes: [string, ...string[]];
}) {
let mostRecentTagCommitHash: string | null = null;
for (const tagPrefix of tagPrefixes) {
const revListArgs = [
'rev-list',
`--tags=${tagPrefix}*`,
'--max-count=1',
'--date-order',
];
const results = await runCommand('git', revListArgs);
if (results.length) {
mostRecentTagCommitHash = results[0];
break;
}
}

if (mostRecentTagCommitHash === null) {
return null;
}
const [mostRecentTagCommitHash] = results;
const [mostRecentTag] = await runCommand('git', [
'describe',
'--tags',
mostRecentTagCommitHash,
]);
assert.equal(mostRecentTag?.[0], 'v', 'Most recent tag should start with v');
return mostRecentTag;
}

Expand Down Expand Up @@ -140,6 +158,7 @@ export type UpdateChangelogOptions = {
repoUrl: string;
isReleaseCandidate: boolean;
projectRootDirectory?: string;
tagPrefixes?: [string, ...string[]];
};

/**
Expand All @@ -159,6 +178,8 @@ export type UpdateChangelogOptions = {
* filter results from various git commands. This path is assumed to be either
* absolute, or relative to the current directory. Defaults to the root of the
* current git repository.
* @param options.tagPrefixes - A list of tag prefixes to look for, where the first is the intended
* prefix and each subsequent prefix is a fallback in case the previous tag prefixes are not found.
* @returns The updated changelog text.
*/
export async function updateChangelog({
Expand All @@ -167,19 +188,29 @@ export async function updateChangelog({
repoUrl,
isReleaseCandidate,
projectRootDirectory,
tagPrefixes = ['v'],
}: UpdateChangelogOptions) {
if (isReleaseCandidate && !currentVersion) {
throw new Error(
`A version must be specified if 'isReleaseCandidate' is set.`,
);
}
const changelog = parseChangelog({ changelogContent, repoUrl });
const changelog = parseChangelog({
changelogContent,
repoUrl,
tagPrefix: tagPrefixes[0],
});

// Ensure we have all tags on remote
await runCommand('git', ['fetch', '--tags']);
const mostRecentTag = await getMostRecentTag();
const mostRecentTag = await getMostRecentTag({
tagPrefixes,
});

if (isReleaseCandidate && mostRecentTag === `v${currentVersion}`) {
if (
isReleaseCandidate &&
mostRecentTag === `${tagPrefixes[0]}${currentVersion}`
) {
throw new Error(
`Current version already has tag, which is unexpected for a release candidate.`,
);
Expand Down
Loading

0 comments on commit 5cc4d22

Please sign in to comment.