Skip to content

Commit

Permalink
feat: Add npmPublish and tarballDir options
Browse files Browse the repository at this point in the history
`npmPublish` can be set to `false` to skip the publishing on npm registry
If `tarballDir` is set the npm tarball (`npm pack`) will be generated in the configured directory
  • Loading branch information
pvdlg committed Dec 2, 2017
1 parent 5fb0b09 commit d04a9b0
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 22 deletions.
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ Determine the last release of the package on the `npm` registry.

## publish

Publish the package on the `npm` registry.
Update the `package.json` version, [create](https://docs.npmjs.com/cli/pack) the `npm` package tarball and [publish](https://docs.npmjs.com/cli/publish) to the `npm` registry.

## Configuration

### Environment variables
### Npm registry authentication

The `npm` authentication configuration is **required** and can be set via environment variables.
The `npm` authentication configuration is **required** and can be set via [environment variables](#environment-variables).

Both the [token](https://docs.npmjs.com/getting-started/working_with_tokens) and the legacy (`username`, `password` and `email`) authentication are supported. It is recommended to use the [token](https://docs.npmjs.com/getting-started/working_with_tokens) authentication. The legacy authentication is supported as the alternative npm registries [Artifactory](https://www.jfrog.com/open-source/#os-arti) and [npm-registry-couchapp](https://github.com/npm/npm-registry-couchapp) only supports that form of authentication at this point.

| Variable | Description
### Environment variables

| Variable | Description |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `NPM_TOKEN` | Npm token created via [npm token create](https://docs.npmjs.com/getting-started/working_with_tokens#how-to-create-new-tokens) |
| `NPM_USERNAME` | Npm username created via [npm adduser](https://docs.npmjs.com/cli/adduser) or on [npmjs.com](https://www.npmjs.com) |
Expand All @@ -37,6 +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` |

### Npm configuration

The plugins are based on `npm` and will use the configuration from [`.npmrc`](https://docs.npmjs.com/files/npmrc). See [npm config](https://docs.npmjs.com/misc/config) for the option list.

The [`registry`](https://docs.npmjs.com/misc/registry) and [`dist-tag`](https://docs.npmjs.com/cli/dist-tag) can be configured in the `package.json` and will take precedence over the configuration in `.npmrc`:
Expand All @@ -54,6 +63,7 @@ The [`registry`](https://docs.npmjs.com/misc/registry) and [`dist-tag`](https://
The plugins are used by default by [semantic-release](https://github.com/semantic-release/semantic-release) so no specific configuration is requiered to use them.

Each individual plugin can be disabled, replaced or used with other plugins in the `package.json`:

```json
{
"release": {
Expand All @@ -63,3 +73,25 @@ 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):

```json
{
"release": {
"verifyConditions": ["@semantic-release/conditions-travis", "@semantic-release/npm", "@semantic-release/git", "@semantic-release/github"],
"getLastRelease": "@semantic-release/git",
"publish": [
{
"path": "@semantic-release/npm",
"npmPublish": false,
"tarballDir": "dist"
},
{
"path": "@semantic-release/github",
"assets": "dist/*.tgz"
},
]
}
}
```
23 changes: 18 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const {castArray} = require('lodash');
const setLegacyToken = require('./lib/set-legacy-token');
const getPkg = require('./lib/get-pkg');
const verifyNpm = require('./lib/verify');
Expand All @@ -6,10 +7,22 @@ const getLastReleaseNpm = require('./lib/get-last-release');

let verified;

async function verifyConditions(pluginConfig, {logger}) {
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 (options.publish) {
const publishPlugin = castArray(options.publish).find(
config => config.path && config.path === '@semantic-release/npm'
);
if (publishPlugin && publishPlugin.npmPublish) {
pluginConfig.npmPublish = publishPlugin.npmPublish;
}
if (publishPlugin && publishPlugin.tarballDir) {
pluginConfig.tarballDir = publishPlugin.tarballDir;
}
}
setLegacyToken();
const pkg = await getPkg();
await verifyNpm(pkg, logger);
await verifyNpm(pluginConfig, pkg, logger);
verified = true;
}

Expand All @@ -18,7 +31,7 @@ async function getLastRelease(pluginConfig, {logger}) {
// Reload package.json in case a previous external step updated it
const pkg = await getPkg();
if (!verified) {
await verifyNpm(pkg, logger);
await verifyNpm(pluginConfig, pkg, logger);
verified = true;
}
return getLastReleaseNpm(pkg, logger);
Expand All @@ -29,10 +42,10 @@ async function publish(pluginConfig, {nextRelease: {version}, logger}) {
// Reload package.json in case a previous external step updated it
const pkg = await getPkg();
if (!verified) {
await verifyNpm(pkg, logger);
await verifyNpm(pluginConfig, pkg, logger);
verified = true;
}
await publishNpm(pkg, version, logger);
await publishNpm(pluginConfig, pkg, version, logger);
}

module.exports = {verifyConditions, getLastRelease, publish};
18 changes: 14 additions & 4 deletions lib/publish.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
const path = require('path');
const {move} = require('fs-extra');
const execa = require('execa');
const getRegistry = require('./get-registry');
const updatePackageVersion = require('./update-package-version');

module.exports = async ({publishConfig, name}, version, logger) => {
module.exports = async ({npmPublish, tarballDir}, {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', '--registry', registry]);
process.stdout.write(shell.stdout);
if (tarballDir) {
logger.log('Creating npm package version %s', version);
const tarball = await execa.stdout('npm', ['pack']);
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]);
process.stdout.write(shell.stdout);
}
};
11 changes: 10 additions & 1 deletion lib/verify.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
const {isString, isUndefined, isBoolean} = require('lodash');
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) => {
module.exports = async ({npmPublish, tarballDir}, pkg, logger) => {
if (!isUndefined(npmPublish) && !isBoolean(npmPublish)) {
throw new SemanticReleaseError('The "npmPublish" options, if defined, must be a Boolean.', 'EINVALIDNPMPUBLISH');
}

if (!isUndefined(tarballDir) && !isString(tarballDir)) {
throw new SemanticReleaseError('The "tarballDir" options, if defined, must be a String.', 'EINVALIDTARBALLDIR');
}

const registry = await getRegistry(pkg.publishConfig, pkg.name);
await setNpmrcAuth(registry, logger);
try {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@semantic-release/error": "^2.1.0",
"execa": "^0.8.0",
"fs-extra": "^4.0.2",
"lodash": "^4.17.4",
"nerf-dart": "^1.0.0",
"npm-conf": "^1.1.3",
"npm-registry-client": "^8.5.0",
Expand Down
69 changes: 61 additions & 8 deletions test/integration.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {writeJson, readFile, appendFile} from 'fs-extra';
import test from 'ava';
import {writeJson, readJson, readFile, appendFile, pathExists} from 'fs-extra';
import execa from 'execa';
import {stub} from 'sinon';
import tempy from 'tempy';
Expand Down Expand Up @@ -57,7 +57,7 @@ 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);
const error = await t.throws(t.context.m.verifyConditions({}, {logger: t.context.logger}));
const error = await t.throws(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));

t.true(error instanceof SemanticReleaseError);
t.is(error.code, 'EINVALIDNPMTOKEN');
Expand All @@ -71,7 +71,7 @@ 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 t.notThrows(t.context.m.verifyConditions({}, {logger: t.context.logger}));
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));

const npmrc = (await readFile('.npmrc')).toString();
t.regex(npmrc, /_auth =/);
Expand All @@ -83,7 +83,7 @@ test.serial('Verify npm auth and package with "npm_config_registry" env var set
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}));
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));

const npmrc = (await readFile('.npmrc')).toString();
t.regex(npmrc, /_auth =/);
Expand Down Expand Up @@ -159,22 +159,75 @@ test.serial('Return nothing for an unpublished package', async t => {
t.falsy(nextRelease);
});

test.serial('Publish a package', async t => {
test('Throw SemanticReleaseError if publish "npmPublish" option is not a Boolean', async t => {
const pkg = {name: 'invalid-npmPublish', publishConfig: {registry: npmRegistry.url}};
await writeJson('./package.json', pkg);
const npmPublish = 42;
const error = await t.throws(
t.context.m.verifyConditions(
{},
{
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', npmPublish}]},
logger: t.context.logger,
}
)
);

t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EINVALIDNPMPUBLISH');
});

test('Throw SemanticReleaseError if publish "tarballDir" option is not a String', async t => {
const pkg = {name: 'invalid-tarballDir', publishConfig: {registry: npmRegistry.url}};
await writeJson('./package.json', pkg);
const tarballDir = 42;
const error = await t.throws(
t.context.m.verifyConditions(
{},
{
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', tarballDir}]},
logger: t.context.logger,
}
)
);

t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EINVALIDTARBALLDIR');
});

test.serial('Publish the package', async t => {
Object.assign(process.env, npmRegistry.authEnv);
const pkg = {name: 'publish', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
const pkg = {name: 'publish', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
await writeJson('./package.json', pkg);

await t.context.m.publish({}, {logger: t.context.logger, nextRelease: {version: '1.0.0'}});

t.is((await execa('npm', ['view', 'publish', 'version'])).stdout, '1.0.0');
t.is((await readJson('./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 t.context.m.publish(
{npmPublish: false, tarballDir: 'dist'},
{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`));
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 t.notThrows(t.context.m.verifyConditions({}, {logger: t.context.logger}));
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));

let nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
t.falsy(nextRelease);
Expand Down
25 changes: 25 additions & 0 deletions test/verify.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import test from 'ava';
import {stub} from 'sinon';
import verify from '../lib/verify';

test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});

test('Throw SemanticReleaseError if "npmPublish" option is not a Boolean', async t => {
const npmPublish = 42;
const error = await t.throws(verify({npmPublish}, {}, t.context.logger));

t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EINVALIDNPMPUBLISH');
});

test('Throw SemanticReleaseError if "tarballDir" option is not a String', async t => {
const tarballDir = 42;
const error = await t.throws(verify({tarballDir}, {}, t.context.logger));

t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EINVALIDTARBALLDIR');
});

0 comments on commit d04a9b0

Please sign in to comment.