From fb96126bfb3e65eb4a3be11e772cc55f423bbd98 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Thu, 28 Jun 2018 01:43:40 -0400 Subject: [PATCH] feat: add `addChannel` plugin step --- README.md | 11 +- index.js | 26 +++- lib/add-channel.js | 45 +++++++ lib/get-release-info.js | 3 +- lib/prepare.js | 5 +- lib/publish.js | 11 +- package.json | 2 +- test/get-release-info.test.js | 2 + test/integration.test.js | 219 ++++++++++++++++++++++++++++++++-- 9 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 lib/add-channel.js diff --git a/README.md b/README.md index 13db8d53..7dc82560 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ [![npm latest version](https://img.shields.io/npm/v/@semantic-release/npm/latest.svg)](https://www.npmjs.com/package/@semantic-release/npm) [![npm next version](https://img.shields.io/npm/v/@semantic-release/npm/next.svg)](https://www.npmjs.com/package/@semantic-release/npm) -| Step | Description | -|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| -| `verifyConditions` | Verify the presence of the `NPM_TOKEN` environment variable, create or update the `.npmrc` file with the token and verify the token is valid. | -| `prepare` | Update the `package.json` version and [create](https://docs.npmjs.com/cli/pack) the npm package tarball. | -| `publish` | [Publish the npm package](https://docs.npmjs.com/cli/publish) to the registry. | +| Step | Description | | +|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| `verifyConditions` | Verify the presence of the `NPM_TOKEN` environment variable, create or update the `.npmrc` file with the token and verify the token is valid. | | +| `prepare` | Update the `package.json` version and [create](https://docs.npmjs.com/cli/pack) the npm package tarball. | | +| `addChannel` | | [Add a release to a dist-tag](https://docs.npmjs.com/cli/dist-tag). | +| `publish` | [Publish the npm package](https://docs.npmjs.com/cli/publish) to the registry. | | ## Install diff --git a/index.js b/index.js index 7330f838..90c4611d 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const setLegacyToken = require('./lib/set-legacy-token'); const getPkg = require('./lib/get-pkg'); const verifyNpmConfig = require('./lib/verify-config'); const verifyNpmAuth = require('./lib/verify-auth'); +const addChannelNpm = require('./lib/add-channel'); const prepareNpm = require('./lib/prepare'); const publishNpm = require('./lib/publish'); @@ -87,4 +88,27 @@ async function publish(pluginConfig, context) { return publishNpm(pluginConfig, pkg, context); } -module.exports = {verifyConditions, prepare, publish}; +async function addChannel(pluginConfig, context) { + let pkg; + const errors = verified ? [] : verifyNpmConfig(pluginConfig); + + setLegacyToken(context); + + try { + // Reload package.json in case a previous external step updated it + pkg = await getPkg(pluginConfig, context); + if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) { + await verifyNpmAuth(pluginConfig, pkg, context); + } + } catch (error) { + errors.push(...error); + } + + if (errors.length > 0) { + throw new AggregateError(errors); + } + + return addChannelNpm(pluginConfig, pkg, context); +} + +module.exports = {verifyConditions, prepare, publish, addChannel}; diff --git a/lib/add-channel.js b/lib/add-channel.js new file mode 100644 index 00000000..4b159139 --- /dev/null +++ b/lib/add-channel.js @@ -0,0 +1,45 @@ +const execa = require('execa'); +const getRegistry = require('./get-registry'); +const getChannel = require('./get-channel'); +const getReleaseInfo = require('./get-release-info'); + +module.exports = async ({npmPublish}, pkg, context) => { + const { + cwd, + env, + stdout, + stderr, + nextRelease: {version, channel}, + logger, + } = context; + + if (npmPublish !== false && pkg.private !== true) { + const registry = getRegistry(pkg, context); + const distTag = getChannel(channel); + + logger.log(`Adding version ${version} to npm registry on dist-tag ${distTag}`); + const result = execa('npm', ['dist-tag', 'add', `${pkg.name}@${version}`, distTag, '--registry', registry], { + cwd, + env, + }); + result.stdout.pipe( + stdout, + {end: false} + ); + result.stderr.pipe( + stderr, + {end: false} + ); + await result; + + logger.log(`Published ${pkg.name}@${version} on ${registry}`); + + return getReleaseInfo(pkg, context, distTag, registry); + } + + logger.log( + `Skip adding to npm channel as ${ + npmPublish === false ? 'npmPublish' : "package.json's private property" + } is ${npmPublish !== false}` + ); +}; diff --git a/lib/get-release-info.js b/lib/get-release-info.js index 5956acb6..bb4e4485 100644 --- a/lib/get-release-info.js +++ b/lib/get-release-info.js @@ -1,7 +1,8 @@ const normalizeUrl = require('normalize-url'); -module.exports = async ({name}, {env: {DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/'}}, distTag, registry) => ({ +module.exports = ({name}, {env: {DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/'}}, distTag, registry) => ({ name: `npm package (@${distTag} dist-tag)`, url: normalizeUrl(registry) === normalizeUrl(DEFAULT_NPM_REGISTRY) ? `https://www.npmjs.com/package/${name}` : undefined, + channel: distTag, }); diff --git a/lib/prepare.js b/lib/prepare.js index 096394d8..9bc65c51 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -7,7 +7,10 @@ module.exports = async ({tarballDir, pkgRoot}, {cwd, env, stdout, stderr, nextRe logger.log('Write version %s to package.json in %s', version, basePath); - const versionResult = execa('npm', ['version', version, '--no-git-tag-version'], {cwd: basePath, env}); + const versionResult = execa('npm', ['version', version, '--no-git-tag-version', '--allow-same-version'], { + cwd: basePath, + env, + }); versionResult.stdout.pipe( stdout, {end: false} diff --git a/lib/publish.js b/lib/publish.js index f5a634eb..8ffdcfa7 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -19,7 +19,7 @@ module.exports = async ({npmPublish, pkgRoot}, pkg, context) => { const registry = getRegistry(pkg, context); const distTag = getChannel(channel); - logger.log('Publishing version %s to npm registry', version); + logger.log(`Publishing version ${version} to npm registry`); const result = execa('npm', ['publish', basePath, '--tag', distTag, '--registry', registry], { cwd, env, @@ -34,11 +34,14 @@ module.exports = async ({npmPublish, pkgRoot}, pkg, context) => { ); await result; - logger.log(`Published ${pkg.name}@${pkg.version} on ${registry}`); + logger.log(`Published ${pkg.name}@${version} to ${registry}`); + return getReleaseInfo(pkg, context, distTag, registry); } + logger.log( - 'Skip publishing to npm registry as %s is %s', - ...(npmPublish === false ? ['npmPublish', false] : ["package.json's private property", true]) + `Skip publishing to npm registry as ${ + npmPublish === false ? 'npmPublish' : "package.json's private property" + } is ${npmPublish !== false}` ); }; diff --git a/package.json b/package.json index 859c431f..49b9bd52 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "all": true }, "peerDependencies": { - "semantic-release": ">=15.9.0 <16.0.0" + "semantic-release": ">=15.9.0 <17.0.0" }, "prettier": { "printWidth": 120, diff --git a/test/get-release-info.test.js b/test/get-release-info.test.js index 2ff2690f..e49ba8c9 100644 --- a/test/get-release-info.test.js +++ b/test/get-release-info.test.js @@ -5,6 +5,7 @@ test('Default registry and scoped module', async t => { t.deepEqual(await getReleaseInfo({name: '@scope/module'}, {env: {}}, 'latest', 'https://registry.npmjs.org/'), { name: 'npm package (@latest dist-tag)', url: 'https://www.npmjs.com/package/@scope/module', + channel: 'latest', }); }); @@ -12,5 +13,6 @@ test('Custom registry and scoped module', async t => { t.deepEqual(await getReleaseInfo({name: '@scope/module'}, {env: {}}, 'latest', 'https://custom.registry.org/'), { name: 'npm package (@latest dist-tag)', url: undefined, + channel: 'latest', }); }); diff --git a/test/integration.test.js b/test/integration.test.js index 49633e2d..149f08c7 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -254,7 +254,7 @@ test('Publish the package', async t => { } ); - t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined}); + t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined, channel: 'latest'}); t.is((await readJson(path.resolve(cwd, 'package.json'))).version, '1.0.0'); t.false(await pathExists(path.resolve(cwd, `${pkg.name}-1.0.0.tgz`))); t.is(await execa.stdout('npm', ['view', pkg.name, 'version'], {cwd, env: testEnv}), '1.0.0'); @@ -279,7 +279,11 @@ test('Publish the package on a dist-tag', async t => { } ); - t.deepEqual(result, {name: 'npm package (@next dist-tag)', url: 'https://www.npmjs.com/package/publish-tag'}); + t.deepEqual(result, { + name: 'npm package (@next dist-tag)', + url: 'https://www.npmjs.com/package/publish-tag', + channel: 'next', + }); t.is((await readJson(path.resolve(cwd, 'package.json'))).version, '1.0.0'); t.false(await pathExists(path.resolve(cwd, `${pkg.name}-1.0.0.tgz`))); t.is(await execa.stdout('npm', ['view', pkg.name, 'version'], {cwd, env: testEnv}), '1.0.0'); @@ -304,7 +308,7 @@ test('Publish the package from a sub-directory', async t => { } ); - t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined}); + t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined, channel: 'latest'}); t.is((await readJson(path.resolve(cwd, 'dist/package.json'))).version, '1.0.0'); t.false(await pathExists(path.resolve(cwd, `${pkg.name}-1.0.0.tgz`))); t.is(await execa.stdout('npm', ['view', pkg.name, 'version'], {cwd, env: testEnv}), '1.0.0'); @@ -338,7 +342,12 @@ test('Create the package and skip publish ("npmPublish" is false)', async t => { test('Create the package and skip publish ("package.private" is true)', async t => { const cwd = tempy.directory(); const env = npmRegistry.authEnv; - const pkg = {name: 'skip-publish', version: '0.0.0', publishConfig: {registry: npmRegistry.url}, private: true}; + const pkg = { + name: 'skip-publish-private', + version: '0.0.0', + publishConfig: {registry: npmRegistry.url}, + private: true, + }; await outputJson(path.resolve(cwd, 'package.json'), pkg); const result = await t.context.m.publish( @@ -531,6 +540,191 @@ test('Throw SemanticReleaseError Array if config option are not valid in prepare t.is(errors[3].code, 'ENOPKG'); }); +test('Publish the package and add to default dist-tag', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = {name: 'add-channel', version: '0.0.0', publishConfig: {registry: npmRegistry.url}}; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + + await t.context.m.publish( + {}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {channel: 'next', version: '1.0.0'}, + } + ); + + const result = await t.context.m.addChannel( + {}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {version: '1.0.0'}, + } + ); + + t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined, channel: 'latest'}); + t.is(await execa.stdout('npm', ['view', pkg.name, 'dist-tags.latest'], {cwd, env}), '1.0.0'); +}); + +test('Publish the package and add to lts dist-tag', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = {name: 'add-channel-legacy', version: '1.0.0', publishConfig: {registry: npmRegistry.url}}; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + + await t.context.m.publish( + {}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {channel: 'latest', version: '1.0.0'}, + } + ); + + const result = await t.context.m.addChannel( + {}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {channel: '1.x', version: '1.0.0'}, + } + ); + + t.deepEqual(result, {name: 'npm package (@release-1.x dist-tag)', url: undefined, channel: 'release-1.x'}); + t.is( + await execa.stdout('npm', ['view', pkg.name, 'dist-tags'], {cwd, env}), + "{ latest: '1.0.0', 'release-1.x': '1.0.0' }" + ); +}); + +test('Skip adding the package to a channel ("npmPublish" is false)', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = {name: 'skip-add-channel', version: '0.0.0', publishConfig: {registry: npmRegistry.url}}; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + + const result = await t.context.m.addChannel( + {npmPublish: false}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {version: '1.0.0'}, + } + ); + + t.falsy(result); + await t.throws(execa('npm', ['view', pkg.name, 'version'], {cwd, env})); +}); + +test('Skip adding the package to a channel ("package.private" is true)', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = { + name: 'skip-add-channel-private', + version: '0.0.0', + publishConfig: {registry: npmRegistry.url}, + private: true, + }; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + + const result = await t.context.m.addChannel( + {}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {version: '1.0.0'}, + } + ); + + t.falsy(result); + await t.throws(execa('npm', ['view', pkg.name, 'version'], {cwd, env})); +}); + +test('Create the package in addChannel step', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = {name: 'add-channel-pkg', version: '0.0.0', publishConfig: {registry: npmRegistry.url}}; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + + await t.context.m.prepare( + {npmPublish: false, tarballDir: 'tarball'}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {version: '1.0.0'}, + } + ); + + t.is((await readJson(path.resolve(cwd, 'package.json'))).version, '1.0.0'); + t.true(await pathExists(path.resolve(cwd, `tarball/${pkg.name}-1.0.0.tgz`))); +}); + +test('Throw SemanticReleaseError Array if config option are not valid in addChannel', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = {publishConfig: {registry: npmRegistry.url}}; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + const npmPublish = 42; + const tarballDir = 42; + const pkgRoot = 42; + + const errors = [ + ...(await t.throws( + t.context.m.addChannel( + {npmPublish, tarballDir, pkgRoot}, + { + cwd, + env, + options: {publish: ['@semantic-release/github', '@semantic-release/npm']}, + nextRelease: {version: '1.0.0'}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + } + ) + )), + ]; + + t.is(errors[0].name, 'SemanticReleaseError'); + t.is(errors[0].code, 'EINVALIDNPMPUBLISH'); + t.is(errors[1].name, 'SemanticReleaseError'); + t.is(errors[1].code, 'EINVALIDTARBALLDIR'); + t.is(errors[2].name, 'SemanticReleaseError'); + t.is(errors[2].code, 'EINVALIDPKGROOT'); + t.is(errors[3].name, 'SemanticReleaseError'); + t.is(errors[3].code, 'ENOPKG'); +}); + test('Verify token and set up auth only on the fist call, then prepare on prepare call only', async t => { const cwd = tempy.directory(); const env = npmRegistry.authEnv; @@ -568,11 +762,22 @@ test('Verify token and set up auth only on the fist call, then prepare on prepar nextRelease: {channel: 'next', version: '1.0.0'}, } ); - t.deepEqual(result, {name: 'npm package (@next dist-tag)', url: undefined}); + t.deepEqual(result, {name: 'npm package (@next dist-tag)', url: undefined, channel: 'next'}); t.is(await execa.stdout('npm', ['view', pkg.name, 'dist-tags.next'], {cwd, env}), '1.0.0'); - result = await t.context.m.addChannel({}, {logger: t.context.logger, nextRelease: {version: '1.0.0'}}); + result = await t.context.m.addChannel( + {}, + { + cwd, + env, + options: {}, + stdout: t.context.stdout, + stderr: t.context.stderr, + logger: t.context.logger, + nextRelease: {version: '1.0.0'}, + } + ); - t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined}); + t.deepEqual(result, {name: 'npm package (@latest dist-tag)', url: undefined, channel: 'latest'}); t.is(await execa.stdout('npm', ['view', pkg.name, 'dist-tags.latest'], {cwd, env}), '1.0.0'); });