Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Replace `commander.js` with `yargs` - Add CLI unit tests - Add a `--version` option - Improve `--help` output - Remove `commander.js` related workaround - Allow to set list option with arg repetition or space separated list - Maintain the list options defined as comma separated list
- Loading branch information
Showing
4 changed files
with
272 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,64 @@ | ||
const program = require('commander'); | ||
const {pickBy, isUndefined} = require('lodash'); | ||
|
||
function list(values) { | ||
return values | ||
.split(',') | ||
.map(value => value.trim()) | ||
.filter(value => value && value !== 'false'); | ||
} | ||
const stringList = { | ||
type: 'string', | ||
array: true, | ||
coerce: values => | ||
values.length === 1 && values[0].trim() === 'false' | ||
? [] | ||
: values.reduce((values, value) => values.concat(value.split(',').map(value => value.trim())), []), | ||
}; | ||
|
||
module.exports = async () => { | ||
program | ||
.name('semantic-release') | ||
.description('Run automated package publishing') | ||
.option('-b, --branch <branch>', 'Branch to release from') | ||
.option('-r, --repository-url <repositoryUrl>', 'Git repository URL') | ||
.option('-t, --tag-format <tagFormat>', `Git tag format`) | ||
.option('-e, --extends <paths>', 'Comma separated list of shareable config paths or packages name', list) | ||
.option( | ||
'--verify-conditions <paths>', | ||
'Comma separated list of paths or packages name for the verifyConditions plugin(s)', | ||
list | ||
) | ||
.option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin') | ||
.option( | ||
'--verify-release <paths>', | ||
'Comma separated list of paths or packages name for the verifyRelease plugin(s)', | ||
list | ||
) | ||
.option('--generate-notes <path>', 'Path or package name for the generateNotes plugin') | ||
.option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list) | ||
.option('--success <paths>', 'Comma separated list of paths or packages name for the success plugin(s)', list) | ||
.option('--fail <paths>', 'Comma separated list of paths or packages name for the fail plugin(s)', list) | ||
.option( | ||
'--no-ci', | ||
'Skip Continuous Integration environment verifications, allowing to make releases from a local machine' | ||
) | ||
.option('--debug', 'Output debugging information') | ||
.option( | ||
'-d, --dry-run', | ||
'Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes' | ||
) | ||
.parse(process.argv); | ||
|
||
if (program.debug) { | ||
// Debug must be enabled before other requires in order to work | ||
require('debug').enable('semantic-release:*'); | ||
} | ||
const cli = require('yargs') | ||
.command('$0', 'Run automated package publishing', yargs => { | ||
yargs.demandCommand(0, 0).usage(`Run automated package publishing | ||
Usage: | ||
semantic-release [options] [plugins]`); | ||
}) | ||
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'}) | ||
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'}) | ||
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'}) | ||
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'}) | ||
.option('ci', {describe: 'Toggle CI verifications', default: true, type: 'boolean', group: 'Options'}) | ||
.option('verify-conditions', {...stringList, group: 'Plugins'}) | ||
.option('analyze-commits', {type: 'string', group: 'Plugins'}) | ||
.option('verify-release', {...stringList, group: 'Plugins'}) | ||
.option('generate-notes', {type: 'string', group: 'Plugins'}) | ||
.option('publish', {...stringList, group: 'Plugins'}) | ||
.option('success', {...stringList, group: 'Plugins'}) | ||
.option('fail', {...stringList, group: 'Plugins'}) | ||
.option('debug', {describe: 'Output debugging information', default: false, type: 'boolean', group: 'Options'}) | ||
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: false, type: 'boolean', group: 'Options'}) | ||
.option('h', {alias: 'help', group: 'Options'}) | ||
.option('v', {alias: 'version', group: 'Options'}) | ||
.strict(false) | ||
.exitProcess(false); | ||
|
||
try { | ||
if (program.args.length > 0) { | ||
program.outputHelp(); | ||
process.exitCode = 1; | ||
} else { | ||
const opts = program.opts(); | ||
// Set the `noCi` options as commander.js sets the `ci` options instead (because args starts with `--no`) | ||
opts.noCi = opts.ci === false ? true : undefined; | ||
// Remove option with undefined values, as commander.js sets non defined options as `undefined` | ||
await require('.')(pickBy(opts, value => !isUndefined(value))); | ||
const {help, version, ...opts} = cli.argv; | ||
if (Boolean(help) || Boolean(version)) { | ||
process.exitCode = 0; | ||
return; | ||
} | ||
|
||
// Set the `noCi` options as yargs sets the `ci` options instead (because arg starts with `--no`) | ||
if (opts.ci === false) { | ||
opts.noCi = true; | ||
} | ||
|
||
if (opts.debug) { | ||
// Debug must be enabled before other requires in order to work | ||
require('debug').enable('semantic-release:*'); | ||
} | ||
|
||
// Remove option with undefined values, as yargs sets non defined options as `undefined` | ||
await require('.')(pickBy(opts, value => !isUndefined(value))); | ||
process.exitCode = 0; | ||
} catch (err) { | ||
if (err.name !== 'YError') { | ||
console.error(err); | ||
} | ||
process.exitCode = 1; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import test from 'ava'; | ||
import proxyquire from 'proxyquire'; | ||
import clearModule from 'clear-module'; | ||
import {stub} from 'sinon'; | ||
|
||
// Save the current process.env and process.argv | ||
const envBackup = Object.assign({}, process.env); | ||
const argvBackup = Object.assign({}, process.argv); | ||
|
||
test.beforeEach(t => { | ||
clearModule('yargs'); | ||
t.context.logs = ''; | ||
t.context.errors = ''; | ||
t.context.stdout = stub(process.stdout, 'write').callsFake(val => { | ||
t.context.logs += val.toString(); | ||
}); | ||
t.context.stderr = stub(process.stderr, 'write').callsFake(val => { | ||
t.context.errors += val.toString(); | ||
}); | ||
}); | ||
|
||
test.afterEach.always(t => { | ||
process.env = envBackup; | ||
process.argv = argvBackup; | ||
t.context.stdout.restore(); | ||
t.context.stderr.restore(); | ||
delete process.exitCode; | ||
}); | ||
|
||
test.serial('Pass options to semantic-release API', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = [ | ||
'', | ||
'', | ||
'-b', | ||
'master', | ||
'-r', | ||
'https://github/com/owner/repo.git', | ||
'-t', | ||
`v\${version}`, | ||
'-e', | ||
'config1', | ||
'config2', | ||
'--verify-conditions', | ||
'condition1', | ||
'condition2', | ||
'--analyze-commits', | ||
'analyze', | ||
'--verify-release', | ||
'verify1', | ||
'verify2', | ||
'--generate-notes', | ||
'notes', | ||
'--publish', | ||
'publish1', | ||
'publish2', | ||
'--success', | ||
'success1', | ||
'success2', | ||
'--fail', | ||
'fail1', | ||
'fail2', | ||
'--debug', | ||
'-d', | ||
]; | ||
|
||
await cli(); | ||
|
||
t.is(run.args[0][0].branch, 'master'); | ||
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git'); | ||
t.is(run.args[0][0].tagFormat, `v\${version}`); | ||
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']); | ||
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']); | ||
t.is(run.args[0][0].analyzeCommits, 'analyze'); | ||
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']); | ||
t.is(run.args[0][0].generateNotes, 'notes'); | ||
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']); | ||
t.deepEqual(run.args[0][0].success, ['success1', 'success2']); | ||
t.deepEqual(run.args[0][0].fail, ['fail1', 'fail2']); | ||
t.is(run.args[0][0].debug, true); | ||
t.is(run.args[0][0].dryRun, true); | ||
|
||
t.is(process.exitCode, 0); | ||
}); | ||
|
||
test.serial('Pass options to semantic-release API with alias arguments', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = [ | ||
'', | ||
'', | ||
'--branch', | ||
'master', | ||
'--repository-url', | ||
'https://github/com/owner/repo.git', | ||
'--tag-format', | ||
`v\${version}`, | ||
'--extends', | ||
'config1', | ||
'config2', | ||
'--dry-run', | ||
]; | ||
|
||
await cli(); | ||
|
||
t.is(run.args[0][0].branch, 'master'); | ||
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git'); | ||
t.is(run.args[0][0].tagFormat, `v\${version}`); | ||
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']); | ||
t.is(run.args[0][0].dryRun, true); | ||
|
||
t.is(process.exitCode, 0); | ||
}); | ||
|
||
test.serial('Pass unknown options to semantic-release API', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = [ | ||
'', | ||
'', | ||
'--bool', | ||
'--first-option', | ||
'value1', | ||
'--second-option', | ||
'value2', | ||
'--second-option', | ||
'value3', | ||
]; | ||
|
||
await cli(); | ||
|
||
t.is(run.args[0][0].bool, true); | ||
t.is(run.args[0][0].firstOption, 'value1'); | ||
t.deepEqual(run.args[0][0].secondOption, ['value2', 'value3']); | ||
|
||
t.is(process.exitCode, 0); | ||
}); | ||
|
||
test.serial('Pass empty Array to semantic-release API for list option set to "false"', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = ['', '', '--publish', 'false']; | ||
|
||
await cli(); | ||
|
||
t.deepEqual(run.args[0][0].publish, []); | ||
|
||
t.is(process.exitCode, 0); | ||
}); | ||
|
||
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = ['', '', '--no-ci']; | ||
|
||
await cli(); | ||
|
||
t.is(run.args[0][0].noCi, true); | ||
|
||
t.is(process.exitCode, 0); | ||
}); | ||
|
||
test.serial('Display help', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = ['', '', '--help']; | ||
|
||
await cli(); | ||
|
||
t.regex(t.context.logs, /Run automated package publishing/); | ||
t.is(process.exitCode, 0); | ||
}); | ||
|
||
test.serial('Returns error code and prints help if called with a command', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = ['', '', 'pre']; | ||
|
||
await cli(); | ||
|
||
t.regex(t.context.errors, /Run automated package publishing/); | ||
t.regex(t.context.errors, /Too many non-option arguments/); | ||
t.is(process.exitCode, 1); | ||
}); | ||
|
||
test.serial('Return error code if multiple plugin are set for single plugin', async t => { | ||
const run = stub().resolves(true); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = ['', '', '--analyze-commits', 'analyze1', 'analyze2']; | ||
|
||
await cli(); | ||
|
||
t.regex(t.context.errors, /Run automated package publishing/); | ||
t.regex(t.context.errors, /Too many non-option arguments/); | ||
t.is(process.exitCode, 1); | ||
}); | ||
|
||
test.serial('Return error code if semantic-release throw error', async t => { | ||
const run = stub().rejects(new Error('semantic-release error')); | ||
const cli = proxyquire('../cli', {'.': run}); | ||
|
||
process.argv = ['', '']; | ||
|
||
await cli(); | ||
|
||
t.regex(t.context.errors, /semantic-release error/); | ||
t.is(process.exitCode, 1); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters