Repository: semantic-release/semantic-release (js)

Issue:
Allow to pass logger option via JS API
## New feature motivation

Currently, I am working on a tool that wraps Semantic Release to ease development at a fairly large company. I'd like to make it more "seamless" with the tool I'm creating (also a CLI tool). Since I'm using [Listr](https://github.com/SamVerschueren/listr) as a way to show the commands I'm running, I hand the Semantic-Release promise to RxJS to keep track of for when it is done. However, it messes up my terminal output when Semantic Release just spews info into the console via the `log` function. Ideally, I'd like a way to silence or redirect that output somewhere so that I can just point my users to it.

## New feature description

Add a `--silent` option that supresses the log output unless there is a failure. Ideally, the `--silent` option would also send the logs to a temp file written to disk that could then be opened after the process is done running.

## New feature implementation

Change:
https://github.com/semantic-release/semantic-release/blob/d548edcf37261ec7eaefeb7f0446289b0e91596c/lib/logger.js#L9

To do a check for `--silent` flag for starters. Then add the flag to:
https://github.com/semantic-release/semantic-release/blob/caribou/cli.js#L18

Pull request description:
JS API improvements
- Use [signale](https://github.com/klauscfhq/signale) for logging
- Clarify log output
- Allow to pass `stdout` and `stdin` when using via API
- Return `lastRelease`, `commits`, `nextRelease`, `releases` from JS API
- Document JS API

Log output example:
![hyper](https://user-images.githubusercontent.com/3250552/43053030-72d12076-8df8-11e8-97f1-1369bf32bfd0.jpg)

Fix #852

Patch:
diff --git a/README.md b/README.md
index e59a4f7..ffb58c3 100644
--- a/README.md
+++ b/README.md
@@ -105,13 +105,11 @@ After running the tests the command `semantic-release` will execute the followin
   - [CI configurations](docs/recipes/README.md)
   - [Package managers and languages](docs/recipes/README.md)
 - Developer guide
+  - [JavaScript API](docs/developer-guide/js-api.md)
   - [Plugins](docs/developer-guide/plugin.md)
   - [Shareable configuration](docs/developer-guide/shareable-configuration.md)
-- Resources
-  - [Videos](docs/resources.md#videos)
-  - [Articles](docs/resources.md#articles)
-  - [Tutorials](docs/resources.md#tutorials)
 - Support
+  - [Resources](docs/support/resources.md)
   - [Frequently Asked Questions](docs/support/FAQ.md)
   - [Troubleshooting](docs/support/troubleshooting.md)
   - [Node version requirement](docs/support/node-version.md)
diff --git a/SUMMARY.md b/SUMMARY.md
index 56b12f1..a9cfa57 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -22,6 +22,7 @@
 - [Package managers and languages](docs/recipes/README.md)
 
 ## Developer guide
+- [JavaScript API](docs/developer-guide/js-api.md)
 - [Plugin](docs/developer-guide/plugin.md)
 - [Shareable configuration](docs/developer-guide/shareable-configuration.md)
 
diff --git a/docs/developer-guide/README.md b/docs/developer-guide/README.md
index afd97c5..e9b3d06 100644
--- a/docs/developer-guide/README.md
+++ b/docs/developer-guide/README.md
@@ -1,4 +1,5 @@
 # Developer guide
 
+- [JavaScript API](js-api.md)
 - [Plugins](plugin.md)
 - [Shareable configuration](shareable-configuration.md)
diff --git a/docs/developer-guide/js-api.md b/docs/developer-guide/js-api.md
new file mode 100644
index 0000000..415bf0e
--- /dev/null
+++ b/docs/developer-guide/js-api.md
@@ -0,0 +1,270 @@
+# JavaScript API
+
+## Usage
+
+```js
+const semanticRelease = require('semantic-release');
+const {WritableStreamBuffer} = require('stream-buffers');
+
+const stdoutBuffer = WritableStreamBuffer();
+const stderrBuffer = WritableStreamBuffer();
+
+try {
+  const result = await semanticRelease({
+    // Core options
+    branch: 'master',
+    repositoryUrl: 'https://github.com/me/my-package.git',
+    // Shareable config
+    extends: 'my-shareable-config',
+    // Plugin options
+    githubUrl: 'https://my-ghe.com',
+    githubApiPathPrefix: '/api-prefix'
+  }, {
+    // Run semantic-release from `/path/to/git/repo/root` without having to change local process `cwd` with `process.chdir()`
+    cwd: '/path/to/git/repo/root',
+    // Pass the variable `MY_ENV_VAR` to semantic-release without having to modify the local `process.env`
+    env: {...process.env, MY_ENV_VAR: 'MY_ENV_VAR_VALUE'},
+    // Store stdout and stderr to use later instead of writing to `process.stdout` and `process.stderr`
+    stdout: stdoutBuffer,
+    stderr: stderrBuffer
+  });
+
+  if (result) {
+    const {lastRelease, commits, nextRelease, releases} = result;
+
+    console.log(`Published ${nextRelease.type} release version ${nextRelease.version} containing ${commits.length} commits.`);
+
+    if (lastRelease.version) {
+      console.log(`The last release was "${lastRelease.version}".`);
+    }
+
+    for (const release of releases) {
+      console.log(`The release was published with plugin "${pluginName}".`);
+    }
+  } else {
+    console.log('No release published.');
+  }
+
+  // Get stdout and stderr content
+  const logs = stdoutBuffer.getContentsAsString('utf8');
+  const errors = stderrBuffer.getContentsAsString('utf8');
+} catch (err) {
+  console.error('The automated release failed with %O', err)
+}
+```
+
+## API
+
+### semanticRelease([options], [config]) => Promise<Result>
+
+Run **semantic-release** and returns a `Promise` that resolves to a [Result](#result) object.
+
+#### options
+
+Type: `Object`
+
+**semantic-release** options.
+
+Can be used to set any [core option](../usage/configuration.md#configuration) or [plugin options](../usage/plugins.md#configuration).
+
+Each option, will take precedence over options configured in the [configuration file](../usage/configuration.md#configuration) and [shareable configurations](../usage/configuration.md#extends).
+
+#### config
+
+Type: `Object`
+
+**semantic-release** configuration specific for API usage.
+
+##### cwd
+
+Type: `String`<br>
+Default: `process.cwd()`
+
+The current working directory to use. It should be configured to the root of the Git repository to release from.
+
+It allows to run **semantic-release** from a specific path without having to change the local process `cwd` with `process.chdir()`.
+
+##### env
+
+Type: `Object`<br>
+Default: `process.env`
+
+The environment variables to use.
+
+It allows to run **semantic-release** with specific environment variables without having to modify the local `process.env`.
+
+##### stdout
+
+Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
+Default: `process.stdout`
+
+The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log information.
+
+It allows to configure **semantic-release** to write logs to a specific stream rather than the local `process.stdout`.
+
+##### stderr
+
+Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
+Default: `process.stderr`
+
+The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log errors.
+
+It allows to configure **semantic-release** to write errors to a specific stream rather than the local `process.stderr`.
+
+### Result
+
+Type: `Object` `Boolean`<br>
+
+And object with [`lastRelease`](#lastrelease), [`nextRelease`](#nextrelease), [`commits`](#commits) and [`releases`](#releases) if a release is published or `false` if no release was published.
+
+#### lastRelease
+
+Type: `Object`
+
+Information related to the last release found:
+
+| Name    | Type     | Description                                                                                        |
+|---------|----------|----------------------------------------------------------------------------------------------------|
+| version | `String` | The version of the last release.                                                                   |
+| gitHead | `String` | The sha of the last commit being part of the last release.                                         |
+| gitTag  | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the last release. |
+
+**Notes**: If no previous release is found, `lastRelease` will be an empty `Object`.
+
+Example:
+```js
+{
+  gitHead: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
+  version: '1.0.0',
+  gitTag: 'v1.0.0',
+}
+```
+
+#### commits
+
+Type: `Array<Object>`
+
+The list of commit included in the new release.<br>
+Each commit object has the following properties:
+
+| Name            | Type     | Description                                     |
+|-----------------|----------|-------------------------------------------------|
+| commit          | `Object` | The commit abbreviated and full hash.           |
+| commit.long     | `String` | The commit hash.                                |
+| commit.short    | `String` | The commit abbreviated hash.                    |
+| tree            | `Object` | The commit abbreviated and full tree hash.      |
+| tree.long       | `String` | The commit tree hash.                           |
+| tree.short      | `String` | The commit abbreviated tree hash.               |
+| author          | `Object` | The commit author information.                  |
+| author.name     | `String` | The commit author name.                         |
+| author.email    | `String` | The commit author email.                        |
+| author.short    | `String` | The commit author date.                         |
+| committer       | `Object` | The committer information.                      |
+| committer.name  | `String` | The committer name.                             |
+| committer.email | `String` | The committer email.                            |
+| committer.short | `String` | The committer date.                             |
+| subject         | `String` | The commit subject.                             |
+| body            | `String` | The commit body.                                |
+| message         | `String` | The commit full message (`subject` and `body`). |
+| hash            | `String` | The commit hash.                                |
+| committerDate   | `String` | The committer date.                             |
+
+Example:
+```js
+[
+  {
+    commit: {
+      long: '68eb2c4d778050b0701136ca129f837d7ed494d2',
+      short: '68eb2c4'
+    },
+    tree: {
+      long: '7ab515d12bd2cf431745511ac4ee13fed15ab578',
+      short: '7ab515d'
+    },
+    author: {
+      name: 'Me',
+      email: 'me@email.com',
+      date: 2018-07-22T20:52:44.000Z
+    },
+    committer: {
+      name: 'Me',
+      email: 'me@email.com',
+      date: 2018-07-22T20:52:44.000Z
+    },
+    subject: 'feat: a new feature',
+    body: 'Description of the new feature',
+    hash: '68eb2c4d778050b0701136ca129f837d7ed494d2',
+    message: 'feat: a new feature\n\nDescription of the new feature',
+    committerDate: 2018-07-22T20:52:44.000Z
+  }
+ ]
+```
+
+#### nextRelease
+
+Type: `Object`
+
+Information related to the newly published release:
+
+| Name    | Type     | Description                                                                                       |
+|---------|----------|---------------------------------------------------------------------------------------------------|
+| type    | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`).               |
+| version | `String` | The version of the new release.                                                                   |
+| gitHead | `String` | The sha of the last commit being part of the new release.                                         |
+| gitTag  | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the new release. |
+| notes   | `String` | The release notes for the new release.                                                            |
+
+Example:
+```js
+{
+  type: 'minor',
+  gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
+  version: '1.1.0',
+  gitTag: 'v1.1.0',
+  notes: 'Release notes for version 1.1.0...',
+}
+```
+
+#### releases
+
+Type: `Array<Object>`
+
+The list of releases published, one release per [publish plugin](../usage/plugins.md#publish-plugin).<br>
+Each release object has the following properties:
+
+| Name       | Type     | Description                                                                                   |
+|------------|----------|-----------------------------------------------------------------------------------------------|
+| name       | `String` | **Optional.** The release name, only if set by the corresponding `publish` plugin.            |
+| url        | `String` | **Optional.** The release URL, only if set by the corresponding `publish` plugin.             |
+| type       | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`).           |
+| version    | `String` | The version of the release.                                                                   |
+| gitHead    | `String` | The sha of the last commit being part of the release.                                         |
+| gitTag     | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the release. |
+| notes      | `String` | The release notes for the release.                                                            |
+| pluginName | `String` | The name of the plugin that published the release.                                            |
+
+Example:
+```js
+[
+  {
+    name: 'GitHub release',
+    url: 'https://github.com/me/my-package/releases/tag/v1.1.0',
+    type: 'minor',
+    gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
+    version: '1.1.0',
+    gitTag: 'v1.1.0',
+    notes: 'Release notes for version 1.1.0...',
+    pluginName: '@semantic-release/github'
+  },
+  {
+    name: 'npm package (@latest dist-tag)',
+    url: 'https://www.npmjs.com/package/my-package',
+    type: 'minor',
+    gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
+    version: '1.1.0',
+    gitTag: 'v1.1.0',
+    notes: 'Release notes for version 1.1.0...',
+    pluginName: '@semantic-release/npm'
+   }
+ ]
+```
diff --git a/index.js b/index.js
index 641be34..3d452df 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,5 @@
 const process = require('process');
-const {template} = require('lodash');
+const {template, pick} = require('lodash');
 const marked = require('marked');
 const TerminalRenderer = require('marked-terminal');
 const envCi = require('env-ci');
@@ -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');
@@ -42,7 +42,7 @@ async function run(context, plugins) {
 
   if (isCi && isPr && !options.noCi) {
     logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
-    return;
+    return false;
   }
 
   if (ciBranch !== options.branch) {
@@ -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);
 
@@ -85,7 +85,7 @@ async function run(context, plugins) {
 
   if (!nextRelease.type) {
     logger.log('There are no relevant changes, so no new version is released.');
-    return;
+    return false;
   }
   context.nextRelease = nextRelease;
   nextRelease.version = getNextVersion(context);
@@ -95,35 +95,36 @@ 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;
+
+  return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
 }
 
-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 +143,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 981c13f..69d20df 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 d9eec16..77da1dd 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 0000000..52b8f0d
--- /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 7d621e3..10089e9 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 56465b7..0000000
--- 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/index.js b/lib/plugins/index.js
index 05b7b33..a1838a9 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -5,7 +5,8 @@ const PLUGINS_DEFINITIONS = require('../definitions/plugins');
 const pipeline = require('./pipeline');
 const normalize = require('./normalize');
 
-module.exports = ({cwd, options, logger}, pluginsPath) => {
+module.exports = (context, pluginsPath) => {
+  const {options, logger} = context;
   const errors = [];
   const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
     (
@@ -30,7 +31,7 @@ module.exports = ({cwd, options, logger}, pluginsPath) => {
       }
 
       const steps = castArray(pluginOpts).map(pluginOpt =>
-        normalize({cwd, options: omit(options, Object.keys(PLUGINS_DEFINITIONS)), logger}, type, pluginOpt, pluginsPath)
+        normalize({...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS))}, type, pluginOpt, pluginsPath)
       );
 
       plugins[type] = async input =>
diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js
index fdcfd6f..15ff4ce 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 bd3c348..1b656f2 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": {


Test patch:
diff --git a/test/get-last-release.test.js b/test/get-last-release.test.js
index 85d3859..bf842cc 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 0000000..31e7562
--- /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 fce0982..0c8eb63 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,15 @@ 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}));
+  const result = await semanticRelease(options, {
+    cwd,
+    env,
+    stdout: {write: () => {}},
+    stderr: {write: () => {}},
+  });
 
   t.is(verifyConditions1.callCount, 1);
   t.deepEqual(verifyConditions1.args[0][0], config);
@@ -159,6 +168,16 @@ test('Plugins are called with expected values', async t => {
     {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
   ]);
 
+  t.deepEqual(result, {
+    lastRelease,
+    commits: [commits[0]],
+    nextRelease: {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`},
+    releases: [
+      {...release1, ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: '[Function: proxy]'},
+      {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
+    ],
+  });
+
   // 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);
   t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), nextRelease.gitHead);
@@ -193,10 +212,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 +256,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 +314,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 +346,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 +392,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 +442,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 +480,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 +529,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 +543,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 +572,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 +616,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 +668,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);
@@ -665,23 +690,25 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
   t.is(publish.args[0][1].nextRelease.notes, notes2);
 });
 
-test('Returns falsy value if triggered by a PR', async t => {
+test('Returns false if triggered by a PR', async t => {
   // Create a git repository, set the current working directory at the root of the repo
   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.false(
+    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."
   );
 });
 
-test('Returns falsy value if triggered on an outdated clone', async t => {
+test('Returns false if triggered on an outdated clone', async t => {
   // Create a git repository, set the current working directory at the root of the repo
   let {cwd, repositoryUrl} = await gitRepo(true);
   const repoDir = cwd;
@@ -694,18 +721,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.false(
+    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,18 +753,18 @@ 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.false(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.'
   );
 });
 
-test('Returns falsy value if there is no relevant changes', async t => {
+test('Returns false if there is no relevant changes', 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
@@ -759,11 +790,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.false(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 +838,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 +861,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 +884,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 +899,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 +935,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 +971,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 d104854..194e1b7 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 33578f7..0000000
--- 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 5a59d0b..4d9bc13 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 55b9439..eac7057 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 => {