Skip to content

Commit 0e5b372

Browse files
committed
feat: return all errors
Allow to report to the user multiple config/auth errors at once
1 parent a28de30 commit 0e5b372

File tree

4 files changed

+75
-64
lines changed

4 files changed

+75
-64
lines changed

lib/verify.js

+41-30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const {isString, isPlainObject, isUndefined, isArray} = require('lodash');
22
const parseGithubUrl = require('parse-github-url');
33
const urlJoin = require('url-join');
4+
const AggregateError = require('aggregate-error');
45
const SemanticReleaseError = require('@semantic-release/error');
56
const resolveConfig = require('./resolve-config');
67
const getClient = require('./get-client');
@@ -9,12 +10,9 @@ const isNonEmptyString = value => isString(value) && value.trim();
910
const isStringOrStringArray = value => isNonEmptyString(value) || (isArray(value) && value.every(isNonEmptyString));
1011

1112
module.exports = async (pluginConfig, {options: {repositoryUrl}, logger}) => {
13+
const errors = [];
1214
const {githubToken, githubUrl, githubApiPathPrefix, assets, successComment} = resolveConfig(pluginConfig);
1315

14-
if (!githubToken) {
15-
throw new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN');
16-
}
17-
1816
if (
1917
!isUndefined(assets) &&
2018
assets !== false &&
@@ -23,21 +21,20 @@ module.exports = async (pluginConfig, {options: {repositoryUrl}, logger}) => {
2321
assets.every(asset => isStringOrStringArray(asset) || (isPlainObject(asset) && isStringOrStringArray(asset.path)))
2422
)
2523
) {
26-
throw new SemanticReleaseError(
27-
'The "assets" options must be an Array of Strings or Objects with a path property.',
28-
'EINVALIDASSETS'
24+
errors.push(
25+
new SemanticReleaseError(
26+
'The "assets" options must be an Array of Strings or Objects with a path property.',
27+
'EINVALIDASSETS'
28+
)
2929
);
3030
}
3131

32-
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
33-
if (!owner || !repo) {
34-
throw new SemanticReleaseError(`The git repository URL is not a valid GitHub URL.`, 'EINVALIDGITURL');
35-
}
36-
3732
if (!isUndefined(successComment) && successComment !== false && !isNonEmptyString(successComment)) {
38-
throw new SemanticReleaseError(
39-
'The "successComment" options, if defined, must be a non empty String.',
40-
'EINVALIDSUCCESSCOMMENT'
33+
errors.push(
34+
new SemanticReleaseError(
35+
'The "successComment" options, if defined, must be a non empty String.',
36+
'EINVALIDSUCCESSCOMMENT'
37+
)
4138
);
4239
}
4340

@@ -47,23 +44,37 @@ module.exports = async (pluginConfig, {options: {repositoryUrl}, logger}) => {
4744
logger.log('Verify GitHub authentication');
4845
}
4946

50-
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
51-
let push;
47+
const {name: repo, owner} = parseGithubUrl(repositoryUrl);
48+
if (!owner || !repo) {
49+
errors.push(new SemanticReleaseError('The git repository URL is not a valid GitHub URL.', 'EINVALIDGITURL'));
50+
}
5251

53-
try {
54-
({data: {permissions: {push}}} = await github.repos.get({repo, owner}));
55-
} catch (err) {
56-
if (err.code === 401) {
57-
throw new SemanticReleaseError('Invalid GitHub token.', 'EINVALIDGHTOKEN');
58-
} else if (err.code === 404) {
59-
throw new SemanticReleaseError(`The repository ${owner}/${repo} doesn't exist.`, 'EMISSINGREPO');
52+
if (githubToken) {
53+
const github = getClient(githubToken, githubUrl, githubApiPathPrefix);
54+
55+
try {
56+
const {data: {permissions: {push}}} = await github.repos.get({repo, owner});
57+
if (!push) {
58+
errors.push(
59+
new SemanticReleaseError(
60+
`The github token doesn't allow to push on the repository ${owner}/${repo}.`,
61+
'EGHNOPERMISSION'
62+
)
63+
);
64+
}
65+
} catch (err) {
66+
if (err.code === 401) {
67+
errors.push(new SemanticReleaseError('Invalid GitHub token.', 'EINVALIDGHTOKEN'));
68+
} else if (err.code === 404) {
69+
errors.push(new SemanticReleaseError(`The repository ${owner}/${repo} doesn't exist.`, 'EMISSINGREPO'));
70+
} else {
71+
throw err;
72+
}
6073
}
61-
throw err;
74+
} else {
75+
errors.push(new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN'));
6276
}
63-
if (!push) {
64-
throw new SemanticReleaseError(
65-
`The github token doesn't allow to push on the repository ${owner}/${repo}.`,
66-
'EGHNOPERMISSION'
67-
);
77+
if (errors.length > 0) {
78+
throw new AggregateError(errors);
6879
}
6980
};

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dependencies": {
1919
"@octokit/rest": "^14.0.9",
2020
"@semantic-release/error": "^2.1.0",
21+
"aggregate-error": "^1.0.0",
2122
"debug": "^3.1.0",
2223
"fs-extra": "^5.0.0",
2324
"globby": "^7.1.1",

test/integration.test.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {stat} from 'fs-extra';
44
import nock from 'nock';
55
import {stub} from 'sinon';
66
import clearModule from 'clear-module';
7-
import SemanticReleaseError from '@semantic-release/error';
87
import {authenticate, upload} from './helpers/mock-github';
98

109
/* eslint camelcase: ["error", {properties: "never"}] */
@@ -92,19 +91,23 @@ test.serial('Verify GitHub auth and assets config', async t => {
9291
});
9392

9493
test.serial('Throw SemanticReleaseError if invalid config', async t => {
95-
process.env.GH_TOKEN = 'github_token';
96-
const owner = 'test_user';
97-
const repo = 'test_repo';
9894
const assets = [{wrongProperty: 'lib/file.js'}];
95+
const successComment = 42;
9996
const options = {
100-
publish: [{path: '@semantic-release/npm'}, {path: '@semantic-release/github', assets}],
101-
repositoryUrl: `git+https://othertesturl.com/${owner}/${repo}.git`,
97+
publish: [{path: '@semantic-release/npm'}, {path: '@semantic-release/github', assets, successComment}],
98+
repositoryUrl: 'invalid_url',
10299
};
103100

104-
const error = await t.throws(t.context.m.verifyConditions({}, {options, logger: t.context.logger}));
101+
const errors = [...(await t.throws(t.context.m.verifyConditions({}, {options, logger: t.context.logger})))];
105102

106-
t.true(error instanceof SemanticReleaseError);
107-
t.is(error.code, 'EINVALIDASSETS');
103+
t.is(errors[0].name, 'SemanticReleaseError');
104+
t.is(errors[0].code, 'EINVALIDASSETS');
105+
t.is(errors[1].name, 'SemanticReleaseError');
106+
t.is(errors[1].code, 'EINVALIDSUCCESSCOMMENT');
107+
t.is(errors[2].name, 'SemanticReleaseError');
108+
t.is(errors[2].code, 'EINVALIDGITURL');
109+
t.is(errors[3].name, 'SemanticReleaseError');
110+
t.is(errors[3].code, 'ENOGHTOKEN');
108111
});
109112

110113
test.serial('Publish a release with an array of assets', async t => {

test/verify.test.js

+21-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import test from 'ava';
22
import nock from 'nock';
33
import {stub} from 'sinon';
4-
import SemanticReleaseError from '@semantic-release/error';
54
import verify from '../lib/verify';
65
import {authenticate} from './helpers/mock-github';
76

@@ -147,29 +146,29 @@ test.serial('Throw SemanticReleaseError if "assets" option is not a String or an
147146
process.env.GITHUB_TOKEN = 'github_token';
148147
const assets = 42;
149148

150-
const error = await t.throws(
149+
const [error] = await t.throws(
151150
verify(
152151
{assets},
153152
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}
154153
)
155154
);
156155

157-
t.true(error instanceof SemanticReleaseError);
156+
t.is(error.name, 'SemanticReleaseError');
158157
t.is(error.code, 'EINVALIDASSETS');
159158
});
160159

161160
test.serial('Throw SemanticReleaseError if "assets" option is an Array with invalid elements', async t => {
162161
process.env.GITHUB_TOKEN = 'github_token';
163162
const assets = ['file.js', 42];
164163

165-
const error = await t.throws(
164+
const [error] = await t.throws(
166165
verify(
167166
{assets},
168167
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}
169168
)
170169
);
171170

172-
t.true(error instanceof SemanticReleaseError);
171+
t.is(error.name, 'SemanticReleaseError');
173172
t.is(error.code, 'EINVALIDASSETS');
174173
});
175174

@@ -257,14 +256,14 @@ test.serial('Throw SemanticReleaseError if "assets" option is an Object missing
257256
process.env.GITHUB_TOKEN = 'github_token';
258257
const assets = {name: 'file.js'};
259258

260-
const error = await t.throws(
259+
const [error] = await t.throws(
261260
verify(
262261
{assets},
263262
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}
264263
)
265264
);
266265

267-
t.true(error instanceof SemanticReleaseError);
266+
t.is(error.name, 'SemanticReleaseError');
268267
t.is(error.code, 'EINVALIDASSETS');
269268
});
270269

@@ -274,24 +273,24 @@ test.serial(
274273
process.env.GITHUB_TOKEN = 'github_token';
275274
const assets = [{path: 'lib/file.js'}, {name: 'file.js'}];
276275

277-
const error = await t.throws(
276+
const [error] = await t.throws(
278277
verify(
279278
{assets},
280279
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}
281280
)
282281
);
283282

284-
t.true(error instanceof SemanticReleaseError);
283+
t.is(error.name, 'SemanticReleaseError');
285284
t.is(error.code, 'EINVALIDASSETS');
286285
}
287286
);
288287

289288
test.serial('Throw SemanticReleaseError for missing github token', async t => {
290-
const error = await t.throws(
289+
const [error] = await t.throws(
291290
verify({}, {options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger})
292291
);
293292

294-
t.true(error instanceof SemanticReleaseError);
293+
t.is(error.name, 'SemanticReleaseError');
295294
t.is(error.code, 'ENOGHTOKEN');
296295
});
297296

@@ -303,21 +302,21 @@ test.serial('Throw SemanticReleaseError for invalid token', async t => {
303302
.get(`/repos/${owner}/${repo}`)
304303
.reply(401);
305304

306-
const error = await t.throws(
305+
const [error] = await t.throws(
307306
verify({}, {options: {repositoryUrl: `https://github.com:${owner}/${repo}.git`}, logger: t.context.logger})
308307
);
309308

310-
t.true(error instanceof SemanticReleaseError);
309+
t.is(error.name, 'SemanticReleaseError');
311310
t.is(error.code, 'EINVALIDGHTOKEN');
312311
t.true(github.isDone());
313312
});
314313

315314
test.serial('Throw SemanticReleaseError for invalid repositoryUrl', async t => {
316315
process.env.GITHUB_TOKEN = 'github_token';
317316

318-
const error = await t.throws(verify({}, {options: {repositoryUrl: 'invalid_url'}, logger: t.context.logger}));
317+
const [error] = await t.throws(verify({}, {options: {repositoryUrl: 'invalid_url'}, logger: t.context.logger}));
319318

320-
t.true(error instanceof SemanticReleaseError);
319+
t.is(error.name, 'SemanticReleaseError');
321320
t.is(error.code, 'EINVALIDGITURL');
322321
});
323322

@@ -329,11 +328,11 @@ test.serial("Throw SemanticReleaseError if token doesn't have the push permissio
329328
.get(`/repos/${owner}/${repo}`)
330329
.reply(200, {permissions: {push: false}});
331330

332-
const error = await t.throws(
331+
const [error] = await t.throws(
333332
verify({}, {options: {repositoryUrl: `https://github.com:${owner}/${repo}.git`}, logger: t.context.logger})
334333
);
335334

336-
t.true(error instanceof SemanticReleaseError);
335+
t.is(error.name, 'SemanticReleaseError');
337336
t.is(error.code, 'EGHNOPERMISSION');
338337
t.true(github.isDone());
339338
});
@@ -346,11 +345,11 @@ test.serial("Throw SemanticReleaseError if the repository doesn't exist", async
346345
.get(`/repos/${owner}/${repo}`)
347346
.reply(404);
348347

349-
const error = await t.throws(
348+
const [error] = await t.throws(
350349
verify({}, {options: {repositoryUrl: `https://github.com:${owner}/${repo}.git`}, logger: t.context.logger})
351350
);
352351

353-
t.true(error instanceof SemanticReleaseError);
352+
t.is(error.name, 'SemanticReleaseError');
354353
t.is(error.code, 'EMISSINGREPO');
355354
t.true(github.isDone());
356355
});
@@ -372,9 +371,8 @@ test.serial('Throw error if github return any other errors', async t => {
372371
});
373372

374373
test('Throw SemanticReleaseError if "successComment" option is not a String', async t => {
375-
process.env.GITHUB_TOKEN = 'github_token';
376374
const successComment = 42;
377-
const error = await t.throws(
375+
const [error] = await t.throws(
378376
verify(
379377
{successComment},
380378
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}
@@ -386,9 +384,8 @@ test('Throw SemanticReleaseError if "successComment" option is not a String', as
386384
});
387385

388386
test('Throw SemanticReleaseError if "successComment" option is an empty String', async t => {
389-
process.env.GITHUB_TOKEN = 'github_token';
390387
const successComment = '';
391-
const error = await t.throws(
388+
const [error] = await t.throws(
392389
verify(
393390
{successComment},
394391
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}
@@ -400,9 +397,8 @@ test('Throw SemanticReleaseError if "successComment" option is an empty String',
400397
});
401398

402399
test('Throw SemanticReleaseError if "successComment" option is a whitespace String', async t => {
403-
process.env.GITHUB_TOKEN = 'github_token';
404400
const successComment = ' \n \r ';
405-
const error = await t.throws(
401+
const [error] = await t.throws(
406402
verify(
407403
{successComment},
408404
{options: {repositoryUrl: 'https://github.com/semantic-release/github.git'}, logger: t.context.logger}

0 commit comments

Comments
 (0)