From d04a9b0e2d8c7ebc9ee9d6824ed7c3c2e2b7ecb7 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Sat, 2 Dec 2017 04:05:37 -0500 Subject: [PATCH] feat: Add `npmPublish` and `tarballDir` options `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 --- README.md | 40 ++++++++++++++++++++--- index.js | 23 +++++++++++--- lib/publish.js | 18 ++++++++--- lib/verify.js | 11 ++++++- package.json | 1 + test/integration.test.js | 69 +++++++++++++++++++++++++++++++++++----- test/verify.test.js | 25 +++++++++++++++ 7 files changed, 165 insertions(+), 22 deletions(-) create mode 100644 test/verify.test.js diff --git a/README.md b/README.md index 5c311b29..9958b75a 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -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`: @@ -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": { @@ -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" + }, + ] + } +} +``` diff --git a/index.js b/index.js index 09ac6de7..1e118115 100644 --- a/index.js +++ b/index.js @@ -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'); @@ -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; } @@ -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); @@ -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}; diff --git a/lib/publish.js b/lib/publish.js index 25f4775b..324b7cdb 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -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); + } }; diff --git a/lib/verify.js b/lib/verify.js index bbc22918..99f4d441 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -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 { diff --git a/package.json b/package.json index 9c5ca2e0..499cfec2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/integration.test.js b/test/integration.test.js index 9e8c2117..5e628666 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -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'; @@ -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'); @@ -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 =/); @@ -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 =/); @@ -159,14 +159,67 @@ 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 => { @@ -174,7 +227,7 @@ test.serial('Verify token and set up auth only on the fist call', async t => { 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); diff --git a/test/verify.test.js b/test/verify.test.js new file mode 100644 index 00000000..ad4d91f5 --- /dev/null +++ b/test/verify.test.js @@ -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'); +});