diff --git a/index.js b/index.js index 641be349..e4456268 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ const getCommits = require('./lib/get-commits'); const getLastRelease = require('./lib/get-last-release'); const {extractErrors} = require('./lib/utils'); const getGitAuthUrl = require('./lib/get-git-auth-url'); -const logger = require('./lib/logger'); +const getLogger = require('./lib/get-logger'); const {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git'); const getError = require('./lib/get-error'); const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants'); @@ -53,6 +53,7 @@ async function run(context, plugins) { ); return false; } + logger.success(`Run automated release from branch ${ciBranch}`); await verify(context); @@ -63,16 +64,15 @@ async function run(context, plugins) { } catch (err) { if (!(await isBranchUpToDate(options.branch, {cwd, env}))) { logger.log( - "The local branch %s is behind the remote one, therefore a new version won't be published.", - options.branch + `The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.` ); return false; } - logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr); + logger.error(`The command "${err.cmd}" failed with the error message ${err.stderr}.`); throw getError('EGITNOPERMISSION', {options}); } - logger.log('Run automated release from branch %s', options.branch); + logger.success(`Allowed to push to the Git repository`); await plugins.verifyConditions(context); @@ -95,35 +95,35 @@ async function run(context, plugins) { if (options.dryRun) { const notes = await plugins.generateNotes(context); - logger.log('Release note for version %s:\n', nextRelease.version); + logger.log(`Release note for version ${nextRelease.version}:`); if (notes) { - logger.stdout(`${marked(notes)}\n`); + context.stdout.write(marked(notes)); } } else { nextRelease.notes = await plugins.generateNotes(context); await plugins.prepare(context); // Create the tag before calling the publish plugins as some require the tag to exists - logger.log('Create tag %s', nextRelease.gitTag); 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); await plugins.success(context); - logger.log('Published release: %s', nextRelease.version); + logger.success(`Published release ${nextRelease.version}`); } return true; } -function logErrors({logger}, err) { +function logErrors({logger, stderr}, err) { const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0)); for (const error of errors) { if (error.semanticRelease) { - logger.log(`%s ${error.message}`, error.code); + logger.error(`${error.code} ${error.message}`); if (error.details) { - logger.stderr(`${marked(error.details)}\n`); + stderr.write(marked(error.details)); } } else { logger.error('An error occurred while running semantic-release: %O', error); @@ -142,10 +142,14 @@ async function callFail(context, plugins, error) { } } -module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) => { - const context = {cwd, env, logger}; - context.logger.log(`Running %s version %s`, pkg.name, pkg.version); - const {unhook} = hookStd({silent: false}, hideSensitive(context.env)); +module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => { + const {unhook} = hookStd( + {silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)}, + hideSensitive(env) + ); + const context = {cwd, env, stdout: stdout || process.stdout, stderr: stderr || process.stderr}; + context.logger = getLogger(context); + context.logger.log(`Running ${pkg.name} version ${pkg.version}`); try { const {plugins, options} = await getConfig(context, opts); context.options = options; diff --git a/lib/get-commits.js b/lib/get-commits.js index 981c13f4..69d20dfb 100644 --- a/lib/get-commits.js +++ b/lib/get-commits.js @@ -24,7 +24,7 @@ module.exports = async ({cwd, env, lastRelease: {gitHead}, logger}) => { commit.gitTags = commit.gitTags.trim(); return commit; }); - logger.log('Found %s commits since last release', commits.length); + logger.log(`Found ${commits.length} commits since last release`); debug('Parsed commits: %o', commits); return commits; }; diff --git a/lib/get-last-release.js b/lib/get-last-release.js index d9eec16e..77da1dd5 100644 --- a/lib/get-last-release.js +++ b/lib/get-last-release.js @@ -42,7 +42,7 @@ module.exports = async ({cwd, env, options: {tagFormat}, logger}) => { const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true}); if (tag) { - logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version); + logger.log(`Found git tag ${tag.gitTag} associated with version ${tag.version}`); return {gitHead: await gitTagHead(tag.gitTag, {cwd, env}), ...tag}; } diff --git a/lib/get-logger.js b/lib/get-logger.js new file mode 100644 index 00000000..52b8f0da --- /dev/null +++ b/lib/get-logger.js @@ -0,0 +1,16 @@ +const {Signale} = require('signale'); +const figures = require('figures'); + +module.exports = ({stdout, stderr}) => + new Signale({ + config: {displayTimestamp: true, underlineMessage: true, displayLabel: false}, + disabled: false, + interactive: false, + scope: 'semantic-release', + stream: [stdout], + types: { + error: {badge: figures.cross, color: 'red', label: '', stream: [stderr]}, + log: {badge: figures.info, color: 'magenta', label: '', stream: [stdout]}, + success: {badge: figures.tick, color: 'green', label: '', stream: [stdout]}, + }, + }); diff --git a/lib/get-next-version.js b/lib/get-next-version.js index 7d621e37..10089e9f 100644 --- a/lib/get-next-version.js +++ b/lib/get-next-version.js @@ -5,10 +5,10 @@ module.exports = ({nextRelease: {type}, lastRelease, logger}) => { let version; if (lastRelease.version) { version = semver.inc(lastRelease.version, type); - logger.log('The next release version is %s', version); + logger.log(`The next release version is ${version}`); } else { version = FIRST_RELEASE; - logger.log('There is no previous release, the next release version is %s', version); + logger.log(`There is no previous release, the next release version is ${version}`); } return version; diff --git a/lib/logger.js b/lib/logger.js deleted file mode 100644 index 56465b74..00000000 --- a/lib/logger.js +++ /dev/null @@ -1,29 +0,0 @@ -const chalk = require('chalk'); - -/** - * Logger with `log` and `error` function. - */ -module.exports = { - log(...args) { - const [format, ...rest] = args; - console.log( - `${chalk.grey('[Semantic release]:')}${ - typeof format === 'string' ? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}` : '' - }`, - ...(typeof format === 'string' ? [] : [format]).concat(rest) - ); - }, - error(...args) { - const [format, ...rest] = args; - console.error( - `${chalk.grey('[Semantic release]:')}${typeof format === 'string' ? ` ${chalk.red(format)}` : ''}`, - ...(typeof format === 'string' ? [] : [format]).concat(rest) - ); - }, - stdout(...args) { - console.log(args); - }, - stderr(...args) { - console.error(args); - }, -}; diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js index fdcfd6fb..15ff4ce2 100644 --- a/lib/plugins/normalize.js +++ b/lib/plugins/normalize.js @@ -13,14 +13,6 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => { const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt; const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path; - if (!isFunction(pluginOpt)) { - if (pluginsPath[path]) { - logger.log('Load plugin "%s" from %s in shareable config %s', type, path, pluginsPath[path]); - } else { - logger.log('Load plugin "%s" from %s', type, path); - } - } - const basePath = pluginsPath[path] ? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path])) : __dirname; @@ -38,18 +30,29 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => { const validator = async input => { const {outputValidator} = PLUGINS_DEFINITIONS[type] || {}; try { - logger.log('Call plugin "%s"', type); - const result = await func(cloneDeep(input)); + logger.log(`Start step "${type}" of plugin "${pluginName}"`); + const result = await func({...cloneDeep(input), 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; } catch (err) { + logger.error(`Failed step "${type}" of plugin "${pluginName}"`); extractErrors(err).forEach(err => Object.assign(err, {pluginName})); throw err; } }; Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true}); + + if (!isFunction(pluginOpt)) { + if (pluginsPath[path]) { + logger.success(`Loaded plugin "${type}" from "${path}" in shareable config "${pluginsPath[path]}"`); + } else { + logger.success(`Loaded plugin "${type}" from "${path}"`); + } + } + return validator; }; diff --git a/package.json b/package.json index bd3c348d..1b656f27 100644 --- a/package.json +++ b/package.json @@ -22,19 +22,19 @@ "@semantic-release/commit-analyzer": "^6.0.0", "@semantic-release/error": "^2.2.0", "@semantic-release/github": "^5.0.0", - "@semantic-release/npm": "^4.0.0", + "@semantic-release/npm": "^4.0.1", "@semantic-release/release-notes-generator": "^7.0.0", "aggregate-error": "^1.0.0", - "chalk": "^2.3.0", "cosmiconfig": "^5.0.1", "debug": "^3.1.0", "env-ci": "^2.0.0", "execa": "^0.10.0", + "figures": "^2.0.0", "find-versions": "^2.0.0", "get-stream": "^3.0.0", "git-log-parser": "^1.2.0", "git-url-parse": "^10.0.1", - "hook-std": "^1.0.1", + "hook-std": "^1.1.0", "hosted-git-info": "^2.7.1", "lodash": "^4.17.4", "marked": "^0.4.0", @@ -44,6 +44,7 @@ "read-pkg-up": "^4.0.0", "resolve-from": "^4.0.0", "semver": "^5.4.1", + "signale": "^1.2.1", "yargs": "^12.0.0" }, "devDependencies": { diff --git a/test/get-last-release.test.js b/test/get-last-release.test.js index 85d3859f..bf842ccd 100644 --- a/test/get-last-release.test.js +++ b/test/get-last-release.test.js @@ -27,7 +27,7 @@ test('Get the highest non-prerelease valid tag', async t => { const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger}); t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'}); - t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']); + t.deepEqual(t.context.log.args[0], ['Found git tag v2.0.0 associated with version 2.0.0']); }); test('Get the highest tag in the history of the current branch', async t => { diff --git a/test/get-logger.test.js b/test/get-logger.test.js new file mode 100644 index 00000000..31e75628 --- /dev/null +++ b/test/get-logger.test.js @@ -0,0 +1,17 @@ +import test from 'ava'; +import {spy} from 'sinon'; +import getLogger from '../lib/get-logger'; + +test('Expose "error", "success" and "log" functions', t => { + const stdout = spy(); + const stderr = spy(); + const logger = getLogger({stdout: {write: stdout}, stderr: {write: stderr}}); + + logger.log('test log'); + logger.success('test success'); + logger.error('test error'); + + t.regex(stdout.args[0][0], /.*test log/); + t.regex(stdout.args[1][0], /.*test success/); + t.regex(stderr.args[0][0], /.*test error/); +}); diff --git a/test/index.test.js b/test/index.test.js index fce09829..f5708b94 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -22,9 +22,13 @@ test.beforeEach(t => { // Stub the logger functions t.context.log = spy(); t.context.error = spy(); - t.context.stdout = spy(); - t.context.stderr = spy(); - t.context.logger = {log: t.context.log, error: t.context.error, stdout: t.context.stdout, stderr: t.context.stderr}; + t.context.success = spy(); + t.context.logger = { + log: t.context.log, + error: t.context.error, + success: t.context.success, + scope: () => t.context.logger, + }; }); test('Plugins are called with expected values', async t => { @@ -68,10 +72,10 @@ test('Plugins are called with expected values', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, extendEnv: false, env})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(verifyConditions1.callCount, 1); t.deepEqual(verifyConditions1.args[0][0], config); @@ -193,10 +197,10 @@ test('Use custom tag format', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); // Verify the tag has been created on the local and remote repo and reference the gitHead t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead); @@ -237,11 +241,11 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(generateNotes.callCount, 2); t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease); @@ -295,11 +299,11 @@ test('Call all "success" plugins even if one errors out', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - await t.throws(semanticRelease(options, {cwd, env: {}})); + await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(success1.callCount, 1); t.deepEqual(success1.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]); @@ -327,15 +331,17 @@ test('Log all "verifyConditions" errors', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))]; + const errors = [ + ...(await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))), + ]; t.deepEqual(errors, [error1, error2, error3]); - t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']); - t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 3', 'ERR3']); - t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [ + t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR2 error 2']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR3 error 3']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 3], [ 'An error occurred while running semantic-release: %O', error1, ]); @@ -371,14 +377,16 @@ test('Log all "verifyRelease" errors', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))]; + const errors = [ + ...(await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))), + ]; t.deepEqual(errors, [error1, error2]); - t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']); - t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']); t.is(fail.callCount, 1); t.deepEqual(fail.args[0][0], config); t.deepEqual(fail.args[0][1].errors, [error1, error2]); @@ -419,10 +427,10 @@ test('Dry-run skips publish and success', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.'); t.is(verifyConditions.callCount, 1); @@ -457,14 +465,16 @@ test('Dry-run skips fail', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))]; + const errors = [ + ...(await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))), + ]; t.deepEqual(errors, [error1, error2]); - t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']); - t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']); t.is(fail.callCount, 0); }); @@ -504,10 +514,10 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: false, branch: 'master'}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.'); t.is(verifyConditions.callCount, 1); @@ -518,7 +528,7 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t t.is(success.callCount, 0); }); -test.serial('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => { +test('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => { // Create a git repository, set the current working directory at the root of the repo const {cwd, repositoryUrl} = await gitRepo(true); // Add commits to the master branch @@ -547,12 +557,12 @@ test.serial('Dry-run does not print changelog if "generateNotes" return "undefin }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); - t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version %s:\n', '2.0.0']); + t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version 2.0.0:']); }); test('Allow local releases with "noCi" option', async t => { @@ -591,10 +601,10 @@ test('Allow local releases with "noCi" option', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: false, branch: 'master', isPr: true}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); 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( @@ -643,10 +653,10 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(analyzeCommits.callCount, 1); t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease); @@ -670,11 +680,13 @@ test('Returns falsy value if triggered by a PR', async t => { const {cwd, repositoryUrl} = await gitRepo(true); const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: true}), }); - t.falsy(await semanticRelease({cwd, repositoryUrl}, {cwd, env: {}})); + t.falsy( + await semanticRelease({cwd, repositoryUrl}, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}) + ); t.is( t.context.log.args[t.context.log.args.length - 1][0], "This run was triggered by a pull request and therefore a new version won't be published." @@ -694,18 +706,22 @@ test('Returns falsy value if triggered on an outdated clone', async t => { await gitPush(repositoryUrl, 'master', {cwd}); const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.falsy(await semanticRelease({repositoryUrl}, {cwd: repoDir, env: {}})); + t.falsy( + await semanticRelease( + {repositoryUrl}, + {cwd: repoDir, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}} + ) + ); t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [ - "The local branch %s is behind the remote one, therefore a new version won't be published.", - 'master', + "The local branch master is behind the remote one, therefore a new version won't be published.", ]); }); -test('Returns falsy value if not running from the configured branch', async t => { +test('Returns false if not running from the configured branch', async t => { // Create a git repository, set the current working directory at the root of the repo const {cwd, repositoryUrl} = await gitRepo(true); const options = { @@ -722,11 +738,11 @@ test('Returns falsy value if not running from the configured branch', async t => }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}), }); - t.falsy(await semanticRelease(options, {cwd, env: {}})); + t.falsy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is( t.context.log.args[1][0], 'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published.' @@ -759,11 +775,11 @@ test('Returns falsy value if there is no relevant changes', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.falsy(await semanticRelease(options, {cwd, env: {}})); + t.falsy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(analyzeCommits.callCount, 1); t.is(verifyRelease.callCount, 0); t.is(generateNotes.callCount, 0); @@ -807,10 +823,10 @@ test('Exclude commits with [skip release] or [release skip] from analysis', asyn }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - await semanticRelease(options, {cwd, env: {}}); + await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}); t.is(analyzeCommits.callCount, 1); t.is(analyzeCommits.args[0][1].commits.length, 2); @@ -830,15 +846,15 @@ test('Log both plugins errors and errors thrown by "fail" plugin', async t => { fail: [stub().rejects(failError1), stub().rejects(failError2)], }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - await t.throws(semanticRelease(options, {cwd, env: {}})); + await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); - t.is(t.context.error.args[t.context.error.args.length - 2][1], failError1); - t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2); - t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s Plugin error', 'ERR']); + t.is(t.context.error.args[t.context.error.args.length - 1][0], 'ERR Plugin error'); + t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1); + t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2); }); test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => { @@ -853,11 +869,11 @@ test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => { fail, }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - await t.throws(semanticRelease(options, {cwd, env: {}})); + await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.true(fail.notCalled); t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError); @@ -868,10 +884,12 @@ test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found const {cwd} = await gitRepo(); const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - const errors = [...(await t.throws(semanticRelease({}, {cwd, env: {}})))]; + const errors = [ + ...(await t.throws(semanticRelease({}, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))), + ]; // Verify error code and type t.is(errors[0].code, 'ENOREPOURL'); @@ -902,10 +920,13 @@ test('Throw an Error if plugin returns an unexpected value', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - const error = await t.throws(semanticRelease(options, {cwd, env: {}}), Error); + const error = await t.throws( + semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}), + Error + ); t.regex(error.details, /string/); }); @@ -935,10 +956,10 @@ test('Get all commits including the ones not in the shallow clone', async t => { }; const semanticRelease = requireNoCache('..', { - './lib/logger': t.context.logger, + './lib/get-logger': () => t.context.logger, 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), }); - t.truthy(await semanticRelease(options, {cwd, env: {}})); + t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})); t.is(analyzeCommits.args[0][1].commits.length, 3); }); diff --git a/test/integration.test.js b/test/integration.test.js index d1048542..194e1b7e 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -465,6 +465,10 @@ test('Run via JS API', async t => { version: '0.0.0-dev', repository: {url: repositoryUrl}, publishConfig: {registry: npmRegistry.url}, + release: { + fail: false, + success: false, + }, }); /* Initial release */ @@ -486,7 +490,7 @@ test('Run via JS API', async t => { t.log('Commit a feature'); await gitCommits(['feat: Initial commit'], {cwd}); t.log('$ Call semantic-release via API'); - await semanticRelease({fail: false, success: false}, {cwd, env}); + await semanticRelease(undefined, {cwd, env, stdout: {write: () => {}}, stderr: {write: () => {}}}); // Verify package.json and has been updated t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version); @@ -550,9 +554,9 @@ test('Log errors inheriting SemanticReleaseError and exit with 1', async t => { t.log('Commit a feature'); await gitCommits(['feat: Initial commit'], {cwd}); t.log('$ semantic-release'); - const {stdout, code} = await execa(cli, [], {env, cwd, reject: false}); + const {stderr, code} = await execa(cli, [], {env, cwd, reject: false}); // Verify the type and message are logged - t.regex(stdout, /EINHERITED Inherited error/); + t.regex(stderr, /EINHERITED Inherited error/); t.is(code, 1); }); @@ -568,13 +572,13 @@ test('Exit with 1 if missing permission to push to the remote repository', async await gitCommits(['feat: Initial commit'], {cwd}); await gitPush('origin', 'master', {cwd}); t.log('$ semantic-release'); - const {stdout, code} = await execa( + const {stderr, code} = await execa( cli, ['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'], {env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false} ); // Verify the type and message are logged - t.regex(stdout, /EGITNOPERMISSION/); + t.regex(stderr, /EGITNOPERMISSION/); t.is(code, 1); }); diff --git a/test/logger.test.js b/test/logger.test.js deleted file mode 100644 index 33578f78..00000000 --- a/test/logger.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import test from 'ava'; -import {stub} from 'sinon'; -import logger from '../lib/logger'; - -test.beforeEach(t => { - t.context.log = stub(console, 'log'); - t.context.error = stub(console, 'error'); -}); - -test.afterEach.always(t => { - t.context.log.restore(); - t.context.error.restore(); -}); - -test.serial('Basic log', t => { - logger.log('test log'); - logger.error('test error'); - - t.regex(t.context.log.args[0][0], /.*test log/); - t.regex(t.context.error.args[0][0], /.*test error/); -}); - -test.serial('Log object', t => { - const obj = {a: 1, b: '2'}; - logger.log(obj); - logger.error(obj); - - t.is(t.context.log.args[0][1], obj); - t.is(t.context.error.args[0][1], obj); -}); - -test.serial('Log with string formatting', t => { - logger.log('test log %s', 'log value'); - logger.error('test error %s', 'error value'); - - t.regex(t.context.log.args[0][0], /.*test log/); - t.regex(t.context.error.args[0][0], /.*test error/); - t.is(t.context.log.args[0][1], 'log value'); - t.is(t.context.error.args[0][1], 'error value'); -}); - -test.serial('Log with error stacktrace and properties', t => { - const error = new Error('error message'); - logger.error(error); - const otherError = new Error('other error message'); - logger.error('test error %O', otherError); - - t.is(t.context.error.args[0][1], error); - t.regex(t.context.error.args[1][0], /.*test error/); - t.is(t.context.error.args[1][1], otherError); -}); diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js index 5a59d0b7..4d9bc13b 100644 --- a/test/plugins/normalize.test.js +++ b/test/plugins/normalize.test.js @@ -8,7 +8,15 @@ const cwd = process.cwd(); test.beforeEach(t => { // Stub the logger functions t.context.log = stub(); - t.context.logger = {log: t.context.log}; + t.context.error = stub(); + t.context.success = stub(); + t.context.stderr = {write: stub()}; + t.context.logger = { + log: t.context.log, + error: t.context.error, + success: t.context.success, + scope: () => t.context.logger, + }; }); test('Normalize and load plugin from string', t => { @@ -21,7 +29,7 @@ test('Normalize and load plugin from string', t => { t.is(plugin.pluginName, './test/fixtures/plugin-noop'); t.is(typeof plugin, 'function'); - t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/plugin-noop']); + t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']); }); test('Normalize and load plugin from object', t => { @@ -34,7 +42,7 @@ test('Normalize and load plugin from object', t => { t.is(plugin.pluginName, './test/fixtures/plugin-noop'); t.is(typeof plugin, 'function'); - t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'publish', './test/fixtures/plugin-noop']); + t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop"']); }); test('Normalize and load plugin from a base file path', t => { @@ -44,11 +52,8 @@ test('Normalize and load plugin from a base file path', t => { t.is(plugin.pluginName, './plugin-noop'); t.is(typeof plugin, 'function'); - t.deepEqual(t.context.log.args[0], [ - 'Load plugin "%s" from %s in shareable config %s', - 'verifyConditions', - './plugin-noop', - './test/fixtures', + t.deepEqual(t.context.success.args[0], [ + 'Loaded plugin "verifyConditions" from "./plugin-noop" in shareable config "./test/fixtures"', ]); }); @@ -90,12 +95,17 @@ test('Normalize and load plugin that retuns multiple functions', t => { ); t.is(typeof plugin, 'function'); - t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/multi-plugin']); + t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/multi-plugin"']); }); test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => { const analyzeCommits = stub().resolves(2); - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'analyzeCommits', analyzeCommits, {}); + const plugin = normalize( + {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, + 'analyzeCommits', + analyzeCommits, + {} + ); const error = await t.throws(plugin()); @@ -108,7 +118,12 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => { const generateNotes = stub().resolves(2); - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'generateNotes', generateNotes, {}); + const plugin = normalize( + {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, + 'generateNotes', + generateNotes, + {} + ); const error = await t.throws(plugin()); @@ -120,11 +135,15 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the }); test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => { - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'publish', './plugin-identity', { - './plugin-identity': './test/fixtures', - }); + const publish = stub().resolves(2); + const plugin = normalize( + {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, + 'publish', + publish, + {} + ); - const error = await t.throws(plugin(2)); + const error = await t.throws(plugin()); t.is(error.code, 'EPUBLISHOUTPUT'); t.is(error.name, 'SemanticReleaseError'); @@ -138,9 +157,11 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi const pluginConf = {path: pluginFunction, conf: 'confValue'}; const options = {global: 'globalValue'}; const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); - await plugin('param'); + await plugin({param: 'param'}); - t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param')); + t.true( + pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, {param: 'param', logger: t.context.logger}) + ); }); test('Prevent plugins to modify "pluginConfig"', async t => { diff --git a/test/plugins/plugins.test.js b/test/plugins/plugins.test.js index 55b94395..eac70576 100644 --- a/test/plugins/plugins.test.js +++ b/test/plugins/plugins.test.js @@ -11,7 +11,8 @@ const cwd = process.cwd(); test.beforeEach(t => { // Stub the logger functions t.context.log = stub(); - t.context.logger = {log: t.context.log}; + t.context.success = stub(); + t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger}; }); test('Export default plugins', t => {