Skip to content

Commit e611dad

Browse files
pvdlggr2m
authored andcommitted
feat: return all errors
Allow to report to the user multiple config/auth errors at once
1 parent 755356b commit e611dad

File tree

8 files changed

+161
-103
lines changed

8 files changed

+161
-103
lines changed

index.js

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const {castArray} = require('lodash');
2+
const AggregateError = require('aggregate-error');
23
const setLegacyToken = require('./lib/set-legacy-token');
34
const getPkg = require('./lib/get-pkg');
45
const verifyNpmConfig = require('./lib/verify-config');
@@ -10,39 +11,50 @@ let verified;
1011
async function verifyConditions(pluginConfig, {options: {publish}, logger}) {
1112
// If the npm publish plugin is used and has `npmPublish`, `tarballDir` or `pkgRoot` configured, validate them now in order to prevent any release if the configuration is wrong
1213
if (publish) {
13-
const publishPlugin = castArray(publish).find(config => config.path && config.path === '@semantic-release/npm');
14-
if (publishPlugin && publishPlugin.npmPublish) {
15-
pluginConfig.npmPublish = publishPlugin.npmPublish;
16-
}
17-
if (publishPlugin && publishPlugin.tarballDir) {
18-
pluginConfig.tarballDir = publishPlugin.tarballDir;
19-
}
20-
if (publishPlugin && publishPlugin.pkgRoot) {
21-
pluginConfig.pkgRoot = publishPlugin.pkgRoot;
22-
}
14+
const publishPlugin =
15+
castArray(publish).find(config => config.path && config.path === '@semantic-release/npm') || {};
16+
17+
pluginConfig.npmPublish = pluginConfig.npmPublish || publishPlugin.npmPublish;
18+
pluginConfig.tarballDir = pluginConfig.tarballDir || publishPlugin.tarballDir;
19+
pluginConfig.pkgRoot = pluginConfig.pkgRoot || publishPlugin.pkgRoot;
2320
}
2421

25-
const pkg = await getPkg(pluginConfig.pkgRoot);
26-
await verifyNpmConfig(pluginConfig, pkg, logger);
22+
const errors = verifyNpmConfig(pluginConfig);
2723

28-
// Verify the npm authentication only if `npmPublish` is not false
29-
if (pluginConfig.npmPublish !== false) {
30-
setLegacyToken();
31-
await verifyNpmAuth(pluginConfig, pkg, logger);
24+
try {
25+
const pkg = await getPkg(pluginConfig.pkgRoot);
26+
27+
// Verify the npm authentication only if `npmPublish` is not false
28+
if (pluginConfig.npmPublish !== false) {
29+
setLegacyToken();
30+
await verifyNpmAuth(pluginConfig, pkg, logger);
31+
}
32+
} catch (err) {
33+
errors.push(...err);
34+
}
35+
if (errors.length > 0) {
36+
throw new AggregateError(errors);
3237
}
3338
verified = true;
3439
}
3540

3641
async function publish(pluginConfig, {nextRelease: {version}, logger}) {
42+
let pkg;
43+
const errors = verifyNpmConfig(pluginConfig);
44+
3745
setLegacyToken();
38-
// Reload package.json in case a previous external step updated it
39-
const pkg = await getPkg(pluginConfig.pkgRoot);
40-
if (!verified) {
41-
await verifyNpmConfig(pluginConfig, pkg, logger);
42-
if (pluginConfig.npmPublish !== false) {
46+
47+
try {
48+
// Reload package.json in case a previous external step updated it
49+
pkg = await getPkg(pluginConfig.pkgRoot);
50+
if (!verified && pluginConfig.npmPublish !== false) {
4351
await verifyNpmAuth(pluginConfig, pkg, logger);
4452
}
45-
verified = true;
53+
} catch (err) {
54+
errors.push(...err);
55+
}
56+
if (errors.length > 0) {
57+
throw new AggregateError(errors);
4658
}
4759
return publishNpm(pluginConfig, pkg, version, logger);
4860
}

lib/get-pkg.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
const readPkg = require('read-pkg');
2+
const AggregateError = require('aggregate-error');
23
const SemanticReleaseError = require('@semantic-release/error');
34

45
module.exports = async pkgRoot => {
6+
const errors = [];
7+
let pkg;
8+
59
try {
6-
const pkg = await readPkg(pkgRoot);
10+
pkg = await readPkg(pkgRoot);
711

812
if (!pkg.name) {
9-
throw new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME');
13+
errors.push(new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME'));
1014
}
1115

1216
if (!pkg.version) {
13-
throw new SemanticReleaseError('No "version" found in package.json.', 'ENOPKGVERSION');
17+
errors.push(new SemanticReleaseError('No "version" found in package.json.', 'ENOPKGVERSION'));
1418
}
15-
16-
return pkg;
1719
} catch (err) {
1820
if (err.code === 'ENOENT') {
19-
throw new SemanticReleaseError('A package.json file is required to release on npm.', 'ENOPKG');
21+
errors.push(new SemanticReleaseError('A package.json file is required to release on npm.', 'ENOPKG'));
22+
} else {
23+
errors.push(err);
2024
}
21-
throw err;
2225
}
26+
if (errors.length > 0) {
27+
throw new AggregateError(errors);
28+
}
29+
return pkg;
2330
};

lib/verify-auth.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const execa = require('execa');
22
const normalizeUrl = require('normalize-url');
3+
const AggregateError = require('aggregate-error');
34
const SemanticReleaseError = require('@semantic-release/error');
45
const getRegistry = require('./get-registry');
56
const setNpmrcAuth = require('./set-npmrc-auth');
@@ -8,18 +9,18 @@ const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/';
89

910
module.exports = async (
1011
pluginConfig,
11-
pkg,
12+
{publishConfig, name},
1213
logger,
1314
defaultRegistry = process.env.DEFAULT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY
1415
) => {
15-
const registry = await getRegistry(pkg.publishConfig, pkg.name);
16+
const registry = await getRegistry(publishConfig, name);
1617
await setNpmrcAuth(registry, logger);
1718

1819
if (normalizeUrl(registry) === normalizeUrl(defaultRegistry)) {
1920
try {
2021
await execa('npm', ['whoami', '--registry', registry]);
2122
} catch (err) {
22-
throw new SemanticReleaseError('Invalid npm token.', 'EINVALIDNPMTOKEN');
23+
throw new AggregateError([new SemanticReleaseError('Invalid npm token.', 'EINVALIDNPMTOKEN')]);
2324
}
2425
}
2526
};

lib/verify-config.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
const {isString, isUndefined, isBoolean} = require('lodash');
22
const SemanticReleaseError = require('@semantic-release/error');
33

4-
module.exports = async ({npmPublish, tarballDir, pkgRoot}) => {
4+
module.exports = ({npmPublish, tarballDir, pkgRoot}) => {
5+
const errors = [];
56
if (!isUndefined(npmPublish) && !isBoolean(npmPublish)) {
6-
throw new SemanticReleaseError('The "npmPublish" options, if defined, must be a Boolean.', 'EINVALIDNPMPUBLISH');
7+
errors.push(
8+
new SemanticReleaseError('The "npmPublish" options, if defined, must be a Boolean.', 'EINVALIDNPMPUBLISH')
9+
);
710
}
811

912
if (!isUndefined(tarballDir) && !isString(tarballDir)) {
10-
throw new SemanticReleaseError('The "tarballDir" options, if defined, must be a String.', 'EINVALIDTARBALLDIR');
13+
errors.push(
14+
new SemanticReleaseError('The "tarballDir" options, if defined, must be a String.', 'EINVALIDTARBALLDIR')
15+
);
1116
}
1217

1318
if (!isUndefined(pkgRoot) && !isString(pkgRoot)) {
14-
throw new SemanticReleaseError('The "pkgRoot" options, if defined, must be a String.', 'EINVALIDPKGROOT');
19+
errors.push(new SemanticReleaseError('The "pkgRoot" options, if defined, must be a String.', 'EINVALIDPKGROOT'));
1520
}
21+
return errors;
1622
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
],
1818
"dependencies": {
1919
"@semantic-release/error": "^2.1.0",
20+
"aggregate-error": "^1.0.0",
2021
"execa": "^0.9.0",
2122
"fs-extra": "^5.0.0",
2223
"lodash": "^4.17.4",

test/get-pkg.test.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test.serial('Verify name and version then return parsed package.json from a sub-
3535
});
3636

3737
test.serial('Throw error if missing package.json', async t => {
38-
const error = await t.throws(getPkg());
38+
const [error] = await t.throws(getPkg());
3939

4040
t.is(error.name, 'SemanticReleaseError');
4141
t.is(error.code, 'ENOPKG');
@@ -44,7 +44,7 @@ test.serial('Throw error if missing package.json', async t => {
4444
test.serial('Throw error if missing package name', async t => {
4545
await outputJson('./package.json', {version: '0.0.0'});
4646

47-
const error = await t.throws(getPkg());
47+
const [error] = await t.throws(getPkg());
4848

4949
t.is(error.name, 'SemanticReleaseError');
5050
t.is(error.code, 'ENOPKGNAME');
@@ -53,16 +53,27 @@ test.serial('Throw error if missing package name', async t => {
5353
test.serial('Throw error if missing package version', async t => {
5454
await outputJson('./package.json', {name: 'package'});
5555

56-
const error = await t.throws(getPkg());
56+
const [error] = await t.throws(getPkg());
5757

5858
t.is(error.name, 'SemanticReleaseError');
5959
t.is(error.code, 'ENOPKGVERSION');
6060
});
6161

62+
test.serial('Throw errors if missing package version and name', async t => {
63+
await outputJson('./package.json', {});
64+
65+
const errors = [...(await t.throws(getPkg()))];
66+
67+
t.is(errors[0].name, 'SemanticReleaseError');
68+
t.is(errors[0].code, 'ENOPKGNAME');
69+
t.is(errors[1].name, 'SemanticReleaseError');
70+
t.is(errors[1].code, 'ENOPKGVERSION');
71+
});
72+
6273
test.serial('Throw error if package.json is malformed', async t => {
6374
await writeFile('./package.json', "{name: 'package',}");
6475

65-
const error = await t.throws(getPkg());
76+
const [error] = await t.throws(getPkg());
6677

6778
t.is(error.name, 'JSONError');
6879
});

test/integration.test.js

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const envBackup = Object.assign({}, process.env);
1313
const cwd = process.cwd();
1414
// Disable logs during tests
1515
stub(process.stdout, 'write');
16-
stub(process.stderr, 'write');
1716

1817
test.before(async () => {
1918
// Start the local NPM registry
@@ -61,7 +60,8 @@ test.serial('Throws error if NPM token is invalid', async t => {
6160
process.env.DEFAULT_NPM_REGISTRY = npmRegistry.url;
6261
const pkg = {name: 'published', version: '1.0.0', publishConfig: {registry: npmRegistry.url}};
6362
await outputJson('./package.json', pkg);
64-
const error = await t.throws(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
63+
64+
const [error] = await t.throws(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
6565

6666
t.true(error instanceof SemanticReleaseError);
6767
t.is(error.code, 'EINVALIDNPMTOKEN');
@@ -83,7 +83,7 @@ test.serial('Skip Token validation if the registry configured is not the default
8383

8484
test.serial('Verify npm auth and package', async t => {
8585
Object.assign(process.env, npmRegistry.authEnv);
86-
console.log(process.env);
86+
8787
const pkg = {name: 'valid-token', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
8888
await outputJson('./package.json', pkg);
8989
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
@@ -109,71 +109,43 @@ test.serial('Verify npm auth and package with "npm_config_registry" env var set
109109
process.env.npm_config_registry = 'https://registry.yarnpkg.com'; // eslint-disable-line camelcase
110110
const pkg = {name: 'valid-token', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
111111
await outputJson('./package.json', pkg);
112-
await t.notThrows(t.context.m.verifyConditions({}, {options: {}, logger: t.context.logger}));
112+
await t.notThrows(t.context.m.verifyConditions({}, {options: {publish: []}, logger: t.context.logger}));
113113

114114
const npmrc = (await readFile('.npmrc')).toString();
115115
t.regex(npmrc, /_auth =/);
116116
t.regex(npmrc, /email =/);
117117
});
118118

119-
test.serial(
120-
'Throw SemanticReleaseError if publish "npmPublish" option in verifyConditions is not a Boolean',
121-
async t => {
122-
const pkg = {name: 'invalid-npmPublish', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
123-
await outputJson('./package.json', pkg);
124-
const npmPublish = 42;
125-
const error = await t.throws(
126-
t.context.m.verifyConditions(
127-
{},
128-
{
129-
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', npmPublish}]},
130-
logger: t.context.logger,
131-
}
132-
)
133-
);
134-
135-
t.is(error.name, 'SemanticReleaseError');
136-
t.is(error.code, 'EINVALIDNPMPUBLISH');
137-
}
138-
);
139-
140-
test.serial(
141-
'Throw SemanticReleaseError if publish "tarballDir" option in verifyConditions is not a String',
142-
async t => {
143-
const pkg = {name: 'invalid-tarballDir', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
144-
await outputJson('./package.json', pkg);
145-
const tarballDir = 42;
146-
const error = await t.throws(
119+
test.serial('Throw SemanticReleaseError Array if config option are not valid in verifyConditions', async t => {
120+
const pkg = {publishConfig: {registry: npmRegistry.url}};
121+
await outputJson('./package.json', pkg);
122+
const npmPublish = 42;
123+
const tarballDir = 42;
124+
const pkgRoot = 42;
125+
const errors = [
126+
...(await t.throws(
147127
t.context.m.verifyConditions(
148128
{},
149129
{
150-
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', tarballDir}]},
130+
options: {
131+
publish: ['@semantic-release/github', {path: '@semantic-release/npm', npmPublish, tarballDir, pkgRoot}],
132+
},
151133
logger: t.context.logger,
152134
}
153135
)
154-
);
155-
156-
t.is(error.name, 'SemanticReleaseError');
157-
t.is(error.code, 'EINVALIDTARBALLDIR');
158-
}
159-
);
160-
161-
test.serial('Throw SemanticReleaseError if publish "pkgRoot" option in verifyConditions is not a String', async t => {
162-
const pkg = {name: 'invalid-pkgRoot', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};
163-
await outputJson('./package.json', pkg);
164-
const pkgRoot = 42;
165-
const error = await t.throws(
166-
t.context.m.verifyConditions(
167-
{},
168-
{
169-
options: {publish: ['@semantic-release/github', {path: '@semantic-release/npm', pkgRoot}]},
170-
logger: t.context.logger,
171-
}
172-
)
173-
);
174-
175-
t.is(error.name, 'SemanticReleaseError');
176-
t.is(error.code, 'EINVALIDPKGROOT');
136+
)),
137+
];
138+
139+
t.is(errors[0].name, 'SemanticReleaseError');
140+
t.is(errors[0].code, 'EINVALIDNPMPUBLISH');
141+
t.is(errors[1].name, 'SemanticReleaseError');
142+
t.is(errors[1].code, 'EINVALIDTARBALLDIR');
143+
t.is(errors[2].name, 'SemanticReleaseError');
144+
t.is(errors[2].code, 'EINVALIDPKGROOT');
145+
t.is(errors[3].name, 'SemanticReleaseError');
146+
t.is(errors[3].code, 'ENOPKGNAME');
147+
t.is(errors[4].name, 'SemanticReleaseError');
148+
t.is(errors[4].code, 'ENOPKGVERSION');
177149
});
178150

179151
test.serial('Publish the package', async t => {
@@ -257,6 +229,38 @@ test.serial('Create the package and skip publish from a sub-directory', async t
257229
await t.throws(execa('npm', ['view', pkg.name, 'version']));
258230
});
259231

232+
test.serial('Throw SemanticReleaseError Array if config option are not valid in publish', async t => {
233+
const pkg = {publishConfig: {registry: npmRegistry.url}};
234+
await outputJson('./package.json', pkg);
235+
const npmPublish = 42;
236+
const tarballDir = 42;
237+
const pkgRoot = 42;
238+
239+
const errors = [
240+
...(await t.throws(
241+
t.context.m.publish(
242+
{npmPublish, tarballDir, pkgRoot},
243+
{
244+
options: {publish: ['@semantic-release/github', '@semantic-release/npm']},
245+
nextRelease: {version: '1.0.0'},
246+
logger: t.context.logger,
247+
}
248+
)
249+
)),
250+
];
251+
252+
t.is(errors[0].name, 'SemanticReleaseError');
253+
t.is(errors[0].code, 'EINVALIDNPMPUBLISH');
254+
t.is(errors[1].name, 'SemanticReleaseError');
255+
t.is(errors[1].code, 'EINVALIDTARBALLDIR');
256+
t.is(errors[2].name, 'SemanticReleaseError');
257+
t.is(errors[2].code, 'EINVALIDPKGROOT');
258+
t.is(errors[3].name, 'SemanticReleaseError');
259+
t.is(errors[3].code, 'ENOPKGNAME');
260+
t.is(errors[4].name, 'SemanticReleaseError');
261+
t.is(errors[4].code, 'ENOPKGVERSION');
262+
});
263+
260264
test.serial('Verify token and set up auth only on the fist call', async t => {
261265
Object.assign(process.env, npmRegistry.authEnv);
262266
const pkg = {name: 'test-module', version: '0.0.0-dev', publishConfig: {registry: npmRegistry.url}};

0 commit comments

Comments
 (0)