Skip to content

Commit

Permalink
fix: Pass registry URL to npm CLI with --registry
Browse files Browse the repository at this point in the history
The environment variable `npm_config_registry` takes precedence to the registry configured in `.npmrc` when running `npm` commands.
Yarn set 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.
  • Loading branch information
pvdlg authored and gr2m committed Nov 30, 2017
1 parent 0f654b1 commit 187b823
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 50 deletions.
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};
6 changes: 4 additions & 2 deletions lib/publish.js
Original file line number Diff line number Diff line change
@@ -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);
};
4 changes: 1 addition & 3 deletions lib/set-npmrc-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 4 additions & 2 deletions lib/verify.js
Original file line number Diff line number Diff line change
@@ -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');
}
Expand Down
44 changes: 44 additions & 0 deletions test/get-registry.test.js
Original file line number Diff line number Diff line change
@@ -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/');
});
19 changes: 14 additions & 5 deletions test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,24 @@ 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
process.chdir(t.context.cwd);
});

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);
Expand All @@ -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}};
Expand Down
51 changes: 14 additions & 37 deletions test/set-npmrc-auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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\}/);
Expand All @@ -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(
Expand All @@ -72,63 +56,56 @@ 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');
});

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');
});

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');
});

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');
});

0 comments on commit 187b823

Please sign in to comment.