diff --git a/README.md b/README.md index c1f2ff5..dce264f 100644 --- a/README.md +++ b/README.md @@ -254,20 +254,34 @@ Be sure to replace `project-name` with the appropriate segment of the project UR the [Markdownlint](#markdownlint) section below. Generated changelogs will fail our linting rules and must be excluded from linting. 1. Ensure that the project's `package.json` file has a - [`repository.url`][package-json-link] field with the URL to the canonical repo - for the project in its git hosting solution, e.g. + [`repository.url`][package-json-link] field with the URL to the canonical repo for + the project in its git hosting solution, e.g. for the [@silvermine/event-emitter](https://github.com/silvermine/event-emitter) project. * This is necessary because conventional-changelog needs to know the URL to the git hosting solution so that it can make links to "compare URLs" in the CHANGELOG - 1. Add the following NPM scripts to the project's `package.json` file: + 1. Run release commands via the bundled bin. After `npm install`, the + `silvermine-standardization` executable is on the local `PATH` for npm scripts and + via `npx`. The recommended invocation is: + + ```bash + npx silvermine-standardization release preview + npx silvermine-standardization release prep-changelog + npx silvermine-standardization release finalize + ``` + + Optionally, projects that prefer `npm run` ergonomics can add wrapper scripts to + their `package.json`: ```json - "release:preview": "node ./node_modules/@silvermine/standardization/scripts/release.js preview", - "release:prep-changelog": "node ./node_modules/@silvermine/standardization/scripts/release.js prep-changelog", - "release:finalize": "node ./node_modules/@silvermine/standardization/scripts/release.js finalize" + "release:preview": "silvermine-standardization release preview", + "release:prep-changelog": "silvermine-standardization release prep-changelog", + "release:finalize": "silvermine-standardization release finalize" ``` + The legacy `node ./node_modules/@silvermine/standardization/scripts/release.js …` + invocation still works but is deprecated and will be removed in a future release. + 1. (Optional) If the project is using an issue tracking system other than what the git hosting solution provides (e.g. the code is hosted on GitHub but uses Azure DevOps for issue tracking), add this config to the project's `package.json`: @@ -309,9 +323,14 @@ At a high-level, the process for releasing a new version of a package is: npm run standards && npm test ``` - 1. Run `npm run release:prep-changelog`. You should now be on a branch named - `changelog-v${NEW_VERSION}` containing the automatically generated changelog + 1. Run `npx silvermine-standardization release prep-changelog`. You should now be on a + branch named `changelog-v` containing the automatically generated changelog additions. + * If your project does not need a separate changelog branch (for example, a small + library where committing the changelog directly to the release branch is + preferable to opening a changelog PR), pass `--no-branch` (e.g. + `npx silvermine-standardization release prep-changelog --no-branch`. The changelog + will be committed to the current branch. * If you receive the message "There were no changelog entries generated" and this is expected, please proceed to [Perform the Version Bump](#perform-the-version-bump). * If the changelog needs to be edited, please make the needed adjustments and amend @@ -327,7 +346,7 @@ At a high-level, the process for releasing a new version of a package is: 1. Once the changelog has been merged, checkout and update the branch that is to be released. The last commit should be the merge commit for the updates to the changelog. - 1. Run `npm run release:finalize` + 1. Run `npx silvermine-standardization release finalize` 1. Preview the changes and push the branch and `v${NEW_VERSION}` tag to the correct remote repo 1. If the version should be published and this is not handled by a CI/CD pipeline, run @@ -335,21 +354,22 @@ At a high-level, the process for releasing a new version of a package is: ##### Special Cases -In most cases, `npm run release:preview`, `npm run release:prep-changelog`, and `npm run -release:finalize` will be run without any additional options. However, there are a few +In most cases, `npx silvermine-standardization release preview`, `npx +silvermine-standardization release prep-changelog`, and `npx silvermine-standardization +release finalize` will be run without any additional options. However, there are a few cases when you may need to supply extra options. ###### First Release -When a package is first created, the package.json typically says the version is v0.1.0. -If that's the version you want to generate the changelog for and publish to NPM, there's a +When a package is first created, the package.json typically says the version is v0.1.0. If +that's the version you want to generate the changelog for and publish to NPM, there's a problem. The release script will want to bump the package to v0.2.0 or v0.1.1. As such, a version of 0.1.0 has to be specified using the `--version` option. For example: ```bash -npm run release:preview -- --version 0.1.0 -npm run release:prep-changelog -- --version 0.1.0 -npm run release:finalize -- --version 0.1.0 +npx silvermine-standardization release preview --version 0.1.0 +npx silvermine-standardization release prep-changelog --version 0.1.0 +npx silvermine-standardization release finalize --version 0.1.0 ``` ###### Releasing v1.0.0 @@ -359,9 +379,9 @@ version. As such, a package's version will stay <v1.0.0 until you tell the re script to publish v1.0.0. This can be done using the `--version` option. For example: ```bash -npm run release:preview -- --version 1.0.0 -npm run release:prep-changelog -- --version 1.0.0 -npm run release:finalize -- --version 1.0.0 +npx silvermine-standardization release preview --version 1.0.0 +npx silvermine-standardization release prep-changelog --version 1.0.0 +npx silvermine-standardization release finalize --version 1.0.0 ``` ###### Prerelease Version (e.g. Alpha, Beta, Release Candidate) @@ -373,11 +393,28 @@ a v1.1.0-rc.0 before creating the final v1.1.0. To do this, you can pass a `--pr rc` option (Values like `alpha` and `beta` also work). For example: ```bash -npm run release:preview -- --prerelease rc -npm run release:prep-changelog -- --prerelease rc -npm run release:finalize -- --prerelease rc +npx silvermine-standardization release preview --prerelease rc +npx silvermine-standardization release prep-changelog --prerelease rc +npx silvermine-standardization release finalize --prerelease rc ``` +### CLI + +This package ships a `silvermine-standardization` executable. Available +subcommands: + + * `silvermine-standardization release preview` — preview the next version + and generated changelog entries without modifying the working tree. + * `silvermine-standardization release prep-changelog` — generate the + changelog for the next release. By default, creates a `changelog-v` + branch and commits the changelog to it. Pass `--no-branch` to commit the + changelog directly to the current branch. + * `silvermine-standardization release finalize` — bump the version in + `package.json` and create the `v` git tag. + +Each subcommand accepts `--prerelease ` and `--version `. Run +any subcommand with `--help` for full option detail. + ### Migration to `standards` NPM script We are in the process of migrating away from grunt as a task runner. This being the case, diff --git a/bin/silvermine-standardization.js b/bin/silvermine-standardization.js new file mode 100755 index 0000000..4c47614 --- /dev/null +++ b/bin/silvermine-standardization.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/* eslint no-console: [ "error", { allow: [ "info", "warn", "error" ] } ] */ +'use strict'; + +const commander = require('commander'), + chalk = require('chalk'), + { createReleaseCommand } = require('../scripts/release-command'); + +async function main() { + const program = new commander.Command('silvermine-standardization'); + + program.addCommand(createReleaseCommand({ cwd: process.cwd() })); + program.action(() => { + program.outputHelp(); + }); + program.exitOverride(); + + try { + await program.parseAsync(process.argv); + } catch(err) { + if (err instanceof commander.CommanderError) { + program.outputHelp(); + } else { + console.error(chalk.red.bold(`ERROR: ${err}`) + `\n${err.stack}`); + } + + process.exit(1); // eslint-disable-line no-process-exit + } +} + +main(); diff --git a/package-lock.json b/package-lock.json index 4482ada..416af58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,9 @@ "stylelint-config-standard-scss": "15.0.1", "stylelint-scss": "6.12.1" }, + "bin": { + "silvermine-standardization": "bin/silvermine-standardization.js" + }, "devDependencies": { "@silvermine/eslint-config": "3.0.1", "eslint": "6.8.0" diff --git a/package.json b/package.json index 7403d4c..7169c74 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "test:stylelint": "./bin/run-stylelint-tests.sh", "test": "npm run test:markdownlint && npm run test:stylelint", "commitlint": "commitlint --from f59a7f4498aa76e29c33888d01aec18a3d81b343", - "release:preview": "node ./scripts/release.js preview", - "release:prep-changelog": "node ./scripts/release.js prep-changelog", - "release:finalize": "node ./scripts/release.js finalize", + "release:preview": "bin/silvermine-standardization.js release preview", + "release:prep-changelog": "bin/silvermine-standardization.js release prep-changelog", + "release:finalize": "bin/silvermine-standardization.js release finalize", "markdownlint": "markdownlint-cli2", "eslint": "eslint .", "standards": "npm run commitlint && npm run eslint && npm run markdownlint" @@ -33,6 +33,9 @@ "url": "https://github.com/silvermine/standardization/issues" }, "homepage": "https://github.com/silvermine/standardization#readme", + "bin": { + "silvermine-standardization": "./bin/silvermine-standardization.js" + }, "dependencies": { "@commitlint/cli": "12.1.1", "@silvermine/markdownlint-rule-indent-alignment": "0.2.0", diff --git a/scripts/release-command.js b/scripts/release-command.js new file mode 100644 index 0000000..fd045a4 --- /dev/null +++ b/scripts/release-command.js @@ -0,0 +1,311 @@ +/* eslint no-console: [ "error", { allow: [ "info", "warn", "error" ] } ] */ +'use strict'; + +const commander = require('commander'), + chalk = require('chalk'), + conventionalChangelog = require('conventional-changelog'), + conventionalRecommendedBump = require('conventional-recommended-bump'), + semver = require('semver'), + fs = require('fs'), + path = require('path'), + childProcess = require('child_process'), + util = require('util'), + gitSemverTags = util.promisify(require('git-semver-tags')), + getRecommendedBump = util.promisify(conventionalRecommendedBump), + execFile = util.promisify(childProcess.execFile); + +const CHANGELOG_CONFIG = { + filename: 'CHANGELOG.md', + releaseEntryPattern: /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+| { + let content = ''; + + const changelogOptions = { + preset: { + name: 'conventionalcommits', + issueUrlFormat: opts.issueUrlFormat, + }, + skipUnstable: opts.ignorePrereleaseTags, + }; + + const context = { version: opts.version }; + + const writerOptions = { + generateOn: (commit) => { + if (opts.ignorePrereleaseTags && isPrereleaseVersion(commit.version)) { + return false; + } + + return semver.valid(commit.version); + }, + }; + + const changelogStream = conventionalChangelog(changelogOptions, context, undefined, undefined, writerOptions); + + changelogStream.on('error', (err) => { + return reject(err); + }); + + changelogStream.on('data', (buffer) => { + content += buffer.toString(); + }); + + changelogStream.on('end', () => { + if (content.trim().split('\n').length === 1) { + resolve(undefined); + } + + resolve(content); + }); + }); +} + +async function addEntriesToChangelog(newEntries) { + const existingChangelog = await (fs.promises.readFile(CHANGELOG_CONFIG.filename, 'utf-8').catch(() => { return ''; })), + existingEntriesStartIndex = existingChangelog.search(CHANGELOG_CONFIG.releaseEntryPattern); + + let existingChangelogEntries = CHANGELOG_CONFIG.footer; + + if (existingEntriesStartIndex !== -1) { + existingChangelogEntries = existingChangelog.substring(existingEntriesStartIndex); + } + + const newChangelog = `${CHANGELOG_CONFIG.header}${newEntries}\n${existingChangelogEntries}`; + + await fs.promises.writeFile(CHANGELOG_CONFIG.filename, newChangelog.trimEnd() + '\n'); +} + +async function createNewBranch(name) { + await execFile('git', [ 'checkout', '-b', name ]); +} + +async function resetChangelogToTag(tag) { + await execFile('git', [ 'checkout', tag, '--', CHANGELOG_CONFIG.filename ]); +} + +async function commitChangelog(version) { + await execFile('git', [ 'add', CHANGELOG_CONFIG.filename ]); + await execFile('git', [ 'commit', '-m', `chore: update changelog for v${version}` ]); +} + +async function getCurrentBranchName() { + const output = await execFile('git', [ 'rev-parse', '--abbrev-ref', 'HEAD' ]); + + return output.stdout.trim(); +} + +async function doesRepositoryHaveUncommittedChanges() { + const output = await execFile('git', [ + 'status', + '--porcelain', + ]); + + return output.stdout !== ''; +} + +function createReleaseCommand({ cwd }) { + // Synchronous read is intentional: the factory itself is sync and runs once at + // command-construction time, not in a hot path. + const packageJSON = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'))), // eslint-disable-line no-sync + currentVersion = packageJSON.version, + changelogConfig = packageJSON['silvermine-standardization'] ? packageJSON['silvermine-standardization'].changelog : {}, + prereleaseOption = new commander.Option('--prerelease ', 'The type of prerelease, e.g. alpha, beta, rc'), + versionOption = new commander.Option('--version ', 'The version to use instead of the auto-calculated version'); + + const noBranchOption = new commander.Option( + '--no-branch', + 'Commit the changelog directly to the current branch instead of creating a `changelog-v` branch' + ); + + async function getNextVersion(options) { + if (options.version) { + const providedVersion = semver.clean(options.version); + + if (!providedVersion) { + quitWithError(`Invalid version: ${options.version}`); + } + + if (options.prerelease) { + quitWithError( + 'The --prerelease flag cannot be used with --version.' + + ` Please specify the desired prerelease in the version, e.g. ${providedVersion}-${options.prerelease}.0` + ); + } + + return providedVersion; + } + + return await calculateNextVersion(currentVersion, options.prerelease); + } + + const release = new commander.Command('release') + .description('Release management commands'); + + release.hook('preAction', async () => { + if (await doesRepositoryHaveUncommittedChanges()) { + quitWithError('The repository has uncommitted changes. Please ensure the repository is clean before running this script'); + } + }); + + release + .command('preview') + .description('Preview the version and changelog for the next release') + .addOption(prereleaseOption) + .addOption(versionOption) + .action(async (options) => { + const targetVersion = await getNextVersion(options); + + console.info(chalk.yellow(`Will bump ${packageJSON.name} from v${currentVersion} to v${targetVersion}`)); + console.info(chalk.whiteBright('\nAutogenerated changelog:\n')); + console.info(await createChangelogAdditions({ + version: targetVersion, + ignorePrereleaseTags: !isPrereleaseVersion(targetVersion), + issueUrlFormat: changelogConfig.issueUrlFormat, + })); + }); + + release + .command('prep-changelog') + .description('Generates the changelog for the next release') + .addOption(prereleaseOption) + .addOption(versionOption) + .addOption(noBranchOption) + .action(async (options) => { + const targetVersion = await getNextVersion(options), + isFinalVersion = !isPrereleaseVersion(targetVersion), + useNewBranch = options.branch, + changelogBranch = `changelog-v${targetVersion}`, + pushTarget = useNewBranch ? changelogBranch : await getCurrentBranchName(); + + console.info(`Generating changelog for ${packageJSON.name}@${targetVersion}...`); + + const changelogAdditions = await createChangelogAdditions({ + version: targetVersion, + ignorePrereleaseTags: isFinalVersion, + issueUrlFormat: changelogConfig.issueUrlFormat, + }); + + if (!changelogAdditions) { + console.warn(chalk.yellow( + 'There were no changelog entries generated. Aborting changelog preparation.' + + ' If this is expected (e.g. only documentation or internal changes were made),' + + ' this version can be finalized without an update to the changelog.' + )); + return; + } + + if (useNewBranch) { + await createNewBranch(changelogBranch); + } else { + console.info(chalk.yellow(`Committing changelog directly to ${pushTarget}`)); + } + + if (isFinalVersion && semver.diff(currentVersion, targetVersion) === 'prerelease') { + const lastFinalReleaseTag = await getLastFinalReleaseTag(); + + if (lastFinalReleaseTag) { + console.info(chalk.yellow(`Resetting changelog to ${lastFinalReleaseTag} to remove prerelease entries...`)); + await resetChangelogToTag(lastFinalReleaseTag); + } else { + console.info(chalk.red( + 'Could not find a tag for a non-prerelease version. Not resetting the changelog to remove prerelease entries.' + )); + } + } + + await addEntriesToChangelog(changelogAdditions); + + await commitChangelog(targetVersion); + + const mrStep = useNewBranch ? ` ${chalk.gray('3.')} Create a merge request\n` : ''; + + console.info( + chalk.whiteBright('The changelog has been updated. Please do the following:\n\n') + + ` ${chalk.gray('1.')} git show ${chalk.gray('# and review the changes')}\n` + + ` ${chalk.gray('2.')} git push $REMOTE_NAME ${pushTarget}\n` + + mrStep + ); + }); + + release + .command('finalize') + .description('Performs the version bump for the release') + .addOption(prereleaseOption) + .addOption(versionOption) + .action(async (options) => { + const targetVersion = await getNextVersion(options), + currentBranch = await getCurrentBranchName(); + + console.info(`Bumping ${packageJSON.name} from v${currentVersion} to v${targetVersion}...`); + + await execFile('npm', [ + 'version', + '--allow-same-version', + targetVersion, + '-m', + `chore: version bump: v${targetVersion}`, + ]); + + console.info( + chalk.whiteBright('The package version has been bumped. Please do the following:\n\n') + + ` ${chalk.gray('1.')} git show ${chalk.gray('# and review the changes')}\n` + + ` ${chalk.gray('2.')} git push $REMOTE_NAME ${currentBranch} v${targetVersion}\n` + + ` ${chalk.gray('3.')} If the package needs to be manually published, run "npm publish"\n` + ); + }); + + return release; +} + +module.exports = { createReleaseCommand }; diff --git a/scripts/release.js b/scripts/release.js index d29e986..e732eaa 100644 --- a/scripts/release.js +++ b/scripts/release.js @@ -3,300 +3,20 @@ const commander = require('commander'), chalk = require('chalk'), - conventionalChangelog = require('conventional-changelog'), - conventionalRecommendedBump = require('conventional-recommended-bump'), - semver = require('semver'), - fs = require('fs'), - path = require('path'), - childProcess = require('child_process'), - util = require('util'), - gitSemverTags = util.promisify(require('git-semver-tags')), - getRecommendedBump = util.promisify(conventionalRecommendedBump), - execFile = util.promisify(childProcess.execFile); + { createReleaseCommand } = require('./release-command'); -const CHANGELOG_CONFIG = { - filename: 'CHANGELOG.md', - releaseEntryPattern: /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+|` instead.' +)); -function isPrereleaseVersion(version) { - return !!semver.prerelease(version); -} - -async function calculateNextVersion(currentVersion, prereleaseIdentifier) { - const recommendedBump = await getRecommendedBump({ - preset: { - name: 'conventionalcommits', - preMajor: semver.lt(currentVersion, '1.0.0'), - }, - }); - - let releaseType = recommendedBump.releaseType; - - if (prereleaseIdentifier) { - if (isPrereleaseVersion(currentVersion)) { - // TODO: We might need to consider adjusting this to support bumping to the next - // version (e.g. a patch prerelease contains a new feature) - // See: https://github.com/conventional-changelog/standard-version/blob/6c75ed0b14f/lib/lifecycles/bump.js#L46-L50 - releaseType = 'prerelease'; - } else { - releaseType = `pre${releaseType}`; // e.g. premajor, preminor, prepatch - } - } - - return semver.inc(currentVersion, releaseType, prereleaseIdentifier); -} - -/** - * Returns the last, based on semver ordering, final release tag (i.e. v1.0.0, not - * v1.0.0-rc.0) found in the git log for the current branch. - */ -async function getLastFinalReleaseTag() { - const semverTags = await gitSemverTags({ - skipUnstable: true, - }); - - return semverTags.length ? semver.rsort(semverTags)[0] : undefined; -} - -function createChangelogAdditions(opts) { - return new Promise((resolve, reject) => { - let content = ''; - - // https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-core#options - const changelogOptions = { - preset: { - name: 'conventionalcommits', - issueUrlFormat: opts.issueUrlFormat, - }, - skipUnstable: opts.ignorePrereleaseTags, - }; - - // https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-writer#context - const context = { version: opts.version }; - - // https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-writer#options - const writerOptions = { - generateOn: (commit) => { - if (opts.ignorePrereleaseTags && isPrereleaseVersion(commit.version)) { - return false; - } - - return semver.valid(commit.version); - }, - }; - - const changelogStream = conventionalChangelog(changelogOptions, context, undefined, undefined, writerOptions); - - changelogStream.on('error', (err) => { - return reject(err); - }); - - changelogStream.on('data', (buffer) => { - content += buffer.toString(); - }); - - changelogStream.on('end', () => { - // In the event the there are no entries added to the changelog (i.e. it's just a - // changelog header), we don't want to add anything to the changelog - if (content.trim().split('\n').length === 1) { - resolve(undefined); - } - - resolve(content); - }); - }); -} - -async function addEntriesToChangelog(newEntries) { - const existingChangelog = await (fs.promises.readFile(CHANGELOG_CONFIG.filename, 'utf-8').catch(() => { return ''; })), - existingEntriesStartIndex = existingChangelog.search(CHANGELOG_CONFIG.releaseEntryPattern); - - let existingChangelogEntries = CHANGELOG_CONFIG.footer; // NOTE: Only added when creating a new changelog - - if (existingEntriesStartIndex !== -1) { - existingChangelogEntries = existingChangelog.substring(existingEntriesStartIndex); - } - - const newChangelog = `${CHANGELOG_CONFIG.header}${newEntries}\n${existingChangelogEntries}`; - - await fs.promises.writeFile(CHANGELOG_CONFIG.filename, newChangelog.trimEnd() + '\n'); -} - -async function createNewBranch(name) { - await execFile('git', [ 'checkout', '-b', name ]); -} - -async function resetChangelogToTag(tag) { - await execFile('git', [ 'checkout', tag, '--', CHANGELOG_CONFIG.filename ]); -} - -async function commitChangelog(version) { - await execFile('git', [ 'add', CHANGELOG_CONFIG.filename ]); - await execFile('git', [ 'commit', '-m', `chore: update changelog for v${version}` ]); -} +async function main() { + const program = createReleaseCommand({ cwd: process.cwd() }); -async function getCurrentBranchName() { - const output = await execFile('git', [ 'rev-parse', '--abbrev-ref', 'HEAD' ]); - - return output.stdout.trim(); -} - -async function doesRepositoryHaveUncommittedChanges() { - const output = await execFile('git', [ - 'status', - '--porcelain', - ]); - - return output.stdout !== ''; -} - -const main = async (argv) => { - function quitWithError(msg) { - console.error(chalk.red.bold(`${msg}\n`)); - process.exit(1); // eslint-disable-line no-process-exit - } - - if (await doesRepositoryHaveUncommittedChanges()) { - quitWithError('The repository has uncommitted changes. Please ensure the repository is clean before running this script'); - } - - const program = new commander.Command(), - packageJSON = JSON.parse(await fs.promises.readFile(path.join(process.cwd(), 'package.json'))), - currentVersion = packageJSON.version, - changelogConfig = packageJSON['silvermine-standardization'] ? packageJSON['silvermine-standardization'].changelog : {}, - prereleaseOption = new commander.Option('--prerelease ', 'The type of prerelease, e.g. alpha, beta, rc'), - versionOption = new commander.Option('--version ', 'The version to use instead of the auto-calculated version'); - - async function getNextVersion(options) { - if (options.version) { - const providedVersion = semver.clean(options.version); - - if (!providedVersion) { - quitWithError(`Invalid version: ${options.version}`); - } - - if (options.prerelease) { - quitWithError( - 'The --prerelease flag cannot be used with --version.' - + ` Please specify the desired prerelease in the version, e.g. ${providedVersion}-${options.prerelease}.0` - ); - } - - return providedVersion; - } - - return await calculateNextVersion(currentVersion, options.prerelease); - } - - program - .command('preview') - .description('Preview the version and changelog for the next release') - .addOption(prereleaseOption) - .addOption(versionOption) - .action(async (options) => { - const targetVersion = await getNextVersion(options); - - console.info(chalk.yellow(`Will bump ${packageJSON.name} from v${currentVersion} to v${targetVersion}`)); - console.info(chalk.whiteBright('\nAutogenerated changelog:\n')); - console.info(await createChangelogAdditions({ - version: targetVersion, - ignorePrereleaseTags: !isPrereleaseVersion(targetVersion), - issueUrlFormat: changelogConfig.issueUrlFormat, - })); - }); - - program - .command('prep-changelog') - .description('Generates the changelog for the next release') - .addOption(prereleaseOption) - .addOption(versionOption) - .action(async (options) => { - const targetVersion = await getNextVersion(options), - isFinalVersion = !isPrereleaseVersion(targetVersion), - changelogBranch = `changelog-v${targetVersion}`; - - console.info(`Generating changelog for ${packageJSON.name}@${targetVersion}...`); - - const changelogAdditions = await createChangelogAdditions({ - version: targetVersion, - ignorePrereleaseTags: isFinalVersion, - issueUrlFormat: changelogConfig.issueUrlFormat, - }); - - if (!changelogAdditions) { - console.warn(chalk.yellow( - 'There were no changelog entries generated. Aborting changelog preparation.' - + ' If this is expected (e.g. only documentation or internal changes were made),' - + ' this version can be finalized without an update to the changelog.' - )); - return; - } - - await createNewBranch(changelogBranch); - - if (isFinalVersion && semver.diff(currentVersion, targetVersion) === 'prerelease') { - const lastFinalReleaseTag = await getLastFinalReleaseTag(); - - if (lastFinalReleaseTag) { - console.info(chalk.yellow(`Resetting changelog to ${lastFinalReleaseTag} to remove prerelease entries...`)); - await resetChangelogToTag(lastFinalReleaseTag); - } else { - console.info(chalk.red( - 'Could not find a tag for a non-prerelease version. Not resetting the changelog to remove prerelease entries.' - )); - } - } - - await addEntriesToChangelog(changelogAdditions); - - await commitChangelog(targetVersion); - - console.info( - chalk.whiteBright('The changelog has been updated. Please do the following:\n\n') - + ` ${chalk.gray('1.')} git show ${chalk.gray('# and review the changes')}\n` - + ` ${chalk.gray('2.')} git push $REMOTE_NAME ${changelogBranch}\n` - + ` ${chalk.gray('3.')} Create a merge request\n` - ); - }); - - program - .command('finalize') - .description('Performs the version bump for the release') - .addOption(prereleaseOption) - .addOption(versionOption) - .action(async (options) => { - const targetVersion = await getNextVersion(options), - currentBranch = await getCurrentBranchName(); - - console.info(`Bumping ${packageJSON.name} from v${currentVersion} to v${targetVersion}...`); - - await execFile('npm', [ - 'version', - // `--allow-same-version` makes it so it's possible to have an initial release - // where the current version equals the target version - '--allow-same-version', - targetVersion, - '-m', - `chore: version bump: v${targetVersion}`, - ]); - - console.info( - chalk.whiteBright('The package version has been bumped. Please do the following:\n\n') - + ` ${chalk.gray('1.')} git show ${chalk.gray('# and review the changes')}\n` - + ` ${chalk.gray('2.')} git push $REMOTE_NAME ${currentBranch} v${targetVersion}\n` - + ` ${chalk.gray('3.')} If the package needs to be manually published, run "npm publish"\n` - ); - }); - - program.action(() => { program.outputHelp(); }); program.exitOverride(); try { - await program.parseAsync(argv); + await program.parseAsync(process.argv); } catch(err) { if (err instanceof commander.CommanderError) { program.outputHelp(); @@ -306,6 +26,6 @@ const main = async (argv) => { process.exit(1); // eslint-disable-line no-process-exit } -}; +} -main(process.argv); +main();