diff --git a/readme.md b/readme.md index 034b4521..0fd86cb6 100644 --- a/readme.md +++ b/readme.md @@ -50,19 +50,20 @@ $ np --help patch | minor | major | prepatch | preminor | premajor | prerelease | 1.2.3 Options - --any-branch Allow publishing from any branch - --branch Name of the release branch (default: master) - --no-cleanup Skips cleanup of node_modules - --no-tests Skips tests - --yolo Skips cleanup and testing - --no-publish Skips publishing - --preview Show tasks without actually executing them - --tag Publish under a given dist-tag - --no-yarn Don't use Yarn - --contents Subdirectory to publish - --no-release-draft Skips opening a GitHub release draft - --test-script Name of npm run script to run tests before publishing (default: test) - --no-2fa Don't enable 2FA on new packages (not recommended) + --any-branch Allow publishing from any branch + --branch Name of the release branch (default: master) + --no-cleanup Skips cleanup of node_modules + --no-tests Skips tests + --yolo Skips cleanup and testing + --no-publish Skips publishing + --preview Show tasks without actually executing them + --tag Publish under a given dist-tag + --no-yarn Don't use Yarn + --contents Subdirectory to publish + --no-release-draft Skips opening a GitHub release draft + --release-draft-only Only opens a GitHub release draft + --test-script Name of npm run script to run tests before publishing (default: test) + --no-2fa Don't enable 2FA on new packages (not recommended) Examples $ np diff --git a/source/cli-implementation.js b/source/cli-implementation.js index eb9e0190..f5ded4b1 100755 --- a/source/cli-implementation.js +++ b/source/cli-implementation.js @@ -21,19 +21,20 @@ const cli = meow(` ${version.SEMVER_INCREMENTS.join(' | ')} | 1.2.3 Options - --any-branch Allow publishing from any branch - --branch Name of the release branch (default: master) - --no-cleanup Skips cleanup of node_modules - --no-tests Skips tests - --yolo Skips cleanup and testing - --no-publish Skips publishing - --preview Show tasks without actually executing them - --tag Publish under a given dist-tag - --no-yarn Don't use Yarn - --contents Subdirectory to publish - --no-release-draft Skips opening a GitHub release draft - --test-script Name of npm run script to run tests before publishing (default: test) - --no-2fa Don't enable 2FA on new packages (not recommended) + --any-branch Allow publishing from any branch + --branch Name of the release branch (default: master) + --no-cleanup Skips cleanup of node_modules + --no-tests Skips tests + --yolo Skips cleanup and testing + --no-publish Skips publishing + --preview Show tasks without actually executing them + --tag Publish under a given dist-tag + --no-yarn Don't use Yarn + --contents Subdirectory to publish + --no-release-draft Skips opening a GitHub release draft + --release-draft-only Only opens a GitHub release draft for the latest published version + --test-script Name of npm run script to run tests before publishing (default: test) + --no-2fa Don't enable 2FA on new packages (not recommended) Examples $ np @@ -65,6 +66,9 @@ const cli = meow(` releaseDraft: { type: 'boolean' }, + releaseDraftOnly: { + type: 'boolean' + }, tag: { type: 'string' }, @@ -113,14 +117,15 @@ updateNotifier({pkg: cli.pkg}).notify(); flags['2fa'] = flags['2Fa']; } - const runPublish = flags.publish && !pkg.private; + const runPublish = !flags.releaseDraftOnly && flags.publish && !pkg.private; const availability = flags.publish ? await isPackageNameAvailable(pkg) : { isAvailable: false, isUnknown: false }; - const version = cli.input.length > 0 ? cli.input[0] : false; + // Use current (latest) version when 'releaseDraftOnly', otherwise use the first argument. + const version = flags.releaseDraftOnly ? pkg.version : (cli.input.length > 0 ? cli.input[0] : false); const options = await ui({ ...flags, @@ -136,7 +141,7 @@ updateNotifier({pkg: cli.pkg}).notify(); console.log(); // Prints a newline for readability const newPkg = await np(options.version, options); - if (options.preview) { + if (options.preview || options.releaseDraftOnly) { return; } diff --git a/source/git-util.js b/source/git-util.js index e3bd672d..50d639cc 100644 --- a/source/git-util.js +++ b/source/git-util.js @@ -29,6 +29,21 @@ const firstCommit = async () => { return stdout; }; +exports.previousTagOrFirstCommit = async () => { + const {stdout} = await execa('git', ['tag']); + const tags = stdout.split('\n'); + + if (tags.length === 0) { + return; + } + + if (tags.length === 1) { + return firstCommit(); + } + + return tags[tags.length - 2]; +}; + exports.latestTagOrFirstCommit = async () => { let latest; try { diff --git a/source/index.js b/source/index.js index bc629844..5aaca744 100644 --- a/source/index.js +++ b/source/index.js @@ -58,6 +58,11 @@ module.exports = async (input = 'patch', options) => { const testScript = options.testScript || 'test'; const testCommand = options.testScript ? ['run', testScript] : [testScript]; + if (options.releaseDraftOnly) { + await releaseTaskHelper(options, pkg); + return pkg; + } + let publishStatus = 'UNKNOWN'; let pushedObjects; diff --git a/source/ui.js b/source/ui.js index 7c5f6ad8..b9e6333e 100644 --- a/source/ui.js +++ b/source/ui.js @@ -11,18 +11,26 @@ const {prereleaseTags, checkIgnoreStrategy, getRegistryUrl, isExternalRegistry} const version = require('./version'); const prettyVersionDiff = require('./pretty-version-diff'); -const printCommitLog = async (repoUrl, registryUrl) => { - const latest = await git.latestTagOrFirstCommit(); - const log = await git.commitLogFromRevision(latest); +const printCommitLog = async (repoUrl, registryUrl, fromLatestTag) => { + const revision = fromLatestTag ? await git.latestTagOrFirstCommit() : await git.previousTagOrFirstCommit(); + if (!revision) { + throw new Error('The package has not been published yet.'); + } + + const log = await git.commitLogFromRevision(revision); if (!log) { return { hasCommits: false, + hasUnreleasedCommits: false, releaseNotes: () => {} }; } - const commits = log.split('\n') + let hasUnreleasedCommits = false; + let commitRangeText = `${revision}...master`; + + let commits = log.split('\n') .map(commit => { const splitIndex = commit.lastIndexOf(' '); return { @@ -31,6 +39,20 @@ const printCommitLog = async (repoUrl, registryUrl) => { }; }); + if (!fromLatestTag) { + const latestTag = await git.latestTag(); + const versionBumpCommitName = latestTag.slice(1); // Name v1.0.1 becomes 1.0.1 + const versionBumpCommitIndex = commits.findIndex(commit => commit.message === versionBumpCommitName); + + if (versionBumpCommitIndex > 0) { + commitRangeText = `${revision}...${latestTag}`; + hasUnreleasedCommits = true; + } + + // Get rid of unreleased commits and of the version bump commit. + commits = commits.slice(versionBumpCommitIndex + 1); + } + const history = commits.map(commit => { const commitMessage = util.linkifyIssues(repoUrl, commit.message); const commitId = util.linkifyCommit(repoUrl, commit.id); @@ -39,14 +61,15 @@ const printCommitLog = async (repoUrl, registryUrl) => { const releaseNotes = nextTag => commits.map(commit => `- ${htmlEscape(commit.message)} ${commit.id}` - ).join('\n') + `\n\n${repoUrl}/compare/${latest}...${nextTag}`; + ).join('\n') + `\n\n${repoUrl}/compare/${revision}...${nextTag}`; - const commitRange = util.linkifyCommitRange(repoUrl, `${latest}...master`); + const commitRange = util.linkifyCommitRange(repoUrl, commitRangeText); console.log(`${chalk.bold('Commits:')}\n${history}\n\n${chalk.bold('Commit Range:')}\n${commitRange}\n\n${chalk.bold('Registry:')}\n${registryUrl}\n`); return { hasCommits: true, + hasUnreleasedCommits, releaseNotes }; }; @@ -92,7 +115,11 @@ module.exports = async (options, pkg) => { } } - console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); + if (options.releaseDraftOnly) { + console.log(`\nCreate a release draft on GitHub for ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); + } else { + console.log(`\nPublish a new version of ${chalk.bold.magenta(pkg.name)} ${chalk.dim(`(current: ${oldVersion})`)}\n`); + } const prompts = [ { @@ -176,7 +203,24 @@ module.exports = async (options, pkg) => { } ]; - const {hasCommits, releaseNotes} = await printCommitLog(repoUrl, registryUrl); + const useLatestTag = !options.releaseDraftOnly; + const {hasCommits, hasUnreleasedCommits, releaseNotes} = await printCommitLog(repoUrl, registryUrl, useLatestTag); + + if (hasUnreleasedCommits && options.releaseDraftOnly) { + const answers = await inquirer.prompt([{ + type: 'confirm', + name: 'confirm', + message: 'Unreleased commits found. They won\'t be included in the release draft. Continue?', + default: false + }]); + + if (!answers.confirm) { + return { + ...options, + ...answers + }; + } + } if (options.version) { return {