diff --git a/index.js b/index.js index c6d18a542d..1baeb7a7f8 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,7 @@ async function run(context, plugins) { const {isCi, branch: ciBranch, isPr} = envCi({env, cwd}); if (!isCi && !options.dryRun && !options.noCi) { - logger.log('This run was not triggered in a known CI environment, running in dry-run mode.'); + logger.warn('This run was not triggered in a known CI environment, running in dry-run mode.'); options.dryRun = true; } else { // When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. See #703. @@ -52,7 +52,9 @@ async function run(context, plugins) { ); return false; } - logger.success(`Run automated release from branch ${ciBranch}${options.dryRun ? ' in dry-run mode' : ''}`); + logger[options.dryRun ? 'warn' : 'success']( + `Run automated release from branch ${ciBranch}${options.dryRun ? ' in dry-run mode' : ''}` + ); await verify(context); @@ -98,24 +100,28 @@ async function run(context, plugins) { nextRelease.notes = await plugins.generateNotes(context); + await plugins.prepare(context); + if (options.dryRun) { - logger.log(`Release note for version ${nextRelease.version}:`); - if (nextRelease.notes) { - context.stdout.write(marked(nextRelease.notes)); - } + logger.warn(`Skip ${nextRelease.gitTag} tag creation in dry-run mode`); } else { - await plugins.prepare(context); - // Create the tag before calling the publish plugins as some require the tag to exists await tag(nextRelease.gitTag, {cwd, env}); await push(options.repositoryUrl, options.branch, {cwd, env}); logger.success(`Created tag ${nextRelease.gitTag}`); + } - context.releases = await plugins.publish(context); + context.releases = await plugins.publish(context); - await plugins.success(context); + await plugins.success(context); - logger.success(`Published release ${nextRelease.version}`); + logger.success(`Published release ${nextRelease.version}`); + + if (options.dryRun) { + logger.log(`Release note for version ${nextRelease.version}:`); + if (nextRelease.notes) { + context.stdout.write(marked(nextRelease.notes)); + } } return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']); @@ -162,9 +168,7 @@ module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdo unhook(); return result; } catch (error) { - if (!options.dryRun) { - await callFail(context, plugins, error); - } + await callFail(context, plugins, error); throw error; } } catch (error) { diff --git a/lib/definitions/plugins.js b/lib/definitions/plugins.js index 7fb6fe6ec2..c171b97aa6 100644 --- a/lib/definitions/plugins.js +++ b/lib/definitions/plugins.js @@ -7,11 +7,13 @@ const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants'); module.exports = { verifyConditions: { required: false, + dryRun: true, pipelineConfig: () => ({settleAll: true}), }, analyzeCommits: { default: ['@semantic-release/commit-analyzer'], required: true, + dryRun: true, outputValidator: output => !output || RELEASE_TYPE.includes(output), preprocess: ({commits, ...inputs}) => ({ ...inputs, @@ -27,10 +29,12 @@ module.exports = { }, verifyRelease: { required: false, + dryRun: true, pipelineConfig: () => ({settleAll: true}), }, generateNotes: { required: false, + dryRun: true, outputValidator: output => !output || isString(output), pipelineConfig: () => ({ getNextInput: ({nextRelease, ...context}, notes) => ({ @@ -45,6 +49,7 @@ module.exports = { }, prepare: { required: false, + dryRun: false, pipelineConfig: ({generateNotes}) => ({ getNextInput: async context => { const newGitHead = await gitHead({cwd: context.cwd}); @@ -61,6 +66,7 @@ module.exports = { }, publish: { required: false, + dryRun: false, outputValidator: output => !output || isPlainObject(output), pipelineConfig: () => ({ // Add `nextRelease` and plugin properties to published release @@ -73,11 +79,13 @@ module.exports = { }, success: { required: false, + dryRun: false, pipelineConfig: () => ({settleAll: true}), preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}), }, fail: { required: false, + dryRun: false, pipelineConfig: () => ({settleAll: true}), preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}), }, diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js index a10f8c41ca..56af5b68f0 100644 --- a/lib/plugins/normalize.js +++ b/lib/plugins/normalize.js @@ -27,20 +27,23 @@ module.exports = (context, type, pluginOpt, pluginsPath) => { } const validator = async input => { - const {outputValidator} = PLUGINS_DEFINITIONS[type] || {}; + const {dryRun, outputValidator} = PLUGINS_DEFINITIONS[type] || {}; try { - logger.log(`Start step "${type}" of plugin "${pluginName}"`); - const result = await func({ - ...cloneDeep(omit(input, ['stdout', 'stderr', 'logger'])), - stdout, - stderr, - logger: logger.scope(logger.scopeName, pluginName), - }); - if (outputValidator && !outputValidator(result)) { - throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName}); + if (!input.options.dryRun || dryRun) { + logger.log(`Start step "${type}" of plugin "${pluginName}"`); + const result = await func({ + ...cloneDeep(omit(input, ['stdout', 'stderr', 'logger'])), + stdout, + stderr, + logger: logger.scope(logger.scopeName, pluginName), + }); + if (outputValidator && !outputValidator(result)) { + throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName}); + } + logger.success(`Completed step "${type}" of plugin "${pluginName}"`); + return result; } - logger.success(`Completed step "${type}" of plugin "${pluginName}"`); - return result; + logger.warn(`Skip step "${type}" of plugin "${pluginName}" in dry-run mode`); } catch (error) { logger.error(`Failed step "${type}" of plugin "${pluginName}"`); extractErrors(error).forEach(err => Object.assign(err, {pluginName})); diff --git a/test/index.test.js b/test/index.test.js index 9c23836edf..046d316e53 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -25,10 +25,12 @@ test.beforeEach(t => { t.context.log = spy(); t.context.error = spy(); t.context.success = spy(); + t.context.warn = spy(); t.context.logger = { log: t.context.log, error: t.context.error, success: t.context.success, + warn: t.context.warn, scope: () => t.context.logger, }; }); @@ -476,14 +478,17 @@ test('Dry-run skips prepare, publish and success', async t => { }) ); - t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.'); + t.not(t.context.warn.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.'); t.is(verifyConditions.callCount, 1); t.is(analyzeCommits.callCount, 1); t.is(verifyRelease.callCount, 1); t.is(generateNotes.callCount, 1); t.is(prepare.callCount, 0); + t.true(t.context.warn.calledWith(`Skip step "prepare" of plugin "[Function: ${prepare.name}]" in dry-run mode`)); t.is(publish.callCount, 0); + t.true(t.context.warn.calledWith(`Skip step "publish" of plugin "[Function: ${publish.name}]" in dry-run mode`)); t.is(success.callCount, 0); + t.true(t.context.warn.calledWith(`Skip step "success" of plugin "[Function: ${success.name}]" in dry-run mode`)); }); test('Dry-run skips fail', async t => { @@ -523,6 +528,7 @@ test('Dry-run skips fail', async t => { t.true(t.context.error.calledWith('ERR1 error 1')); t.true(t.context.error.calledWith('ERR2 error 2')); t.is(fail.callCount, 0); + t.true(t.context.warn.calledWith(`Skip step "fail" of plugin "[Function: ${fail.name}]" in dry-run mode`)); }); test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t => { @@ -573,7 +579,7 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t }) ); - t.is(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.'); + t.true(t.context.warn.calledWith('This run was not triggered in a known CI environment, running in dry-run mode.')); t.is(verifyConditions.callCount, 1); t.is(analyzeCommits.callCount, 1); t.is(verifyRelease.callCount, 1); diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js index 96ae1d8e47..d42ae4792e 100644 --- a/test/plugins/normalize.test.js +++ b/test/plugins/normalize.test.js @@ -62,7 +62,7 @@ test('Wrap plugin in a function that add the "pluginName" to the error"', async './plugin-error': './test/fixtures', }); - const error = await t.throws(plugin()); + const error = await t.throws(plugin({options: {}})); t.is(error.pluginName, './plugin-error'); }); @@ -72,7 +72,7 @@ test('Wrap plugin in a function that add the "pluginName" to multiple errors"', './plugin-errors': './test/fixtures', }); - const errors = [...(await t.throws(plugin()))]; + const errors = [...(await t.throws(plugin({options: {}})))]; for (const error of errors) { t.is(error.pluginName, './plugin-errors'); } @@ -107,7 +107,7 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the {} ); - const error = await t.throws(plugin()); + const error = await t.throws(plugin({options: {}})); t.is(error.code, 'EANALYZECOMMITSOUTPUT'); t.is(error.name, 'SemanticReleaseError'); @@ -125,7 +125,7 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the {} ); - const error = await t.throws(plugin()); + const error = await t.throws(plugin({options: {}})); t.is(error.code, 'EGENERATENOTESOUTPUT'); t.is(error.name, 'SemanticReleaseError'); @@ -143,7 +143,7 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin {} ); - const error = await t.throws(plugin()); + const error = await t.throws(plugin({options: {}})); t.is(error.code, 'EPUBLISHOUTPUT'); t.is(error.name, 'SemanticReleaseError'); @@ -157,7 +157,7 @@ test('Plugin is called with "pluginConfig" (with object definition) and input', const pluginConf = {path: pluginFunction, conf: 'confValue'}; const options = {global: 'globalValue'}; const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); - await plugin({param: 'param'}); + await plugin({options: {}, param: 'param'}); t.true( pluginFunction.calledWithMatch( @@ -172,7 +172,7 @@ test('Plugin is called with "pluginConfig" (with array definition) and input', a const pluginConf = [pluginFunction, {conf: 'confValue'}]; const options = {global: 'globalValue'}; const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); - await plugin({param: 'param'}); + await plugin({options: {}, param: 'param'}); t.true( pluginFunction.calledWithMatch( @@ -189,7 +189,7 @@ test('Prevent plugins to modify "pluginConfig"', async t => { const pluginConf = {path: pluginFunction, conf: {subConf: 'originalConf'}}; const options = {globalConf: {globalSubConf: 'originalGlobalConf'}}; const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); - await plugin(); + await plugin({options: {}}); t.is(pluginConf.conf.subConf, 'originalConf'); t.is(options.globalConf.globalSubConf, 'originalGlobalConf'); @@ -199,7 +199,7 @@ test('Prevent plugins to modify its input', async t => { const pluginFunction = stub().callsFake((pluginConfig, options) => { options.param.subParam = 'otherParam'; }); - const input = {param: {subParam: 'originalSubParam'}}; + const input = {param: {subParam: 'originalSubParam'}, options: {}}; const plugin = normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {}); await plugin(input); @@ -220,7 +220,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with string', asyn './test/fixtures/plugin-result-config', {} ); - const pluginResult = await plugin(); + const pluginResult = await plugin({options: {}}); t.deepEqual(pluginResult.pluginConfig, {}); }); @@ -233,7 +233,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async {path: './test/fixtures/plugin-result-config'}, {} ); - const pluginResult = await plugin(); + const pluginResult = await plugin({options: {}}); t.deepEqual(pluginResult.pluginConfig, {}); }); diff --git a/test/plugins/plugins.test.js b/test/plugins/plugins.test.js index a0020623f0..bdfa07bcf3 100644 --- a/test/plugins/plugins.test.js +++ b/test/plugins/plugins.test.js @@ -63,14 +63,14 @@ test('Export plugins based on "plugins" config (array)', async t => { {} ); - await plugins.verifyConditions({}); + await plugins.verifyConditions({options: {}}); t.true(plugin1.verifyConditions.calledOnce); t.true(plugin2.verifyConditions.calledOnce); - await plugins.publish({}); + await plugins.publish({options: {}}); t.true(plugin1.publish.calledOnce); - await plugins.verifyRelease({}); + await plugins.verifyRelease({options: {}}); t.true(plugin2.verifyRelease.notCalled); // Verify the module returns a function for each plugin @@ -88,10 +88,10 @@ test('Export plugins based on "plugins" config (single definition)', async t => const plugin1 = {verifyConditions: stub(), publish: stub()}; const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {}); - await plugins.verifyConditions({}); + await plugins.verifyConditions({options: {}}); t.true(plugin1.verifyConditions.calledOnce); - await plugins.publish({}); + await plugins.publish({options: {}}); t.true(plugin1.publish.calledOnce); // Verify the module returns a function for each plugin @@ -118,14 +118,14 @@ test('Merge global options, "plugins" options and step options', async t => { {} ); - await plugins.verifyConditions({}); + await plugins.verifyConditions({options: {}}); t.deepEqual(plugin1[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'}); t.deepEqual(plugin2[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt2: 'plugin2'}); - await plugins.publish({}); + await plugins.publish({options: {}}); t.deepEqual(plugin1[0].publish.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'}); - await plugins.verifyRelease({}); + await plugins.verifyRelease({options: {}}); t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'}); }); @@ -248,7 +248,7 @@ test('Merge global options with plugin options', async t => { {} ); - const [result] = await plugins.verifyRelease(); + const [result] = await plugins.verifyRelease({options: {}}); t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'}); });