Repository: semantic-release/npm
Pull request #23 (2017-12-20 01:12:06)

Add `pkgRoot` option to publish a sub-directory
Allow to publish from a sub-directory, similarly to `npm publish sub-dir`.

Fix semantic-release/semantic-release#570


Patch:
diff --git a/README.md b/README.md
index 9958b75..a91bb55 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,13 @@ Use either `NPM_TOKEN` for token authentication or `NPM_USERNAME`, `NPM_PASSWORD
 
 ### Options
 
-| Options      | Description                                                                                                            | Default |
-| ------------ | ---------------------------------------------------------------------------------------------------------------------- | ------- |
-| `npmPublish` | Whether to publish the `npm` package to the registry. If `false` the `package.json` version will still be updated.     | `true`  |
-| `tarballDir` | Directory path in which to generate the the package tarball. If `false` the tarball is not be kept on the file system. | `false` |
+| Options      | Description                                                                                                         | Default |
+|--------------|---------------------------------------------------------------------------------------------------------------------|---------|
+| `npmPublish` | Whether to publish the `npm` package to the registry. If `false` the `package.json` version will still be updated.  | `true`  |
+| `pkgRoot`    | Directory path to publish.                                                                                          | `.`     |
+| `tarballDir` | Directory path in which to write the the package tarball. If `false` the tarball is not be kept on the file system. | `false` |
+
+**Note**: The `pkgRoot` directory must contains a `package.json`. The version will be updated only in the `package.json` and `npm-shrinkwrap.json` within the `pkgRoot` directory.
 
 ### Npm configuration
 
@@ -74,7 +77,7 @@ Each individual plugin can be disabled, replaced or used with other plugins in t
 }
 ```
 
-The `npmPublish` and `tarballDir` option can be used to skip the publishing to the `npm` registry and instead, release the package tarball with another plugin. For example with the [github](https://github.com/semantic-release/github):
+The `npmPublish` and `tarballDir` option can be used to skip the publishing to the `npm` registry and instead, release the package tarball with another plugin. For example with the [github](https://github.com/semantic-release/github) plugin:
 
 ```json
 {
@@ -95,3 +98,27 @@ The `npmPublish` and `tarballDir` option can be used to skip the publishing to t
   }
 }
 ```
+
+When publishing from a sub-directory with the `pkgRoot` option, the `package.json` and `npm-shrinkwrap.json` updated with the new version can be moved to another directory with a `postpublish` [npm script](https://docs.npmjs.com/misc/scripts). For example with the [git](https://github.com/semantic-release/git) plugin:
+
+```json
+{
+  "release": {
+    "verifyConditions": ["@semantic-release/conditions-travis", "@semantic-release/npm", "@semantic-release/git"],
+    "getLastRelease": "@semantic-release/npm",
+    "publish": [
+      {
+        "path": "@semantic-release/npm",
+        "pkgRoot": "dist"
+      },
+      {
+        "path": "@semantic-release/git",
+        "assets": ["package.json", "npm-shrinkwrap.json"]
+      },
+    ]
+  },
+  "scripts": {
+    "postpublish": "cp -r dist/package.json . && cp -r dist/npm-shrinkwrap.json ."
+  }
+}
+```
diff --git a/index.js b/index.js
index 1e11811..dac2ee8 100644
--- a/index.js
+++ b/index.js
@@ -8,7 +8,7 @@ const getLastReleaseNpm = require('./lib/get-last-release');
 let verified;
 
 async function verifyConditions(pluginConfig, {options, logger}) {
-  // If the npm publish plugin is used and has `npmPublish` or `tarballDir` configured, validate them now in order to prevent any release if the configuration is wrong
+  // If the npm publish plugin is used and has `npmPublish`, `tarballDir` or `pkgRoot` configured, validate them now in order to prevent any release if the configuration is wrong
   if (options.publish) {
     const publishPlugin = castArray(options.publish).find(
       config => config.path && config.path === '@semantic-release/npm'
@@ -19,18 +19,29 @@ async function verifyConditions(pluginConfig, {options, logger}) {
     if (publishPlugin && publishPlugin.tarballDir) {
       pluginConfig.tarballDir = publishPlugin.tarballDir;
     }
+    if (publishPlugin && publishPlugin.pkgRoot) {
+      pluginConfig.pkgRoot = publishPlugin.pkgRoot;
+    }
   }
   setLegacyToken();
-  const pkg = await getPkg();
+  const pkg = await getPkg(pluginConfig.pkgRoot);
   await verifyNpm(pluginConfig, pkg, logger);
   verified = true;
 }
 
-async function getLastRelease(pluginConfig, {logger}) {
+async function getLastRelease(pluginConfig, {options, logger}) {
   setLegacyToken();
   // Reload package.json in case a previous external step updated it
-  const pkg = await getPkg();
+  const pkg = await getPkg(pluginConfig.pkgRoot);
   if (!verified) {
+    if (options.publish) {
+      const publishPlugin = castArray(options.publish).find(
+        config => config.path && config.path === '@semantic-release/npm'
+      );
+      if (publishPlugin && publishPlugin.pkgRoot) {
+        pluginConfig.pkgRoot = publishPlugin.pkgRoot;
+      }
+    }
     await verifyNpm(pluginConfig, pkg, logger);
     verified = true;
   }
@@ -40,7 +51,7 @@ async function getLastRelease(pluginConfig, {logger}) {
 async function publish(pluginConfig, {nextRelease: {version}, logger}) {
   setLegacyToken();
   // Reload package.json in case a previous external step updated it
-  const pkg = await getPkg();
+  const pkg = await getPkg(pluginConfig.pkgRoot);
   if (!verified) {
     await verifyNpm(pluginConfig, pkg, logger);
     verified = true;
diff --git a/lib/get-pkg.js b/lib/get-pkg.js
index dfb4ec8..e7a3a91 100644
--- a/lib/get-pkg.js
+++ b/lib/get-pkg.js
@@ -1,16 +1,18 @@
-const readPkgUp = require('read-pkg-up');
+const readPkg = require('read-pkg');
 const SemanticReleaseError = require('@semantic-release/error');
 
-module.exports = async () => {
-  const {pkg} = await readPkgUp();
+module.exports = async pkgRoot => {
+  try {
+    const pkg = await readPkg(pkgRoot);
+    if (!pkg.name) {
+      throw new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME');
+    }
 
-  if (!pkg) {
-    throw new SemanticReleaseError('A package.json file is required to release on npm.', 'ENOPKG');
+    return pkg;
+  } catch (err) {
+    if (err.code === 'ENOENT') {
+      throw new SemanticReleaseError('A package.json file is required to release on npm.', 'ENOPKG');
+    }
+    throw err;
   }
-
-  if (!pkg.name) {
-    throw new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME');
-  }
-
-  return pkg;
 };
diff --git a/lib/publish.js b/lib/publish.js
index 324b7cd..dc8d7d8 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -4,19 +4,20 @@ const execa = require('execa');
 const getRegistry = require('./get-registry');
 const updatePackageVersion = require('./update-package-version');
 
-module.exports = async ({npmPublish, tarballDir}, {publishConfig, name}, version, logger) => {
+module.exports = async ({npmPublish, tarballDir, pkgRoot}, {publishConfig, name}, version, logger) => {
+  const basePath = pkgRoot || '.';
   const registry = await getRegistry(publishConfig, name);
-  await updatePackageVersion(version, logger);
+  await updatePackageVersion(version, basePath, logger);
 
   if (tarballDir) {
     logger.log('Creating npm package version %s', version);
-    const tarball = await execa.stdout('npm', ['pack']);
+    const tarball = await execa.stdout('npm', ['pack', `./${basePath}`]);
     await move(tarball, path.join(tarballDir.trim(), tarball));
   }
 
   if (npmPublish !== false) {
     logger.log('Publishing version %s to npm registry', version);
-    const shell = await execa('npm', ['publish', '--registry', registry]);
+    const shell = await execa('npm', ['publish', `./${basePath}`, '--registry', registry]);
     process.stdout.write(shell.stdout);
   }
 };
diff --git a/lib/update-package-version.js b/lib/update-package-version.js
index 821909a..cc71361 100644
--- a/lib/update-package-version.js
+++ b/lib/update-package-version.js
@@ -1,15 +1,18 @@
+const path = require('path');
 const {readFile, writeFile, pathExists} = require('fs-extra');
 
-module.exports = async (version, logger) => {
-  const pkg = (await readFile('./package.json')).toString();
+module.exports = async (version, basePath, logger) => {
+  const packagePath = path.join(basePath, 'package.json');
+  const shrinkwrapPath = path.join(basePath, 'npm-shrinkwrap.json');
+  const pkg = (await readFile(packagePath)).toString();
 
-  await writeFile('./package.json', replaceVersion(pkg, version));
-  logger.log('Wrote version %s to package.json', version);
+  await writeFile(packagePath, replaceVersion(pkg, version));
+  logger.log('Wrote version %s to %s', version, packagePath);
 
-  if (await pathExists('./npm-shrinkwrap.json')) {
-    const shrinkwrap = (await readFile('./npm-shrinkwrap.json')).toString();
-    await writeFile('./npm-shrinkwrap.json', replaceVersion(shrinkwrap, version));
-    logger.log('Wrote version %s to npm-shrinkwrap.json', version);
+  if (await pathExists(shrinkwrapPath)) {
+    const shrinkwrap = (await readFile(shrinkwrapPath)).toString();
+    await writeFile(shrinkwrapPath, replaceVersion(shrinkwrap, version));
+    logger.log('Wrote version %s to %s', version, shrinkwrapPath);
   }
 };
 
diff --git a/lib/verify.js b/lib/verify.js
index 99f4d44..d585734 100644
--- a/lib/verify.js
+++ b/lib/verify.js
@@ -4,7 +4,7 @@ const SemanticReleaseError = require('@semantic-release/error');
 const getRegistry = require('./get-registry');
 const setNpmrcAuth = require('./set-npmrc-auth');
 
-module.exports = async ({npmPublish, tarballDir}, pkg, logger) => {
+module.exports = async ({npmPublish, tarballDir, pkgRoot}, pkg, logger) => {
   if (!isUndefined(npmPublish) && !isBoolean(npmPublish)) {
     throw new SemanticReleaseError('The "npmPublish" options, if defined, must be a Boolean.', 'EINVALIDNPMPUBLISH');
   }
@@ -13,6 +13,10 @@ module.exports = async ({npmPublish, tarballDir}, pkg, logger) => {
     throw new SemanticReleaseError('The "tarballDir" options, if defined, must be a String.', 'EINVALIDTARBALLDIR');
   }
 
+  if (!isUndefined(pkgRoot) && !isString(pkgRoot)) {
+    throw new SemanticReleaseError('The "pkgRoot" options, if defined, must be a String.', 'EINVALIDPKGROOT');
+  }
+
   const registry = await getRegistry(pkg.publishConfig, pkg.name);
   await setNpmrcAuth(registry, logger);
   try {
diff --git a/package.json b/package.json
index 85604e0..cd344c7 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
     "nerf-dart": "^1.0.0",
     "npm-conf": "^1.1.3",
     "npm-registry-client": "^8.5.0",
-    "read-pkg-up": "^3.0.0",
+    "read-pkg": "^3.0.0",
     "registry-auth-token": "^3.3.1"
   },
   "devDependencies": {


Test patch:
diff --git a/test/get-pkg.test.js b/test/get-pkg.test.js
index 03b0f2e..4e72cd9 100644
--- a/test/get-pkg.test.js
+++ b/test/get-pkg.test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import {writeJson, writeFile} from 'fs-extra';
+import {outputJson, writeFile} from 'fs-extra';
 import tempy from 'tempy';
 import getPkg from '../lib/get-pkg';
 
@@ -18,13 +18,22 @@ test.afterEach.always(() => {
 
 test.serial('Verify name and return parsed package.json', async t => {
   const pkg = {name: 'package', version: '0.0.0'};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
 
   const result = await getPkg();
   t.is(pkg.name, result.name);
   t.is(pkg.version, result.version);
 });
 
+test.serial('Verify name and return parsed package.json from a sub-directory', async t => {
+  const pkg = {name: 'package', version: '0.0.0'};
+  await outputJson('./dist/package.json', pkg);
+
+  const result = await getPkg('dist');
+  t.is(pkg.name, result.name);
+  t.is(pkg.version, result.version);
+});
+
 test.serial('Throw error if missing package.json', async t => {
   const error = await t.throws(getPkg());
 
@@ -33,7 +42,7 @@ test.serial('Throw error if missing package.json', async t => {
 });
 
 test.serial('Throw error if missing package name', async t => {
-  await writeJson('./package.json', {version: '0.0.0'});
+  await outputJson('./package.json', {version: '0.0.0'});
 
   const error = await t.throws(getPkg());
 
diff --git a/test/integration.test.js b/test/integration.test.js
index ccc3637..52ffa39 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import {writeJson, readJson, readFile, appendFile, pathExists} from 'fs-extra';
+import {outputJson, readJson, readFile, appendFile, pathExists} from 'fs-extra';
 import execa from 'execa';
 import {stub} from 'sinon';
 import tempy from 'tempy';
@@ -51,7 +51,7 @@ test.after.always(async () => {
 test.serial('Throws error if NPM token is invalid', async t => {
   process.env.NPM_TOKEN = 'wrong_token';
   const pkg = {name: 'published', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
   const error = await t.throws(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
 
   t.true(error instanceof SemanticReleaseError);
@@ -65,7 +65,7 @@ test.serial('Throws error if NPM token is invalid', async t => {
 test.serial('Verify npm auth and package', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'valid-token', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
   await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
 
   const npmrc = (await readFile('.npmrc')).toString();
@@ -73,11 +73,22 @@ test.serial('Verify npm auth and package', async t => {
   t.regex(npmrc, /email =/);
 });
 
+test.serial('Verify npm auth and package from a sub-directory', async t => {
+  Object.assign(process.env, npmRegistry.authEnv);
+  const pkg = {name: 'valid-token', publishConfig: {registry: npmRegistry.url}};
+  await outputJson('./dist/package.json', pkg);
+  await t.notThrows(t.context.m.verifyConditions({pkgRoot: 'dist'}, {options: {}, logger: t.context.logger}));
+
+  const npmrc = (await readFile('.npmrc')).toString();
+  t.regex(npmrc, /_auth =/);
+  t.regex(npmrc, /email =/);
+});
+
 test.serial('Verify npm auth and package with "npm_config_registry" env var set by yarn', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   process.env.npm_config_registry = 'https://registry.yarnpkg.com'; // eslint-disable-line camelcase
   const pkg = {name: 'valid-token', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
   await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
 
   const npmrc = (await readFile('.npmrc')).toString();
@@ -88,8 +99,8 @@ test.serial('Verify npm auth and package with "npm_config_registry" env var set
 test.serial('Return nothing if no version if published', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'not-published', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
-  const nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
+  await outputJson('./package.json', pkg);
+  const nextRelease = await t.context.m.getLastRelease({}, {options: {}, logger: t.context.logger});
 
   t.falsy(nextRelease);
 });
@@ -97,7 +108,7 @@ test.serial('Return nothing if no version if published', async t => {
 test.serial('Return last version published', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'published', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
 
   await appendFile(
     './.npmrc',
@@ -108,14 +119,18 @@ test.serial('Return last version published', async t => {
 
   await execa('npm', ['publish']);
 
-  const nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
+  const nextRelease = await t.context.m.getLastRelease({}, {options: {}, logger: t.context.logger});
   t.is(nextRelease.version, '1.0.0');
 });
 
 test.serial('Return last version published on a dist-tag', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
-  const pkg = {name: 'published-next', version: '1.0.0', publishConfig: {registry: npmRegistry.url, tag: 'next'}};
-  await writeJson('./package.json', pkg);
+  const pkg = {
+    name: 'published-next',
+    version: '1.0.0',
+    publishConfig: {options: {}, registry: npmRegistry.url, tag: 'next'},
+  };
+  await outputJson('./package.json', pkg);
 
   await appendFile(
     './.npmrc',
@@ -127,18 +142,18 @@ test.serial('Return last version published on a dist-tag', async t => {
   // Publish version 1.0.0 on latest and next
   await execa('npm', ['publish', '--tag=next']);
   pkg.version = '1.1.0';
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
   // Publish version 1.1.0 on next
   await execa('npm', ['publish', '--tag=next']);
 
-  const nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
+  const nextRelease = await t.context.m.getLastRelease({}, {options: {}, logger: t.context.logger});
   t.is(nextRelease.version, '1.1.0');
 });
 
 test.serial('Return nothing for an unpublished package', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'unpublished', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
 
   await appendFile(
     './.npmrc',
@@ -150,13 +165,34 @@ test.serial('Return nothing for an unpublished package', async t => {
   await execa('npm', ['publish']);
   await execa('npm', ['unpublish', 'unpublished', '--force']);
 
-  const nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
+  const nextRelease = await t.context.m.getLastRelease(
+    {},
+    {options: {publish: ['@semantic-release/npm']}, logger: t.context.logger}
+  );
   t.falsy(nextRelease);
 });
 
-test('Throw SemanticReleaseError if publish "npmPublish" option is not a Boolean', async t => {
+test('Throw SemanticReleaseError if publish "pkgRoot" option in getLastRelease is not a String', async t => {
+  const pkg = {name: 'invalid-pkgRoot', publishConfig: {registry: npmRegistry.url}};
+  await outputJson('./dist/package.json', pkg);
+  const pkgRoot = 42;
+  const error = await t.throws(
+    t.context.m.getLastRelease(
+      {},
+      {
+        options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', pkgRoot}]},
+        logger: t.context.logger,
+      }
+    )
+  );
+
+  t.is(error.name, 'SemanticReleaseError');
+  t.is(error.code, 'EINVALIDPKGROOT');
+});
+
+test('Throw SemanticReleaseError if publish "npmPublish" option in verifyConditions is not a Boolean', async t => {
   const pkg = {name: 'invalid-npmPublish', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
   const npmPublish = 42;
   const error = await t.throws(
     t.context.m.verifyConditions(
@@ -172,9 +208,9 @@ test('Throw SemanticReleaseError if publish "npmPublish" option is not a Boolean
   t.is(error.code, 'EINVALIDNPMPUBLISH');
 });
 
-test('Throw SemanticReleaseError if publish "tarballDir" option is not a String', async t => {
+test('Throw SemanticReleaseError if publish "tarballDir" option in verifyConditions is not a String', async t => {
   const pkg = {name: 'invalid-tarballDir', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
   const tarballDir = 42;
   const error = await t.throws(
     t.context.m.verifyConditions(
@@ -190,10 +226,28 @@ test('Throw SemanticReleaseError if publish "tarballDir" option is not a String'
   t.is(error.code, 'EINVALIDTARBALLDIR');
 });
 
+test('Throw SemanticReleaseError if publish "pkgRoot" option in verifyConditions is not a String', async t => {
+  const pkg = {name: 'invalid-pkgRoot', publishConfig: {registry: npmRegistry.url}};
+  await outputJson('./dist/package.json', pkg);
+  const pkgRoot = 42;
+  const error = await t.throws(
+    t.context.m.verifyConditions(
+      {},
+      {
+        options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', pkgRoot}]},
+        logger: t.context.logger,
+      }
+    )
+  );
+
+  t.is(error.name, 'SemanticReleaseError');
+  t.is(error.code, 'EINVALIDPKGROOT');
+});
+
 test.serial('Publish the package', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'publish', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
 
   await t.context.m.publish({}, {logger: t.context.logger, nextRelease: {version: '1.0.0'}});
 
@@ -202,33 +256,60 @@ test.serial('Publish the package', async t => {
   t.is((await execa('npm', ['view', pkg.name, 'version'])).stdout, '1.0.0');
 });
 
+test.serial('Publish the package from a sub-directory', async t => {
+  Object.assign(process.env, npmRegistry.authEnv);
+  const pkg = {name: 'publish-sub-dir', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
+  await outputJson('./dist/package.json', pkg);
+
+  await t.context.m.publish({pkgRoot: 'dist'}, {logger: t.context.logger, nextRelease: {version: '1.0.0'}});
+
+  t.is((await readJson('./dist/package.json')).version, '1.0.0');
+  t.false(await pathExists(`./${pkg.name}-1.0.0.tgz`));
+  t.is((await execa('npm', ['view', pkg.name, 'version'])).stdout, '1.0.0');
+});
+
 test.serial('Create the package and skip publish', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'skip-publish', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
 
   await t.context.m.publish(
-    {npmPublish: false, tarballDir: 'dist'},
+    {npmPublish: false, tarballDir: 'tarball'},
     {logger: t.context.logger, nextRelease: {version: '1.0.0'}}
   );
 
   t.is((await readJson('./package.json')).version, '1.0.0');
-  t.true(await pathExists(`./dist/${pkg.name}-1.0.0.tgz`));
+  t.true(await pathExists(`./tarball/${pkg.name}-1.0.0.tgz`));
+  await t.throws(execa('npm', ['view', pkg.name, 'version']));
+});
+
+test.serial('Create the package and skip publish from a sub-directory', async t => {
+  Object.assign(process.env, npmRegistry.authEnv);
+  const pkg = {name: 'skip-publish-sub-dir', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
+  await outputJson('./dist/package.json', pkg);
+
+  await t.context.m.publish(
+    {npmPublish: false, tarballDir: './tarball', pkgRoot: './dist'},
+    {logger: t.context.logger, nextRelease: {version: '1.0.0'}}
+  );
+
+  t.is((await readJson('./dist/package.json')).version, '1.0.0');
+  t.true(await pathExists(`./tarball/${pkg.name}-1.0.0.tgz`));
   await t.throws(execa('npm', ['view', pkg.name, 'version']));
 });
 
 test.serial('Verify token and set up auth only on the fist call', async t => {
   Object.assign(process.env, npmRegistry.authEnv);
   const pkg = {name: 'test-module', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
-  await writeJson('./package.json', pkg);
+  await outputJson('./package.json', pkg);
 
   await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
 
-  let nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
+  let nextRelease = await t.context.m.getLastRelease({}, {options: {}, logger: t.context.logger});
   t.falsy(nextRelease);
 
   await t.context.m.publish({}, {logger: t.context.logger, nextRelease: {version: '1.0.0'}});
 
-  nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
+  nextRelease = await t.context.m.getLastRelease({}, {options: {}, logger: t.context.logger});
   t.is(nextRelease.version, '1.0.0');
 });
diff --git a/test/update-package-version.test.js b/test/update-package-version.test.js
index 4f67cc7..ac167b0 100644
--- a/test/update-package-version.test.js
+++ b/test/update-package-version.test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import {writeFile, readFile} from 'fs-extra';
+import {outputFile, readFile} from 'fs-extra';
 import tempy from 'tempy';
 import execa from 'execa';
 import {stub} from 'sinon';
@@ -34,15 +34,15 @@ test.serial('Updade package.json', async t => {
 `;
 
   // Create package.json in repository root
-  await writeFile('./package.json', pkg);
+  await outputFile('./package.json', pkg);
 
-  await updatePackageVersion('1.0.0', t.context.logger);
+  await updatePackageVersion('1.0.0', '.', t.context.logger);
 
   // Verify package.json has been updated
   t.is((await readFile('./package.json')).toString(), pkg.replace('0.0.0-dev', '1.0.0'));
 
   // Verify the logger has been called with the version updated
-  t.deepEqual(t.context.log.args[0], ['Wrote version %s to package.json', '1.0.0']);
+  t.deepEqual(t.context.log.args[0], ['Wrote version %s to %s', '1.0.0', 'package.json']);
 });
 
 test.serial('Updade package.json and npm-shrinkwrap.json', async t => {
@@ -54,17 +54,40 @@ test.serial('Updade package.json and npm-shrinkwrap.json', async t => {
     "version":      "0.0.0-dev"
   }`;
   // Create package.json in repository root
-  await writeFile('./package.json', pkg);
+  await outputFile('./package.json', pkg);
   // Create a npm-shrinkwrap.json file
   await execa('npm', ['shrinkwrap']);
   const shrinkwrap = (await readFile('./npm-shrinkwrap.json')).toString();
 
-  await updatePackageVersion('1.0.0', t.context.logger);
+  await updatePackageVersion('1.0.0', '.', t.context.logger);
 
   // Verify package.json and npm-shrinkwrap.json have been updated
   t.is((await readFile('./package.json')).toString(), pkg.replace('0.0.0-dev', '1.0.0'));
   t.is((await readFile('./npm-shrinkwrap.json')).toString(), shrinkwrap.replace('0.0.0-dev', '1.0.0'));
   // Verify the logger has been called with the version updated
-  t.deepEqual(t.context.log.args[0], ['Wrote version %s to package.json', '1.0.0']);
-  t.deepEqual(t.context.log.args[1], ['Wrote version %s to npm-shrinkwrap.json', '1.0.0']);
+  t.deepEqual(t.context.log.args[0], ['Wrote version %s to %s', '1.0.0', 'package.json']);
+  t.deepEqual(t.context.log.args[1], ['Wrote version %s to %s', '1.0.0', 'npm-shrinkwrap.json']);
+});
+
+test.serial('Updade package.json and npm-shrinkwrap.json in a sub-directory', async t => {
+  const pkg = `{
+    "name": "test","description":"pacakage description","version":"0.0.0-dev"
+  }`;
+
+  // Create package.json in repository root
+  await outputFile('./dist/package.json', pkg);
+  process.chdir('dist');
+  // Create a npm-shrinkwrap.json file
+  await execa('npm', ['shrinkwrap']);
+  process.chdir('..');
+  const shrinkwrap = (await readFile('./dist/npm-shrinkwrap.json')).toString();
+
+  await updatePackageVersion('1.0.0', 'dist', t.context.logger);
+
+  // Verify package.json and npm-shrinkwrap.json have been updated
+  t.is((await readFile('./dist/package.json')).toString(), pkg.replace('0.0.0-dev', '1.0.0'));
+  t.is((await readFile('./dist/npm-shrinkwrap.json')).toString(), shrinkwrap.replace('0.0.0-dev', '1.0.0'));
+  // Verify the logger has been called with the version updated
+  t.deepEqual(t.context.log.args[0], ['Wrote version %s to %s', '1.0.0', 'dist/package.json']);
+  t.deepEqual(t.context.log.args[1], ['Wrote version %s to %s', '1.0.0', 'dist/npm-shrinkwrap.json']);
 });
diff --git a/test/verify.test.js b/test/verify.test.js
index ad4d91f..faa03ee 100644
--- a/test/verify.test.js
+++ b/test/verify.test.js
@@ -23,3 +23,11 @@ test('Throw SemanticReleaseError if "tarballDir" option is not a String', async
   t.is(error.name, 'SemanticReleaseError');
   t.is(error.code, 'EINVALIDTARBALLDIR');
 });
+
+test('Throw SemanticReleaseError if "pkgRoot" option is not a String', async t => {
+  const pkgRoot = 42;
+  const error = await t.throws(verify({pkgRoot}, {}, t.context.logger));
+
+  t.is(error.name, 'SemanticReleaseError');
+  t.is(error.code, 'EINVALIDPKGROOT');
+});