Skip to content

Commit d04a9b0

Browse files
committed
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
1 parent 5fb0b09 commit d04a9b0

7 files changed

Lines changed: 165 additions & 22 deletions

File tree

README.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ Determine the last release of the package on the `npm` registry.
1616

1717
## publish
1818

19-
Publish the package on the `npm` registry.
19+
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.
2020

2121
## Configuration
2222

23-
### Environment variables
23+
### Npm registry authentication
2424

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

2727
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.
2828

29-
| Variable | Description
29+
### Environment variables
30+
31+
| Variable | Description |
3032
| -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
3133
| `NPM_TOKEN` | Npm token created via [npm token create](https://docs.npmjs.com/getting-started/working_with_tokens#how-to-create-new-tokens) |
3234
| `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
3739

3840
### Options
3941

42+
| Options | Description | Default |
43+
| ------------ | ---------------------------------------------------------------------------------------------------------------------- | ------- |
44+
| `npmPublish` | Whether to publish the `npm` package to the registry. If `false` the `package.json` version will still be updated. | `true` |
45+
| `tarballDir` | Directory path in which to generate the the package tarball. If `false` the tarball is not be kept on the file system. | `false` |
46+
47+
### Npm configuration
48+
4049
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.
4150

4251
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://
5463
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.
5564

5665
Each individual plugin can be disabled, replaced or used with other plugins in the `package.json`:
66+
5767
```json
5868
{
5969
"release": {
@@ -63,3 +73,25 @@ Each individual plugin can be disabled, replaced or used with other plugins in t
6373
}
6474
}
6575
```
76+
77+
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):
78+
79+
```json
80+
{
81+
"release": {
82+
"verifyConditions": ["@semantic-release/conditions-travis", "@semantic-release/npm", "@semantic-release/git", "@semantic-release/github"],
83+
"getLastRelease": "@semantic-release/git",
84+
"publish": [
85+
{
86+
"path": "@semantic-release/npm",
87+
"npmPublish": false,
88+
"tarballDir": "dist"
89+
},
90+
{
91+
"path": "@semantic-release/github",
92+
"assets": "dist/*.tgz"
93+
},
94+
]
95+
}
96+
}
97+
```

index.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const {castArray} = require('lodash');
12
const setLegacyToken = require('./lib/set-legacy-token');
23
const getPkg = require('./lib/get-pkg');
34
const verifyNpm = require('./lib/verify');
@@ -6,10 +7,22 @@ const getLastReleaseNpm = require('./lib/get-last-release');
67

78
let verified;
89

9-
async function verifyConditions(pluginConfig, {logger}) {
10+
async function verifyConditions(pluginConfig, {options, logger}) {
11+
// 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
12+
if (options.publish) {
13+
const publishPlugin = castArray(options.publish).find(
14+
config => config.path && config.path === '@semantic-release/npm'
15+
);
16+
if (publishPlugin && publishPlugin.npmPublish) {
17+
pluginConfig.npmPublish = publishPlugin.npmPublish;
18+
}
19+
if (publishPlugin && publishPlugin.tarballDir) {
20+
pluginConfig.tarballDir = publishPlugin.tarballDir;
21+
}
22+
}
1023
setLegacyToken();
1124
const pkg = await getPkg();
12-
await verifyNpm(pkg, logger);
25+
await verifyNpm(pluginConfig, pkg, logger);
1326
verified = true;
1427
}
1528

@@ -18,7 +31,7 @@ async function getLastRelease(pluginConfig, {logger}) {
1831
// Reload package.json in case a previous external step updated it
1932
const pkg = await getPkg();
2033
if (!verified) {
21-
await verifyNpm(pkg, logger);
34+
await verifyNpm(pluginConfig, pkg, logger);
2235
verified = true;
2336
}
2437
return getLastReleaseNpm(pkg, logger);
@@ -29,10 +42,10 @@ async function publish(pluginConfig, {nextRelease: {version}, logger}) {
2942
// Reload package.json in case a previous external step updated it
3043
const pkg = await getPkg();
3144
if (!verified) {
32-
await verifyNpm(pkg, logger);
45+
await verifyNpm(pluginConfig, pkg, logger);
3346
verified = true;
3447
}
35-
await publishNpm(pkg, version, logger);
48+
await publishNpm(pluginConfig, pkg, version, logger);
3649
}
3750

3851
module.exports = {verifyConditions, getLastRelease, publish};

lib/publish.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
const path = require('path');
2+
const {move} = require('fs-extra');
13
const execa = require('execa');
24
const getRegistry = require('./get-registry');
35
const updatePackageVersion = require('./update-package-version');
46

5-
module.exports = async ({publishConfig, name}, version, logger) => {
7+
module.exports = async ({npmPublish, tarballDir}, {publishConfig, name}, version, logger) => {
68
const registry = await getRegistry(publishConfig, name);
79
await updatePackageVersion(version, logger);
810

9-
logger.log('Publishing version %s to npm registry', version);
10-
const shell = await execa('npm', ['publish', '--registry', registry]);
11-
process.stdout.write(shell.stdout);
11+
if (tarballDir) {
12+
logger.log('Creating npm package version %s', version);
13+
const tarball = await execa.stdout('npm', ['pack']);
14+
await move(tarball, path.join(tarballDir.trim(), tarball));
15+
}
16+
17+
if (npmPublish !== false) {
18+
logger.log('Publishing version %s to npm registry', version);
19+
const shell = await execa('npm', ['publish', '--registry', registry]);
20+
process.stdout.write(shell.stdout);
21+
}
1222
};

lib/verify.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
const {isString, isUndefined, isBoolean} = require('lodash');
12
const execa = require('execa');
23
const SemanticReleaseError = require('@semantic-release/error');
34
const getRegistry = require('./get-registry');
45
const setNpmrcAuth = require('./set-npmrc-auth');
56

6-
module.exports = async (pkg, logger) => {
7+
module.exports = async ({npmPublish, tarballDir}, pkg, logger) => {
8+
if (!isUndefined(npmPublish) && !isBoolean(npmPublish)) {
9+
throw new SemanticReleaseError('The "npmPublish" options, if defined, must be a Boolean.', 'EINVALIDNPMPUBLISH');
10+
}
11+
12+
if (!isUndefined(tarballDir) && !isString(tarballDir)) {
13+
throw new SemanticReleaseError('The "tarballDir" options, if defined, must be a String.', 'EINVALIDTARBALLDIR');
14+
}
15+
716
const registry = await getRegistry(pkg.publishConfig, pkg.name);
817
await setNpmrcAuth(registry, logger);
918
try {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@semantic-release/error": "^2.1.0",
2020
"execa": "^0.8.0",
2121
"fs-extra": "^4.0.2",
22+
"lodash": "^4.17.4",
2223
"nerf-dart": "^1.0.0",
2324
"npm-conf": "^1.1.3",
2425
"npm-registry-client": "^8.5.0",

test/integration.test.js

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {writeJson, readFile, appendFile} from 'fs-extra';
21
import test from 'ava';
2+
import {writeJson, readJson, readFile, appendFile, pathExists} from 'fs-extra';
33
import execa from 'execa';
44
import {stub} from 'sinon';
55
import tempy from 'tempy';
@@ -57,7 +57,7 @@ test.serial('Throws error if NPM token is invalid', async t => {
5757
process.env.NPM_TOKEN = 'wrong_token';
5858
const pkg = {name: 'published', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
5959
await writeJson('./package.json', pkg);
60-
const error = await t.throws(t.context.m.verifyConditions({}, {logger: t.context.logger}));
60+
const error = await t.throws(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
6161

6262
t.true(error instanceof SemanticReleaseError);
6363
t.is(error.code, 'EINVALIDNPMTOKEN');
@@ -71,7 +71,7 @@ test.serial('Verify npm auth and package', async t => {
7171
Object.assign(process.env, npmRegistry.authEnv);
7272
const pkg = {name: 'valid-token', publishConfig: {registry: npmRegistry.url}};
7373
await writeJson('./package.json', pkg);
74-
await t.notThrows(t.context.m.verifyConditions({}, {logger: t.context.logger}));
74+
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
7575

7676
const npmrc = (await readFile('.npmrc')).toString();
7777
t.regex(npmrc, /_auth =/);
@@ -83,7 +83,7 @@ test.serial('Verify npm auth and package with "npm_config_registry" env var set
8383
process.env.npm_config_registry = 'https://registry.yarnpkg.com'; // eslint-disable-line camelcase
8484
const pkg = {name: 'valid-token', publishConfig: {registry: npmRegistry.url}};
8585
await writeJson('./package.json', pkg);
86-
await t.notThrows(t.context.m.verifyConditions({}, {logger: t.context.logger}));
86+
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
8787

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

162-
test.serial('Publish a package', async t => {
162+
test('Throw SemanticReleaseError if publish "npmPublish" option is not a Boolean', async t => {
163+
const pkg = {name: 'invalid-npmPublish', publishConfig: {registry: npmRegistry.url}};
164+
await writeJson('./package.json', pkg);
165+
const npmPublish = 42;
166+
const error = await t.throws(
167+
t.context.m.verifyConditions(
168+
{},
169+
{
170+
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', npmPublish}]},
171+
logger: t.context.logger,
172+
}
173+
)
174+
);
175+
176+
t.is(error.name, 'SemanticReleaseError');
177+
t.is(error.code, 'EINVALIDNPMPUBLISH');
178+
});
179+
180+
test('Throw SemanticReleaseError if publish "tarballDir" option is not a String', async t => {
181+
const pkg = {name: 'invalid-tarballDir', publishConfig: {registry: npmRegistry.url}};
182+
await writeJson('./package.json', pkg);
183+
const tarballDir = 42;
184+
const error = await t.throws(
185+
t.context.m.verifyConditions(
186+
{},
187+
{
188+
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', tarballDir}]},
189+
logger: t.context.logger,
190+
}
191+
)
192+
);
193+
194+
t.is(error.name, 'SemanticReleaseError');
195+
t.is(error.code, 'EINVALIDTARBALLDIR');
196+
});
197+
198+
test.serial('Publish the package', async t => {
163199
Object.assign(process.env, npmRegistry.authEnv);
164-
const pkg = {name: 'publish', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
200+
const pkg = {name: 'publish', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
165201
await writeJson('./package.json', pkg);
166202

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

169-
t.is((await execa('npm', ['view', 'publish', 'version'])).stdout, '1.0.0');
205+
t.is((await readJson('./package.json')).version, '1.0.0');
206+
t.false(await pathExists(`./${pkg.name}-1.0.0.tgz`));
207+
t.is((await execa('npm', ['view', pkg.name, 'version'])).stdout, '1.0.0');
208+
});
209+
210+
test.serial('Create the package and skip publish', async t => {
211+
Object.assign(process.env, npmRegistry.authEnv);
212+
const pkg = {name: 'skip-publish', version: '0.0.0', publishConfig: {registry: npmRegistry.url}};
213+
await writeJson('./package.json', pkg);
214+
215+
await t.context.m.publish(
216+
{npmPublish: false, tarballDir: 'dist'},
217+
{logger: t.context.logger, nextRelease: {version: '1.0.0'}}
218+
);
219+
220+
t.is((await readJson('./package.json')).version, '1.0.0');
221+
t.true(await pathExists(`./dist/${pkg.name}-1.0.0.tgz`));
222+
await t.throws(execa('npm', ['view', pkg.name, 'version']));
170223
});
171224

172225
test.serial('Verify token and set up auth only on the fist call', async t => {
173226
Object.assign(process.env, npmRegistry.authEnv);
174227
const pkg = {name: 'test-module', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
175228
await writeJson('./package.json', pkg);
176229

177-
await t.notThrows(t.context.m.verifyConditions({}, {logger: t.context.logger}));
230+
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
178231

179232
let nextRelease = await t.context.m.getLastRelease({}, {logger: t.context.logger});
180233
t.falsy(nextRelease);

test/verify.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import test from 'ava';
2+
import {stub} from 'sinon';
3+
import verify from '../lib/verify';
4+
5+
test.beforeEach(t => {
6+
// Stub the logger functions
7+
t.context.log = stub();
8+
t.context.logger = {log: t.context.log};
9+
});
10+
11+
test('Throw SemanticReleaseError if "npmPublish" option is not a Boolean', async t => {
12+
const npmPublish = 42;
13+
const error = await t.throws(verify({npmPublish}, {}, t.context.logger));
14+
15+
t.is(error.name, 'SemanticReleaseError');
16+
t.is(error.code, 'EINVALIDNPMPUBLISH');
17+
});
18+
19+
test('Throw SemanticReleaseError if "tarballDir" option is not a String', async t => {
20+
const tarballDir = 42;
21+
const error = await t.throws(verify({tarballDir}, {}, t.context.logger));
22+
23+
t.is(error.name, 'SemanticReleaseError');
24+
t.is(error.code, 'EINVALIDTARBALLDIR');
25+
});

0 commit comments

Comments
 (0)