@favware/cliff-jumper
Version:
A small CLI tool to create a semantic release and git-cliff powered Changelog
203 lines • 14.1 kB
JavaScript
import { bumpVersion } from '#commands/bump-version';
import { commitRelease } from '#commands/commit-release';
import { createGitHubRelease } from '#commands/create-github-release';
import { createTag } from '#commands/create-tag';
import { getConventionalBump } from '#commands/get-conventional-bump';
import { getNewVersion } from '#commands/get-new-version';
import { installDependencies } from '#commands/install-dependencies';
import { pushTag } from '#commands/push-tag';
import { stageFiles } from '#commands/stage-files';
import { updateChangelog } from '#commands/update-changelog';
import { cliRootDir, indent } from '#lib/constants';
import { logVerboseError, logVerboseInfo } from '#lib/logger';
import { parseOptionsFile } from '#lib/options-parser';
import { preflightChecks } from '#lib/preflight-checks';
import { doActionAndLog, getFullPackageName, getGitRepo, getGitToken, getReleaseType, resolveInstallCommand, resolvePublishCommand, resolveUsedPackageManager } from '#lib/utils';
import { isNullishOrEmpty } from '@sapphire/utilities';
import { blue, blueBright, cyan, green, yellow } from 'colorette';
import { Command, InvalidArgumentError } from 'commander';
import { readFile } from 'node:fs/promises';
import { URL } from 'node:url';
const packageManagerUsed = resolveUsedPackageManager();
const installCommand = resolveInstallCommand(packageManagerUsed);
const packageFile = new URL('package.json', cliRootDir);
const packageJson = JSON.parse(await readFile(packageFile, 'utf-8'));
const monoRepoDescription = [
'Whether the package to be bumped resides in a mono repo,',
'which enables Lerna-like scanning for what kind of version bump should be applied',
'Defaults to "true" when "org" is set, false otherwise'
].join('\n');
const skipChangelogDescription = [
'Whether to skip updating your changelog file', //
'default "true" when CI=true, "false" otherwise'
].join('\n');
const skipTagDescription = [
'Whether to skip creating a git tag', //
'default "true" when CI=true, "false" otherwise'
].join('\n');
const githubReleaseDescription = [
'Note that this is only supported if "--git-host-variant" is set to "github"',
'Whether to create a release on GitHub, requires "--push-tag" to be enabled, otherwise there will be no tag to create a release from',
'For the repository the release is created on the value from "--git-repo" will be used',
'If the changelog section from git-cliff is empty, the release notes will be auto-generated by GitHub.'
].join('\n');
const pushTagDescription = [
'Whether to push the tag to the remote repository.',
'This will simply execute "git push && git push --tags" so make sure you have configured git for pushing properly beforehand.'
].join('\n');
const command = new Command()
.version(packageJson.version)
.option('-n, --name <string>', 'The package name to release')
.option('-p, --package-path <string>', 'The path to the current package. For non-monorepos this is just "."')
.option('--dry-run', 'Whether the package should be bumped or not. When this is set no actions will be taken and only the release strategy will be logged')
.option('--skip-automatic-bump', 'Whether to skip bumping the version (useful if this is the first version, or if you have manually set the version)')
.option('--mono-repo', monoRepoDescription)
.option('--no-mono-repo', monoRepoDescription)
.option('-o, --org <string>', 'The NPM org scope that should be used WITHOUT "@" sign or trailing "/"')
.option('--preid [string]', 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver')
.option('--identifier-base <number>', 'The base number (0 or 1) to be used for the prerelease identifier.', (input) => {
if (input === '0' || input === '1')
return input;
throw new InvalidArgumentError('Not a number of 0 or 1.');
})
.option('--no-identifier-base', 'Do not use a base number for the prerelease identifier.')
.option('-c, --commit-message-template [string]', [
'A custom commit message template to use.',
'Defaults to "chore({{name}}): release {{full-name}}@{{new-version}}"',
'You can use "{{new-version}}" in your template which will be dynamically replaced with whatever the new version is that will be published.',
'You can use "{{name}}" in your template, this will be replaced with the name provided through "-n", "--name" or the same value set in your config file.',
'You can use "{{full-name}}" in your template, this will be replaced "{{name}}" (when "org" is not provided), or "@{{org}}/{{name}}" (when "org" is provided).'
].join('\n'))
.option('--tag-template [string]', [
'A custom tag template to use.',
'When "org" is provided this will default to "@{{org}}/{{name}}@{{new-version}}", for example "@favware/cliff-jumper@1.0.0"',
'When "org" is not provided this will default to "v{{new-version}}", for example "v1.0.0"',
'You can use "{{new-version}}" in your template which will be dynamically replaced with whatever the new version is that will be published.',
'You can use "{{org}}" in your template, this will be replaced with the org provided through "-o", "--org" or the same value set in your config file.',
'You can use "{{name}}" in your template, this will be replaced with the name provided through "-n", "--name" or the same value set in your config file.',
'You can use "{{full-name}}" in your template, this will be replaced "{{name}}" (when "org" is not provided), or "@{{org}}/{{name}}" (when "org" is provided).'
].join('\n'))
.option('-i, --install', `Whether to run ${installCommand} after bumping the version but before committing and creating a git tag. This is useful when you have a mono repo where bumping one package would then cause the lockfile to be out of date.`)
.option('--skip-changelog', skipChangelogDescription)
.option('--no-skip-changelog', skipChangelogDescription)
.option('-t, --skip-tag', skipTagDescription)
.option('--no-skip-tag', skipTagDescription)
.option('--changelog-prepend-file [string]', 'The file that git-cliff should use for the --prepend flag, defaults to ./CHANGELOG.md. This should be relative to the current working directory.')
.option('--skip-commit [skipCommit...]', 'Repeatable, each will be treated as a new entry. A list of SHA1 commit hashes that will be skipped in the changelog.', (value, previous) => (previous ?? []).concat([value]))
.option('--git-host-variant [gitHostVariant]', 'The git host variant. Git-cliff supports 4 hosting websites, GitHub, GitLab, Gitea, and BitBucket. By setting this option you control which api is used by git-cliff. Defaults to "github" for backwards compatibility.')
.option('--git-repo', [
`The git repository to use for linking to issues and PRs in the changelog.`, //
'You can pass the unique string "auto" to automatically set this value as {{org}}/{{name}} as provided from --org and --name',
'This should be in the format "owner/repo"',
`You can use the "GIT_REPO" environment variable to automatically set this value`
].join('\n'))
.option('--git-token', [
'A token to authenticate requests to the Git host API. This can be a GitHub, GitLab, Gitea, or BitBucket token. Which is used is determined by "--git-host-variant". This is required when using the "--git-repo" option.',
'You can also set the one of the following environment variables.',
'- GITHUB_TOKEN',
'- GITLAB_TOKEN',
'- GITEA_TOKEN',
'- BITBUCKET_TOKEN',
'- GH_TOKEN'
].join('\n'))
.option('--push-tag', pushTagDescription)
.option('--no-push-tag', pushTagDescription)
.option('--github-release', githubReleaseDescription)
.option('--no-github-release', githubReleaseDescription)
.option('--github-release-draft', ['Note that this is only supported if "--git-host-variant" is set to "github"', 'Whether the release should be a draft'].join('\n'))
.option('--github-release-pre-release', ['Note that this is only supported if "--git-host-variant" is set to "github"', 'Whether the release should be a pre-release'].join('\n'))
.option('--github-release-latest', [
'Note that this is only supported if "--git-host-variant" is set to "github"',
'Whether the release should be marked as the latest release, will try to read this value, then the value of --github-release, and then default to false. Please note that when setting --github-release-pre-release to `true` GitHub will prevent the release to be marked as latest an this option will essentially be ignored.'
].join('\n'))
.option('--github-release-name-template [string]', [
'Note that this is only supported if "--git-host-variant" is set to "github"',
'A GitHub release name template to use. Defaults to an empty string, which means GitHub will use the tag name as the release name.',
'You can use "{{new-version}}" in your template which will be dynamically replaced with whatever the new version is that will be published.',
'You can use "{{org}}" in your template, this will be replaced with the org provided through "-o", "--org" or the same value set in your config file.',
'You can use "{{name}}" in your template, this will be replaced with the name provided through "-n", "--name" or the same value set in your config file.',
'You can use "{{full-name}}" in your template, this will be replaced "{{name}}" (when "org" is not provided), or "@{{org}}/{{name}}" (when "org" is provided).'
].join('\n'))
.option('--github-base-url [string]', [
'Note that this is only supported if "--git-host-variant" is set to "github"',
'The base URL for the GitHub API. Defaults to "https://api.github.com".'
].join('\n'))
.option('-v, --verbose', 'Whether to print verbose information', false);
const program = command.parse(process.argv);
const options = await parseOptionsFile(program.opts());
logVerboseInfo([
'Resolved options: ',
`${indent}name: ${JSON.stringify(options.name)}`,
`${indent}package path: ${JSON.stringify(options.packagePath)}`,
`${indent}dry run: ${JSON.stringify(options.dryRun)}`,
`${indent}skip automatic bump: ${JSON.stringify(options.skipAutomaticBump)}`,
`${indent}mono repo: ${JSON.stringify(options.monoRepo)}`,
`${indent}npm org: ${JSON.stringify(options.org)}`,
`${indent}preid: ${JSON.stringify(options.preid)}`,
`${indent}identifier base: ${JSON.stringify(options.identifierBase)}`,
`${indent}commit message template: ${JSON.stringify(options.commitMessageTemplate)}`,
`${indent}tag template: ${JSON.stringify(options.tagTemplate)}`,
`${indent}install: ${JSON.stringify(options.install)}`,
`${indent}skip changelog: ${JSON.stringify(options.skipChangelog)}`,
`${indent}skip tag: ${JSON.stringify(options.skipTag)}`,
`${indent}commits to be skipped: ${JSON.stringify(options.skipCommit)}`,
`${indent}verbose: ${JSON.stringify(options.verbose)}`,
`${indent}changelog prepend file: ${options.changelogPrependFile}`,
`${indent}git host variant: ${options.gitHostVariant}`,
`${indent}git repo: ${JSON.stringify(getGitRepo(options))}`,
`${indent}git token: ${getGitToken(options) ? 'Unset' : 'SECRET([REDACTED])'}`,
`${indent}push tag: ${JSON.stringify(options.pushTag)}`,
`${indent}github release: ${JSON.stringify(options.githubRelease)}`,
`${indent}github release draft: ${JSON.stringify(options.githubReleaseDraft)}`,
`${indent}github release pre-release: ${JSON.stringify(options.githubReleasePrerelease)}`,
`${indent}github release latest: ${JSON.stringify(options.githubReleaseLatest)}`,
`${indent}github release name template: ${JSON.stringify(options.githubReleaseNameTemplate)}`,
`${indent}github base URL: ${JSON.stringify(options.githubBaseUrl)}`,
''
], options.verbose);
await preflightChecks(options);
const fullPackageName = getFullPackageName(options);
const bumperRecommendation = await doActionAndLog('Retrieving the strategy to use for bumping the package', //
getConventionalBump(options));
if (isNullishOrEmpty(bumperRecommendation.reason) || isNullishOrEmpty(bumperRecommendation.releaseType)) {
logVerboseError({
text: [`No recommended bump level found for ${fullPackageName}`],
exitAfterLog: true,
verbose: options.verbose
});
}
const infoIcon = blue('ℹ️');
const releaseType = yellow(`${getReleaseType(options, bumperRecommendation)}`);
console.info(cyan(`${infoIcon} Bumping the ${releaseType} version of ${blueBright(fullPackageName)}: ${yellow(bumperRecommendation.reason)}`));
let newVersion;
if (!options.skipAutomaticBump) {
const resolvedNewVersion = await bumpVersion(options, bumperRecommendation);
newVersion = typeof resolvedNewVersion === 'string' ? resolvedNewVersion : await getNewVersion();
console.log(green(`📦 Bumped ${fullPackageName}@${newVersion}`));
}
if (!options.skipChangelog) {
newVersion = isNullishOrEmpty(newVersion) ? await getNewVersion() : newVersion;
const changelogSection = await updateChangelog(options, newVersion);
if (!options.skipTag) {
if (options.install) {
await installDependencies(options, packageManagerUsed);
}
await stageFiles(options, packageManagerUsed);
await commitRelease(options, newVersion);
await createTag(options, newVersion);
const publishText = resolvePublishCommand(packageManagerUsed);
if (options.pushTag) {
await pushTag(options);
if (options.gitHostVariant === 'github' && options.githubRelease) {
await createGitHubRelease(options, newVersion, changelogSection);
}
console.info(infoIcon + green(` Run \`${publishText}\` to publish to your package registry`));
}
else {
console.info(infoIcon + green(` Run \`git push && git push --tags && ${publishText}\` to publish to your package registry`));
}
}
}
process.exit(0);
//# sourceMappingURL=cli.js.map