Repository: semantic-release/npm
Pull request #9 (2017-11-30 05:05:14)

Fix EINVALIDNPMTOKEN error with Yarn
Pass registry URL to `npm` CLI with `--registry`.

The environment variable `npm_config_registry` takes precedence to the registry configured in `.npmrc` when running `npm` commands.
Yarn sets this variable to `https://registry.yarnpkg.com` creating an `EINVALIDNPMTOKEN` error when running `semantic-release` with Yarn.

Passing `--registry` with the value retrieved from `.npmrc` or `package.json` override the `npm_config_registry` set by Yarn.

Fix semantic-release/semantic-release#533


Patch:
diff --git a/index.js b/index.js
index 10d44f3..862d6b1 100644
--- a/index.js
+++ b/index.js
@@ -28,7 +28,7 @@ async function publish(pluginConfig, {nextRelease: {version}, logger}) {
     await verifyNpm(pkg, logger);
     verified = true;
   }
-  await publishNpm(version, logger);
+  await publishNpm(pkg, version, logger);
 }
 
 module.exports = {verifyConditions, getLastRelease, publish};
diff --git a/lib/publish.js b/lib/publish.js
index d2db3e2..25f4775 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -1,10 +1,12 @@
 const execa = require('execa');
+const getRegistry = require('./get-registry');
 const updatePackageVersion = require('./update-package-version');
 
-module.exports = async (version, logger) => {
+module.exports = async ({publishConfig, name}, version, logger) => {
+  const registry = await getRegistry(publishConfig, name);
   await updatePackageVersion(version, logger);
 
   logger.log('Publishing version %s to npm registry', version);
-  const shell = await execa('npm', ['publish']);
+  const shell = await execa('npm', ['publish', '--registry', registry]);
   process.stdout.write(shell.stdout);
 };
diff --git a/lib/set-npmrc-auth.js b/lib/set-npmrc-auth.js
index 41c88b8..902016a 100644
--- a/lib/set-npmrc-auth.js
+++ b/lib/set-npmrc-auth.js
@@ -2,10 +2,8 @@ const {appendFile} = require('fs-extra');
 const getAuthToken = require('registry-auth-token');
 const nerfDart = require('nerf-dart');
 const SemanticReleaseError = require('@semantic-release/error');
-const getRegistry = require('./get-registry');
 
-module.exports = async ({publishConfig, name}, logger) => {
-  const registry = await getRegistry(publishConfig, name);
+module.exports = async (registry, logger) => {
   logger.log('Verify authentication for registry %s', registry);
   const {NPM_TOKEN, NPM_USERNAME, NPM_PASSWORD, NPM_EMAIL} = process.env;
 
diff --git a/lib/verify.js b/lib/verify.js
index 9e3e812..bbc2291 100644
--- a/lib/verify.js
+++ b/lib/verify.js
@@ -1,11 +1,13 @@
 const execa = require('execa');
 const SemanticReleaseError = require('@semantic-release/error');
+const getRegistry = require('./get-registry');
 const setNpmrcAuth = require('./set-npmrc-auth');
 
 module.exports = async (pkg, logger) => {
-  await setNpmrcAuth(pkg, logger);
+  const registry = await getRegistry(pkg.publishConfig, pkg.name);
+  await setNpmrcAuth(registry, logger);
   try {
-    await execa('npm', ['whoami']);
+    await execa('npm', ['whoami', '--registry', registry]);
   } catch (err) {
     throw new SemanticReleaseError('Invalid npm token.', 'EINVALIDNPMTOKEN');
   }


Test patch:
diff --git a/test/get-registry.test.js b/test/get-registry.test.js
new file mode 100644
index 0000000..fbe897d
--- /dev/null
+++ b/test/get-registry.test.js
@@ -0,0 +1,44 @@
+import test from 'ava';
+import {appendFile} from 'fs-extra';
+import tempy from 'tempy';
+import getRegistry from '../lib/get-registry';
+
+test.beforeEach(t => {
+  // Save the current process.env
+  t.context.env = Object.assign({}, process.env);
+  // Save the current working diretory
+  t.context.cwd = process.cwd();
+  // Change current working directory to a temp directory
+  process.chdir(tempy.directory());
+});
+
+test.afterEach.always(t => {
+  // Restore the current working directory
+  process.chdir(t.context.cwd);
+});
+
+test.serial('Get default registry', async t => {
+  const registry = await getRegistry({}, 'package-name');
+
+  t.is(registry, 'https://registry.npmjs.org/');
+});
+
+test.serial('Get the registry configured in ".npmrc" and normalize trailing slash', async t => {
+  await appendFile('./.npmrc', 'registry = https://custom1.registry.com');
+  const registry = await getRegistry({}, 'package-name');
+
+  t.is(registry, 'https://custom1.registry.com/');
+});
+
+test.serial('Get the registry configured from "publishConfig"', async t => {
+  const registry = await getRegistry({registry: 'https://custom2.registry.com/'}, 'package-name');
+
+  t.is(registry, 'https://custom2.registry.com/');
+});
+
+test.serial('Get the registry configured in ".npmrc" for scoped package', async t => {
+  await appendFile('./.npmrc', '@scope:registry = https://custom3.registry.com');
+  const registry = await getRegistry({}, '@scope/package-name');
+
+  t.is(registry, 'https://custom3.registry.com/');
+});
diff --git a/test/integration.test.js b/test/integration.test.js
index f51cc82..d219c8d 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -39,8 +39,6 @@ test.beforeEach(t => {
 });
 
 test.afterEach.always(t => {
-  // Clear `rc` from the npm cache as it cache the relative path of .npmrc files, preventing to load a new file after changing current working directory
-  clearModule('rc');
   // Restore process.env
   process.env = Object.assign({}, t.context.env);
   // Restore the current working directory
@@ -48,18 +46,17 @@ test.afterEach.always(t => {
 });
 
 test.after.always(async () => {
-  // Stop the local NPM registry
-  await npmRegistry.stop();
   // Restore stdout and stderr
   processStderr.restore();
   processStdout.restore();
+  // Stop the local NPM registry
+  await npmRegistry.stop();
 });
 
 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 appendFile('./.npmrc', `\nregistry = ${npmRegistry.url}`);
   const error = await t.throws(t.context.m.verifyConditions({}, {logger: t.context.logger}));
 
   t.true(error instanceof SemanticReleaseError);
@@ -81,6 +78,18 @@ test.serial('Verify npm auth and package', async t => {
   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 t.notThrows(t.context.m.verifyConditions({}, {logger: t.context.logger}));
+
+  const npmrc = (await readFile('.npmrc')).toString();
+  t.regex(npmrc, /_auth =/);
+  t.regex(npmrc, /email =/);
+});
+
 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}};
diff --git a/test/set-npmrc-auth.test.js b/test/set-npmrc-auth.test.js
index 1bbcf1e..5dd4fd0 100644
--- a/test/set-npmrc-auth.test.js
+++ b/test/set-npmrc-auth.test.js
@@ -2,8 +2,6 @@ import {readFile, appendFile} from 'fs-extra';
 import test from 'ava';
 import {stub} from 'sinon';
 import tempy from 'tempy';
-import clearModule from 'clear-module';
-import SemanticReleaseError from '@semantic-release/error';
 import setNpmrcAuth from '../lib/set-npmrc-auth';
 
 test.beforeEach(t => {
@@ -18,35 +16,21 @@ test.beforeEach(t => {
   t.context.cwd = process.cwd();
   // Change current working directory to a temp directory
   process.chdir(tempy.directory());
-  // Prevent to use `.npmrc` from the home directory in case there is a valid token set there
-  process.env.HOME = process.cwd();
-  process.env.USERPROFILE = process.cwd();
   // Stub the logger
   t.context.log = stub();
   t.context.logger = {log: t.context.log};
 });
 
 test.afterEach.always(t => {
-  // Clear `rc` from the npm cache as it cache the relative path of .npmrc files, preventing to load a new file after changing current working directory. See https://github.com/dominictarr/rc/issues/101
-  clearModule('rc');
   // Restore process.env
   process.env = Object.assign({}, t.context.env);
   // Restore the current working directory
   process.chdir(t.context.cwd);
 });
 
-test.serial('Set auth with "NPM_TOKEN" and default registry', async t => {
+test.serial('Set auth with "NPM_TOKEN"', async t => {
   process.env.NPM_TOKEN = 'npm_token';
-  await setNpmrcAuth({name: 'package-name'}, t.context.logger);
-
-  const npmrc = (await readFile('.npmrc')).toString();
-  t.regex(npmrc, /\/\/registry.npmjs.org\/:_authToken = \$\{NPM_TOKEN\}/);
-  t.true(t.context.log.calledWith('Wrote NPM_TOKEN to .npmrc.'));
-});
-
-test.serial('Set auth with "NPM_TOKEN" and custom registry', async t => {
-  process.env.NPM_TOKEN = 'npm_token';
-  await setNpmrcAuth({name: 'package-name', publishConfig: {registry: 'http://custom.registry.com'}}, t.context.logger);
+  await setNpmrcAuth('http://custom.registry.com', t.context.logger);
 
   const npmrc = (await readFile('.npmrc')).toString();
   t.regex(npmrc, /\/\/custom.registry.com\/:_authToken = \$\{NPM_TOKEN\}/);
@@ -58,7 +42,7 @@ test.serial('Set auth with "NPM_USERNAME", "NPM_PASSWORD" and "NPM_EMAIL"', asyn
   process.env.NPM_PASSWORD = 'npm_pasword';
   process.env.NPM_EMAIL = 'npm_email';
 
-  await setNpmrcAuth({name: 'package-name'}, t.context.logger);
+  await setNpmrcAuth('http://custom.registry.com', t.context.logger);
 
   const npmrc = (await readFile('.npmrc')).toString();
   t.regex(
@@ -72,33 +56,26 @@ test.serial('Set auth with "NPM_USERNAME", "NPM_PASSWORD" and "NPM_EMAIL"', asyn
 });
 
 test.serial('Do not modify ".npmrc" if auth is already configured', async t => {
-  await appendFile('./.npmrc', `//registry.npmjs.org/:_authToken = \${NPM_TOKEN}`);
-  await setNpmrcAuth({name: 'package-name'}, t.context.logger);
-
-  t.true(t.context.log.calledOnce);
-});
-
-test.serial('Do not modify ".npmrc" if auth is already configured with custom registry', async t => {
   await appendFile('./.npmrc', `//custom.registry.com/:_authToken = \${NPM_TOKEN}`);
-  await setNpmrcAuth({name: 'package-name', publishConfig: {registry: 'http://custom.registry.com'}}, t.context.logger);
+  await setNpmrcAuth('http://custom.registry.com', t.context.logger);
 
   t.true(t.context.log.calledOnce);
 });
 
-test.serial('Do not modify ".npmrc" is auth is already configured for a scoped package', async t => {
+test.serial('Do not modify ".npmrc" if auth is already configured for a scoped package', async t => {
   await appendFile(
     './.npmrc',
     `@scope:registry=http://custom.registry.com\n//custom.registry.com/:_authToken = \${NPM_TOKEN}`
   );
-  await setNpmrcAuth({name: '@scope/package-name'}, t.context.logger);
+  await setNpmrcAuth('http://custom.registry.com', t.context.logger);
 
   t.true(t.context.log.calledOnce);
 });
 
 test.serial('Throw error if "NPM_TOKEN" is missing', async t => {
-  const error = await t.throws(setNpmrcAuth({name: 'package-name'}, t.context.logger));
+  const error = await t.throws(setNpmrcAuth('http://custom.registry.com', t.context.logger));
 
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   t.is(error.message, 'No npm token specified.');
   t.is(error.code, 'ENONPMTOKEN');
 });
@@ -106,9 +83,9 @@ test.serial('Throw error if "NPM_TOKEN" is missing', async t => {
 test.serial('Throw error if "NPM_USERNAME" is missing', async t => {
   process.env.NPM_PASSWORD = 'npm_pasword';
   process.env.NPM_EMAIL = 'npm_email';
-  const error = await t.throws(setNpmrcAuth({name: 'package-name'}, t.context.logger));
+  const error = await t.throws(setNpmrcAuth('http://custom.registry.com', t.context.logger));
 
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   t.is(error.message, 'No npm token specified.');
   t.is(error.code, 'ENONPMTOKEN');
 });
@@ -116,9 +93,9 @@ test.serial('Throw error if "NPM_USERNAME" is missing', async t => {
 test.serial('Throw error if "NPM_PASSWORD" is missing', async t => {
   process.env.NPM_USERNAME = 'npm_username';
   process.env.NPM_EMAIL = 'npm_email';
-  const error = await t.throws(setNpmrcAuth({name: 'package-name'}, t.context.logger));
+  const error = await t.throws(setNpmrcAuth('http://custom.registry.com', t.context.logger));
 
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   t.is(error.message, 'No npm token specified.');
   t.is(error.code, 'ENONPMTOKEN');
 });
@@ -126,9 +103,9 @@ test.serial('Throw error if "NPM_PASSWORD" is missing', async t => {
 test.serial('Throw error if "NPM_EMAIL" is missing', async t => {
   process.env.NPM_USERNAME = 'npm_username';
   process.env.NPM_PASSWORD = 'npm_password';
-  const error = await t.throws(setNpmrcAuth({name: 'package-name'}, t.context.logger));
+  const error = await t.throws(setNpmrcAuth('http://custom.registry.com', t.context.logger));
 
-  t.true(error instanceof SemanticReleaseError);
+  t.is(error.name, 'SemanticReleaseError');
   t.is(error.message, 'No npm token specified.');
   t.is(error.code, 'ENONPMTOKEN');
 });