UNPKG

@favware/cliff-jumper

Version:

A small CLI tool to create a semantic release and git-cliff powered Changelog

198 lines 13.8 kB
#!/usr/bin/env node 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('-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)}`, '' ], 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