Repository: semantic-release/semantic-release
Pull request #537 (2017-11-29 06:54:11)

Various fixes
Fix a few typos in doc and log messages.
Minor improvement in tests and a few additional tests.
All the details should be in the commits messages.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 3d14d27..e08cb38 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,7 +21,6 @@ install:
   - travis_retry npm install
 
 script:
-  - git config --get user.email
   - npm run test
 
 after_success:
diff --git a/index.js b/index.js
index ae22e27..a42467a 100644
--- a/index.js
+++ b/index.js
@@ -19,7 +19,7 @@ module.exports = async opts => {
     throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
   }
 
-  logger.log('Run automated release from branch %s', options.name, options.branch);
+  logger.log('Run automated release from branch %s', options.branch);
 
   if (!options.dryRun) {
     logger.log('Call plugin %s', 'verify-conditions');
diff --git a/lib/get-commits.js b/lib/get-commits.js
index 06cd437..5f1e342 100644
--- a/lib/get-commits.js
+++ b/lib/get-commits.js
@@ -18,6 +18,7 @@ const getVersionHead = require('./get-version-head');
  * @typedef {Object} LastRelease
  * @property {string} version The version number of the last release.
  * @property {string} [gitHead] The commit sha used to make the last release.
+ * @property {string} [gitTag] The tag used to make the last release.
  */
 
 /**
@@ -45,7 +46,7 @@ const getVersionHead = require('./get-version-head');
  * @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `lastRelease.gitHead` or the commit sha derived from `config.lastRelease.version` is not in the direct history of `branch`.
  * @throws {SemanticReleaseError} with code `ENOGITHEAD` if `lastRelease.gitHead` is undefined and no commit sha can be found for the `config.lastRelease.version`.
  */
-module.exports = async ({version, gitHead}, branch, logger) => {
+module.exports = async ({version, gitHead} = {}, branch, logger) => {
   let gitTag;
   if (gitHead || version) {
     try {
diff --git a/lib/get-next-version.js b/lib/get-next-version.js
index a48921b..2d02a0f 100644
--- a/lib/get-next-version.js
+++ b/lib/get-next-version.js
@@ -1,13 +1,9 @@
 const semver = require('semver');
-const SemanticReleaseError = require('@semantic-release/error');
 
 module.exports = (type, lastRelease, logger) => {
   let version;
   if (lastRelease.version) {
     version = semver.inc(lastRelease.version, type);
-    if (!version) {
-      throw new SemanticReleaseError(`Invalid release type ${type}`, 'EINVALIDTYPE');
-    }
     logger.log('The next release version is %s', version);
   } else {
     version = '1.0.0';
diff --git a/lib/plugins/definitions.js b/lib/plugins/definitions.js
index 60909d5..20a22e0 100644
--- a/lib/plugins/definitions.js
+++ b/lib/plugins/definitions.js
@@ -38,7 +38,7 @@ module.exports = {
     },
     output: {
       validator: output => !output || RELEASE_TYPE.includes(output),
-      message: 'The "analyzeCommits" plugin output must be either undefined or a valid semver release type.',
+      message: 'The "analyzeCommits" plugin output, if defined, must be a valid semver release type.',
     },
   },
   verifyRelease: {
@@ -57,8 +57,8 @@ module.exports = {
         'The "generateNotes" plugin, if defined, must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
     },
     output: {
-      validator: output => isString(output),
-      message: 'The "generateNotes" plugin output must be a string.',
+      validator: output => !output || isString(output),
+      message: 'The "generateNotes" plugin output, if defined, must be a string.',
     },
   },
   publish: {
diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js
index 3a6175f..4b508a9 100644
--- a/lib/plugins/normalize.js
+++ b/lib/plugins/normalize.js
@@ -27,7 +27,7 @@ module.exports = (pluginType, pluginConfig, logger, validator) => {
     const result = await func(cloneDeep(input));
 
     if (validator && !validator.validator(result)) {
-      throw new Error(`${validator.message}. Received: ${inspect(result)}`);
+      throw new Error(`${validator.message} Received: ${inspect(result)}`);
     }
     return result;
   };
diff --git a/package.json b/package.json
index bec1dc5..0c30734 100644
--- a/package.json
+++ b/package.json
@@ -127,8 +127,7 @@
       "prettier"
     ],
     "rules": {
-      "prettier/prettier": 2,
-      "no-duplicate-imports": 2
+      "prettier/prettier": 2
     }
   }
 }


Test patch:
diff --git a/test/get-commits.test.js b/test/get-commits.test.js
index 3366de4..ad59f60 100644
--- a/test/get-commits.test.js
+++ b/test/get-commits.test.js
@@ -1,6 +1,5 @@
 import test from 'ava';
 import {stub} from 'sinon';
-import SemanticReleaseError from '@semantic-release/error';
 import getCommits from '../lib/get-commits';
 import {
   gitRepo,
@@ -409,7 +408,7 @@ test.serial('Throws ENOGITHEAD error if the gitHead of the last release cannot b
 
   // Verify error code and type
   t.is(error.code, 'ENOGITHEAD');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   // Verify the log function has been called with a message explaining the error
   t.regex(
     t.context.error.firstCall.args[0],
@@ -428,7 +427,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in history', async t =
 
   // Verify error code and type
   t.is(error.code, 'ENOTINHISTORY');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   // Verify the log function has been called with a message mentionning the branch
   t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
   // Verify the log function has been called with a message mentionning the missing gitHead
@@ -453,7 +452,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in branch history but
 
   // Verify error code and type
   t.is(error.code, 'ENOTINHISTORY');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   // Verify the log function has been called with a message mentionning the branch
   t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
   // Verify the log function has been called with a message mentionning the missing gitHead
@@ -482,7 +481,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in detached head but p
 
   // Verify error code and type
   t.is(error.code, 'ENOTINHISTORY');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   // Verify the log function has been called with a message mentionning the branch
   t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
   // Verify the log function has been called with a message mentionning the missing gitHead
@@ -508,7 +507,7 @@ test.serial('Throws ENOTINHISTORY error when a tag is not in branch history but
   const error = await t.throws(getCommits({version: '1.0.0'}, 'master', t.context.logger));
   // Verify error code and type
   t.is(error.code, 'ENOTINHISTORY');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   // Verify the log function has been called with a message mentionning the branch
   t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
   // Verify the log function has been called with a message mentionning the missing gitHead
diff --git a/test/get-next-version.test.js b/test/get-next-version.test.js
index 32ea3bc..96ccddd 100644
--- a/test/get-next-version.test.js
+++ b/test/get-next-version.test.js
@@ -1,6 +1,5 @@
 import test from 'ava';
 import {stub} from 'sinon';
-import SemanticReleaseError from '@semantic-release/error';
 import getNextVersion from '../lib/get-next-version';
 
 test.beforeEach(t => {
@@ -28,9 +27,3 @@ test('Return 1.0.0 if there is no previous release', t => {
   const version = getNextVersion('minor', {}, t.context.logger);
   t.is(version, '1.0.0');
 });
-
-test('Return an error if the release type is invalid', t => {
-  const error = t.throws(() => getNextVersion('invalid', {version: '1.0.0'}, t.context.logger));
-  t.is(error.code, 'EINVALIDTYPE');
-  t.true(error instanceof SemanticReleaseError);
-});
diff --git a/test/git.test.js b/test/git.test.js
index 2409793..57ae1e5 100644
--- a/test/git.test.js
+++ b/test/git.test.js
@@ -36,7 +36,7 @@ test.serial('Throw error if the last commit sha cannot be found', async t => {
   // Create a git repository, set the current working directory at the root of the repo
   await gitRepo();
 
-  await t.throws(gitHead());
+  await t.throws(gitHead(), Error);
 });
 
 test.serial('Unshallow repository', async t => {
diff --git a/test/helpers/mockserver.js b/test/helpers/mockserver.js
index fbae5a5..4e8f0bf 100644
--- a/test/helpers/mockserver.js
+++ b/test/helpers/mockserver.js
@@ -4,48 +4,47 @@ import got from 'got';
 import pRetry from 'p-retry';
 import {mockServerClient} from 'mockserver-client';
 
+const IMAGE = 'jamesdbloom/mockserver:latest';
 const MOCK_SERVER_PORT = 1080;
 const MOCK_SERVER_HOST = 'localhost';
 const docker = new Docker();
 let container;
 
-async function checkStatus() {
-  const response = await got.put(`http://${MOCK_SERVER_HOST}:${MOCK_SERVER_PORT}/status`, {cache: false});
-  if (response.status === 200) {
-    // If the registry returns a 200 status, it's ready. Abort the retry.
-    throw new pRetry.AbortError();
-  }
-}
-
 /**
  * Download the `mockserver` Docker image, create a new container and start it.
  *
  * @return {Promise} Promise that resolves when the container is started.
  */
 async function start() {
-  await getStream(await docker.pull('jamesdbloom/mockserver:latest'));
+  await getStream(await docker.pull(IMAGE));
 
   container = await docker.createContainer({
-    Image: 'jamesdbloom/mockserver',
+    Tty: true,
+    Image: IMAGE,
     PortBindings: {[`${MOCK_SERVER_PORT}/tcp`]: [{HostPort: `${MOCK_SERVER_PORT}`}]},
   });
   await container.start();
 
   try {
     // Wait for the mock server to be ready
-    await pRetry(checkStatus, {retries: 5, minTimeout: 1000, factor: 2});
+    await pRetry(() => got.put(`http://${MOCK_SERVER_HOST}:${MOCK_SERVER_PORT}/status`, {cache: false}), {
+      retries: 7,
+      minTimeout: 1000,
+      factor: 2,
+    });
   } catch (err) {
-    throw new Error(`Couldn't start mock-server after 30s`);
+    throw new Error(`Couldn't start mock-server after 2 min`);
   }
 }
 
 /**
- * Stop the `mockserver` Docker container.
+ * Stop and remote the `mockserver` Docker container.
  *
  * @return {Promise} Promise that resolves when the container is stopped.
  */
 async function stop() {
-  return container.stop();
+  await container.stop();
+  await container.remove();
 }
 
 /**
diff --git a/test/helpers/npm-registry.js b/test/helpers/npm-registry.js
index 0646fdb..2689339 100644
--- a/test/helpers/npm-registry.js
+++ b/test/helpers/npm-registry.js
@@ -3,6 +3,7 @@ import getStream from 'get-stream';
 import got from 'got';
 import pRetry from 'p-retry';
 
+const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
 const SERVER_PORT = 15986;
 const COUCHDB_PORT = 5984;
 const SERVER_HOST = 'localhost';
@@ -12,19 +13,17 @@ const NPM_EMAIL = 'integration@test.com';
 const docker = new Docker();
 let container;
 
-async function checkStatus() {
-  const response = await got(`http://${SERVER_HOST}:${SERVER_PORT}/registry/_design/app`, {cache: false});
-  if (response.status === 200) {
-    // If the registry returns a 200 status, it's ready. Abort the retry.
-    throw new pRetry.AbortError();
-  }
-}
-
+/**
+ * Download the `npm-docker-couchdb` Docker image, create a new container and start it.
+ *
+ * @return {Promise} Promise that resolves when the container is started.
+ */
 async function start() {
-  await getStream(await docker.pull('npmjs/npm-docker-couchdb:1.6.1'));
+  await getStream(await docker.pull(IMAGE));
 
   container = await docker.createContainer({
-    Image: 'npmjs/npm-docker-couchdb:1.6.1',
+    Tty: true,
+    Image: IMAGE,
     PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
   });
 
@@ -32,9 +31,13 @@ async function start() {
 
   try {
     // Wait for the registry to be ready
-    await pRetry(checkStatus, {retries: 5, minTimeout: 1000, factor: 2});
+    await pRetry(() => got(`http://${SERVER_HOST}:${SERVER_PORT}/registry/_design/app`, {cache: false}), {
+      retries: 7,
+      minTimeout: 1000,
+      factor: 2,
+    });
   } catch (err) {
-    throw new Error(`Couldn't start npm-docker-couchdb after 30s`);
+    throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
   }
 
   // Create user
@@ -62,8 +65,14 @@ const authEnv = {
   NPM_EMAIL,
 };
 
+/**
+ * Stop and remote the `npm-docker-couchdb` Docker container.
+ *
+ * @return {Promise} Promise that resolves when the container is stopped.
+ */
 async function stop() {
-  return container.stop();
+  await container.stop();
+  await container.remove();
 }
 
 export default {start, stop, authEnv, url};
diff --git a/test/index.test.js b/test/index.test.js
index d0ff908..54051a5 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -2,7 +2,7 @@ import test from 'ava';
 import proxyquire from 'proxyquire';
 import {stub} from 'sinon';
 import tempy from 'tempy';
-import SemanticReleaseError from '@semantic-release/error';
+import DEFINITIONS from '../lib/plugins/definitions';
 import {gitHead as getGitHead} from '../lib/git';
 import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils';
 
@@ -193,6 +193,54 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
   t.true(publish.notCalled);
 });
 
+test.serial('Accept "undefined" values for the "getLastRelease" and "generateNotes" plugins', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Add commits to the master branch
+  await gitCommits(['First']);
+  // Create the tag corresponding to version 1.0.0
+  await gitTagVersion('v1.0.0');
+  // Add new commits to the master branch
+  await gitCommits(['Second']);
+
+  const lastRelease = {gitHead: undefined, gitTag: undefined, version: undefined};
+  const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
+  const verifyConditions = stub().resolves();
+  const getLastRelease = stub().resolves();
+  const analyzeCommits = stub().resolves(nextRelease.type);
+  const verifyRelease = stub().resolves();
+  const generateNotes = stub().resolves();
+  const publish = stub().resolves();
+
+  const options = {
+    branch: 'master',
+    repositoryUrl: 'git@hostname.com:owner/module.git',
+    verifyConditions: [verifyConditions],
+    getLastRelease,
+    analyzeCommits,
+    verifyRelease,
+    generateNotes,
+    publish,
+  };
+
+  await t.context.semanticRelease(options);
+
+  t.true(getLastRelease.calledOnce);
+
+  t.true(analyzeCommits.calledOnce);
+  t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
+
+  t.true(verifyRelease.calledOnce);
+  t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
+
+  t.true(generateNotes.calledOnce);
+  t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
+
+  t.true(publish.calledOnce);
+  t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
+  t.falsy(publish.firstCall.args[1].nextRelease.notes);
+});
+
 test.serial('Throw SemanticReleaseError if not running from a git repository', async t => {
   // Set the current working directory to a temp directory
   process.chdir(tempy.directory());
@@ -201,10 +249,10 @@ test.serial('Throw SemanticReleaseError if not running from a git repository', a
 
   // Verify error code and type
   t.is(error.code, 'ENOGITREPO');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
 });
 
-test.serial('Throw SemanticReleaseError if repositoryUrl is not set and canot be found', async t => {
+test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found', async t => {
   // Create a git repository, set the current working directory at the root of the repo
   await gitRepo();
 
@@ -212,5 +260,32 @@ test.serial('Throw SemanticReleaseError if repositoryUrl is not set and canot be
 
   // Verify error code and type
   t.is(error.code, 'ENOREPOURL');
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
+});
+
+test.serial('Throw an Error if returns an unexpected value', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Add commits to the master branch
+  await gitCommits(['First']);
+  // Create the tag corresponding to version 1.0.0
+  await gitTagVersion('v1.0.0');
+  // Add new commits to the master branch
+  await gitCommits(['Second']);
+
+  const verifyConditions = stub().resolves();
+  const getLastRelease = stub().resolves('string');
+
+  const options = {
+    branch: 'master',
+    repositoryUrl: 'git@hostname.com:owner/module.git',
+    verifyConditions: [verifyConditions],
+    getLastRelease,
+  };
+
+  const error = await t.throws(t.context.semanticRelease(options), Error);
+
+  // Verify error message
+  t.regex(error.message, new RegExp(DEFINITIONS.getLastRelease.output.message));
+  t.regex(error.message, /Received: 'string'/);
 });
diff --git a/test/integration.test.js b/test/integration.test.js
index 89dc76b..3827583 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -9,15 +9,20 @@ import semanticRelease from '..';
 
 /* eslint camelcase: ["error", {properties: "never"}] */
 
-// Environment variables used with cli
+// Environment variables used with semantic-release cli (similar to what a user would setup)
 const env = {
-  npm_config_registry: npmRegistry.url,
   GH_TOKEN: 'github_token',
   GITHUB_URL: mockServer.url,
   NPM_EMAIL: 'integration@test.com',
   NPM_USERNAME: 'integration',
   NPM_PASSWORD: 'suchsecure',
 };
+// Environment variables used only for the local npm command used to do verification
+const testEnv = Object.assign({}, process.env, {
+  npm_config_registry: npmRegistry.url,
+  NPM_EMAIL: 'integration@test.com',
+  LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
+});
 const cli = require.resolve('../bin/semantic-release');
 const pluginError = require.resolve('./fixtures/plugin-error');
 const pluginInheritedError = require.resolve('./fixtures/plugin-error-inherited');
@@ -92,7 +97,7 @@ test.serial('Release patch, minor and major versions', async t => {
     publishConfig: {registry: npmRegistry.url},
   });
   // Create a npm-shrinkwrap.json file
-  await execa('npm', ['shrinkwrap'], {env});
+  await execa('npm', ['shrinkwrap'], {env: testEnv});
 
   /* No release */
 
@@ -143,7 +148,7 @@ test.serial('Release patch, minor and major versions', async t => {
 
   // Retrieve the published package from the registry and check version and gitHead
   let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   t.is(releasedVersion, version);
   t.is(releasedGitHead, await gitHead());
@@ -188,7 +193,7 @@ test.serial('Release patch, minor and major versions', async t => {
 
   // Retrieve the published package from the registry and check version and gitHead
   [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   t.is(releasedVersion, version);
   t.is(releasedGitHead, await gitHead());
@@ -233,7 +238,7 @@ test.serial('Release patch, minor and major versions', async t => {
 
   // Retrieve the published package from the registry and check version and gitHead
   [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   t.is(releasedVersion, version);
   t.is(releasedGitHead, await gitHead());
@@ -278,7 +283,7 @@ test.serial('Release patch, minor and major versions', async t => {
 
   // Retrieve the published package from the registry and check version and gitHead
   [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   t.is(releasedVersion, version);
   t.is(releasedGitHead, await gitHead());
@@ -337,7 +342,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
   // Verify package.json has been updated
   t.is((await readJson('./package.json')).version, version);
   // Retrieve the published package from the registry and check version
-  let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
+  let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
   t.is(releasedVersion, version);
   t.log(`+ released ${releasedVersion}`);
   await mockServer.verify(verifyMock);
@@ -378,7 +383,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
   t.is((await readJson('./package.json')).version, version);
 
   // Retrieve the published package from the registry and check version
-  releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
+  releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
   t.is(releasedVersion, version);
   t.log(`+ released ${releasedVersion}`);
   await mockServer.verify(verifyMock);
@@ -452,7 +457,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
 
   // Retrieve the published package from the registry and check version and gitHead
   let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   const head = await gitHead();
   t.is(releasedGitHead, head);
@@ -527,7 +532,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
   t.is((await readJson('./package.json')).version, version);
 
   // Retrieve the published package from the registry and check version and gitHead
-  releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
+  releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
   t.is(releasedVersion, version);
   t.log(`+ released ${releasedVersion}`);
   await mockServer.verify(verifyMock);
@@ -596,7 +601,7 @@ test.serial('Pass options via CLI arguments', async t => {
 
   // Retrieve the published package from the registry and check version and gitHead
   const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   t.is(releasedVersion, version);
   t.is(releasedGitHead, await gitHead());
@@ -658,7 +663,7 @@ test.serial('Run via JS API', async t => {
 
   // Retrieve the published package from the registry and check version and gitHead
   const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
-    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
   );
   t.is(releasedVersion, version);
   t.is(releasedGitHead, await gitHead());
diff --git a/test/plugins/definitions.test.js b/test/plugins/definitions.test.js
index 2582026..5c1f673 100644
--- a/test/plugins/definitions.test.js
+++ b/test/plugins/definitions.test.js
@@ -89,12 +89,12 @@ test('The "analyzeCommits" plugin output must be either undefined or a valid sem
   t.true(definitions.analyzeCommits.output.validator('major'));
 });
 
-test('The "generateNotes" plugin output must be a string', t => {
-  t.false(definitions.generateNotes.output.validator());
-  t.false(definitions.generateNotes.output.validator(null));
+test('The "generateNotes" plugin output, if defined, must be a string', t => {
   t.false(definitions.generateNotes.output.validator(1));
   t.false(definitions.generateNotes.output.validator({}));
 
+  t.true(definitions.generateNotes.output.validator());
+  t.true(definitions.generateNotes.output.validator(null));
   t.true(definitions.generateNotes.output.validator(''));
   t.true(definitions.generateNotes.output.validator('string'));
 });
diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js
index 7667e03..5171117 100644
--- a/test/plugins/normalize.test.js
+++ b/test/plugins/normalize.test.js
@@ -40,13 +40,13 @@ test('Wrap plugin in a function that validate the output of the plugin', async t
   const pluginFunction = stub().resolves(1);
   const plugin = normalize('', pluginFunction, t.context.logger, {
     validator: output => output === 1,
-    message: 'The output must be 1',
+    message: 'The output must be 1.',
   });
 
   await t.notThrows(plugin());
 
   pluginFunction.resolves(2);
-  const error = await t.throws(plugin());
+  const error = await t.throws(plugin(), Error);
   t.is(error.message, 'The output must be 1. Received: 2');
 });
 
@@ -104,7 +104,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async
 });
 
 test('Throws an error if the plugin return an object without the expected plugin function', t => {
-  const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger));
+  const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger), Error);
 
   t.is(
     error.message,
diff --git a/test/plugins/pipeline.test.js b/test/plugins/pipeline.test.js
index ea26f2c..3b35633 100644
--- a/test/plugins/pipeline.test.js
+++ b/test/plugins/pipeline.test.js
@@ -32,7 +32,7 @@ test('Stop execution and throw error is a step rejects', async t => {
   const step2 = stub().throws(new Error('test error'));
   const step3 = stub().resolves(3);
 
-  const error = await t.throws(pipeline([step1, step2, step3])(0));
+  const error = await t.throws(pipeline([step1, step2, step3])(0), Error);
   t.is(error.message, 'test error');
   t.true(step1.calledWith(0));
   t.true(step2.calledWith(0));
diff --git a/test/plugins/plugins.test.js b/test/plugins/plugins.test.js
index a4525cf..edea9c9 100644
--- a/test/plugins/plugins.test.js
+++ b/test/plugins/plugins.test.js
@@ -49,7 +49,7 @@ test('Use default when only options are passed for a single plugin', t => {
 });
 
 test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
-  const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger));
+  const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger), Error);
 
   t.is(
     error.message,
@@ -58,7 +58,10 @@ test('Throw an error if plugin configuration is missing a path for plugin pipeli
 });
 
 test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
-  const error = t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, t.context.logger));
+  const error = t.throws(
+    () => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, t.context.logger),
+    Error
+  );
 
   t.is(
     error.message,