From 144fb6355e5bbed770ada1eb176c5210697c054e Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Fri, 13 Dec 2019 10:24:48 -0500 Subject: [PATCH] feat: add an otpProvider option to allow users to use a module to provide an OTP to semantic-release --- README.md | 13 +++++++------ lib/publish.js | 13 +++++++++++-- test/helpers/fake-otp-provider.js | 1 + test/integration.test.js | 27 +++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 test/helpers/fake-otp-provider.js diff --git a/README.md b/README.md index c4b23afc..ac1f2076 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ The npm authentication configuration is **required** and can be set via [environ 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. -**Note**: Only the `auth-only` [level of npm two-factor authentication](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication) is supported, **semantic-release** will not work with the default `auth-and-writes` level. +**Note**: Only the `auth-only` [level of npm two-factor authentication](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication) is supported, **semantic-release** will not work with the default `auth-and-writes` level by default. If you want to use `auth-and-writes` you will need to use the `otpProvider` option to somehow give semantic-release an OTP token to use. ### Environment variables @@ -58,11 +58,12 @@ 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. | `false` if the `package.json` [private](https://docs.npmjs.com/files/package.json#private) property is `true`, `true` otherwise. | -| `pkgRoot` | Directory path to publish. | `.` | -| `tarballDir` | Directory path in which to write the the package tarball. If `false` the tarball is not be kept on the file system. | `false` | +| Options | Description | Default | +|---------------|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------| +| `npmPublish` | Whether to publish the `npm` package to the registry. If `false` the `package.json` version will still be updated. | `false` if the `package.json` [private](https://docs.npmjs.com/files/package.json#private) property is `true`, `true` otherwise. | +| `pkgRoot` | Directory path to publish. | `.` | +| `tarballDir` | Directory path in which to write the the package tarball. If `false` the tarball is not be kept on the file system. | `false` | +| `otpProvider` | Package name for an npm module that exports a single async function that resolves with an OTP code. | `undefined` | **Note**: The `pkgRoot` directory must contains a `package.json`. The version will be updated only in the `package.json` and `npm-shrinkwrap.json` within the `pkgRoot` directory. diff --git a/lib/publish.js b/lib/publish.js index 6f7756e7..b8f91d69 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -3,7 +3,7 @@ const execa = require('execa'); const getRegistry = require('./get-registry'); const getReleaseInfo = require('./get-release-info'); -module.exports = async (npmrc, {npmPublish, pkgRoot}, pkg, context) => { +module.exports = async (npmrc, {npmPublish, otpProvider, pkgRoot}, pkg, context) => { const { cwd, env, @@ -18,7 +18,16 @@ module.exports = async (npmrc, {npmPublish, pkgRoot}, pkg, context) => { const registry = getRegistry(pkg, context); logger.log('Publishing version %s to npm registry', version); - const result = execa('npm', ['publish', basePath, '--userconfig', npmrc, '--registry', registry], {cwd, env}); + const otpArgs = []; + if (otpProvider) { + const otp = await require(otpProvider)(); + otpArgs.push('--otp', otp); + } + + const result = execa('npm', ['publish', basePath, '--userconfig', npmrc, '--registry', registry, ...otpArgs], { + cwd, + env, + }); result.stdout.pipe(stdout, {end: false}); result.stderr.pipe(stderr, {end: false}); await result; diff --git a/test/helpers/fake-otp-provider.js b/test/helpers/fake-otp-provider.js new file mode 100644 index 00000000..64caaffc --- /dev/null +++ b/test/helpers/fake-otp-provider.js @@ -0,0 +1 @@ +module.exports = () => Promise.resolve('123'); diff --git a/test/integration.test.js b/test/integration.test.js index d70baaba..997b6a75 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -267,6 +267,33 @@ test('Publish the package on a dist-tag', async t => { t.is((await execa('npm', ['view', pkg.name, 'version'], {cwd, env: testEnv})).stdout, '1.0.0'); }); +test('Publish the package with OTP', async t => { + const cwd = tempy.directory(); + const env = npmRegistry.authEnv; + const pkg = {name: 'publish-otp', version: '0.0.0', publishConfig: {registry: npmRegistry.url}}; + await outputJson(path.resolve(cwd, 'package.json'), pkg); + + const result = await t.context.m.publish( + { + otpProvider: path.resolve(__dirname, 'helpers', 'fake-otp-provider.js'), + }, + { + 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.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('npm', ['view', pkg.name, 'version'], {cwd, env: testEnv})).stdout, '1.0.0'); +}); + test('Publish the package from a sub-directory', async t => { const cwd = tempy.directory(); const env = npmRegistry.authEnv;