From cdb98f919f80134b91a2798cb8dedd61c531c604 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Fri, 26 Jan 2018 02:56:49 -0500 Subject: [PATCH] feat: log all verification errors --- cli.js | 6 ---- index.js | 25 ++++++++++--- lib/plugins/pipeline.js | 24 ++++++++++--- package.json | 2 ++ test/index.test.js | 65 +++++++++++++++++++++++++++++++++ test/plugins/pipeline.test.js | 68 ++++++++++++++++++++++++++++++++--- 6 files changed, 171 insertions(+), 19 deletions(-) diff --git a/cli.js b/cli.js index 70f24901..32ea6d79 100755 --- a/cli.js +++ b/cli.js @@ -1,6 +1,5 @@ const program = require('commander'); const {pickBy, isUndefined} = require('lodash'); -const logger = require('./lib/logger'); function list(values) { return values.split(',').map(value => value.trim()); @@ -56,10 +55,5 @@ module.exports = async () => { } } catch (err) { process.exitCode = 1; - if (err.semanticRelease) { - logger.log(`%s ${err.message}`, err.code); - } else { - logger.error('An error occurred while running semantic-release: %O', err); - } } }; diff --git a/index.js b/index.js index bf9e4cfa..2d22841e 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const getCommits = require('./lib/get-commits'); const logger = require('./lib/logger'); const {gitHead: getGitHead, isGitRepo} = require('./lib/git'); -module.exports = async opts => { +async function run(opts) { const {isCi, branch, isPr} = envCi(); const config = await getConfig(opts, logger); const {plugins, options} = config; @@ -39,7 +39,7 @@ module.exports = async opts => { logger.log('Run automated release from branch %s', options.branch); logger.log('Call plugin %s', 'verify-conditions'); - await plugins.verifyConditions({options, logger}); + await plugins.verifyConditions({options, logger}, true); logger.log('Call plugin %s', 'get-last-release'); const {commits, lastRelease} = await getCommits( @@ -63,7 +63,7 @@ module.exports = async opts => { const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`}; logger.log('Call plugin %s', 'verify-release'); - await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}); + await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, true); const generateNotesParam = {options, logger, lastRelease, commits, nextRelease}; @@ -78,7 +78,7 @@ module.exports = async opts => { nextRelease.notes = await plugins.generateNotes(generateNotesParam); logger.log('Call plugin %s', 'publish'); - await plugins.publish({options, logger, lastRelease, commits, nextRelease}, async prevInput => { + await plugins.publish({options, logger, lastRelease, commits, nextRelease}, false, async prevInput => { const newGitHead = await getGitHead(); // If previous publish plugin has created a commit (gitHead changed) if (prevInput.nextRelease.gitHead !== newGitHead) { @@ -93,4 +93,21 @@ module.exports = async opts => { logger.log('Published release: %s', nextRelease.version); } return true; +} + +module.exports = async opts => { + try { + const result = await run(opts); + return result; + } catch (err) { + const errors = err.name === 'AggregateError' ? Array.from(err).sort(error => !error.semanticRelease) : [err]; + for (const error of errors) { + if (error.semanticRelease) { + logger.log(`%s ${error.message}`, error.code); + } else { + logger.error('An error occurred while running semantic-release: %O', error); + } + } + throw err; + } }; diff --git a/lib/plugins/pipeline.js b/lib/plugins/pipeline.js index 10ce2720..ee4e8183 100644 --- a/lib/plugins/pipeline.js +++ b/lib/plugins/pipeline.js @@ -1,19 +1,33 @@ const {identity} = require('lodash'); +const pReflect = require('p-reflect'); const pReduce = require('p-reduce'); +const AggregateError = require('aggregate-error'); -module.exports = steps => async (input, getNextInput = identity) => { +module.exports = steps => async (input, settleAll = false, getNextInput = identity) => { const results = []; + const errors = []; await pReduce( steps, async (prevResult, nextStep) => { - // Call the next step with the input computed at the end of the previous iteration - const result = await nextStep(prevResult); - // Save intermediary result - results.push(result); + let result; + + // Call the next step with the input computed at the end of the previous iteration and save intermediary result + if (settleAll) { + const {isFulfilled, value, reason} = await pReflect(nextStep(prevResult)); + result = isFulfilled ? value : reason; + (isFulfilled ? results : errors).push(result); + } else { + result = await nextStep(prevResult); + results.push(result); + } + // Prepare input for next step, passing the result of the previous iteration and the current one return getNextInput(prevResult, result); }, input ); + if (errors.length > 0) { + throw new AggregateError(errors); + } return results; }; diff --git a/package.json b/package.json index f6570c22..2961c872 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@semantic-release/github": "^3.0.1", "@semantic-release/npm": "^2.0.0", "@semantic-release/release-notes-generator": "^6.0.0", + "aggregate-error": "^1.0.0", "chalk": "^2.3.0", "commander": "^2.11.0", "cosmiconfig": "^4.0.0", @@ -36,6 +37,7 @@ "marked": "^0.3.9", "marked-terminal": "^2.0.0", "p-reduce": "^1.0.0", + "p-reflect": "^1.0.0", "read-pkg-up": "^3.0.0", "resolve-from": "^4.0.0", "semver": "^5.4.1" diff --git a/test/index.test.js b/test/index.test.js index 6bca7ccf..8436aed7 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -2,6 +2,7 @@ import test from 'ava'; import proxyquire from 'proxyquire'; import {stub} from 'sinon'; import tempy from 'tempy'; +import SemanticReleaseError from '@semantic-release/error'; import DEFINITIONS from '../lib/plugins/definitions'; import {gitHead as getGitHead} from '../lib/git'; import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils'; @@ -161,6 +162,70 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes})); }); +test.serial('Log all "verifyConditions" errors', async t => { + // Create a git repository, set the current working directory at the root of the repo + await gitRepo(); + // Add commits to the master branch + await gitCommits(['First']); + + const error1 = new Error('error 1'); + const error2 = new SemanticReleaseError('error 2', 'ERR2'); + const error3 = new SemanticReleaseError('error 3', 'ERR3'); + const options = { + branch: 'master', + repositoryUrl: 'git@hostname.com:owner/module.git', + verifyConditions: [stub().rejects(error1), stub().rejects(error2), stub().rejects(error3)], + }; + + const semanticRelease = proxyquire('..', { + './lib/logger': t.context.logger, + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + const errors = await t.throws(semanticRelease(options)); + + t.deepEqual(Array.from(errors), [error1, error2, error3]); + t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']); + t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 3', 'ERR3']); + t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [ + 'An error occurred while running semantic-release: %O', + error1, + ]); + t.true(t.context.error.calledAfter(t.context.log)); +}); + +test.serial('Log all "verifyRelease" errors', async t => { + // Create a git repository, set the current working directory at the root of the repo + await gitRepo(); + // Add commits to the master branch + let commits = await gitCommits(['First']); + // Create the tag corresponding to version 1.0.0 + await gitTagVersion('v1.0.0'); + // Add new commits to the master branch + commits = (await gitCommits(['Second'])).concat(commits); + + const error1 = new SemanticReleaseError('error 1', 'ERR1'); + const error2 = new SemanticReleaseError('error 2', 'ERR2'); + const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; + const options = { + branch: 'master', + repositoryUrl: 'git@hostname.com:owner/module.git', + verifyConditions: stub().resolves(), + getLastRelease: stub().resolves(lastRelease), + analyzeCommits: stub().resolves('major'), + verifyRelease: [stub().rejects(error1), stub().rejects(error2)], + }; + + const semanticRelease = proxyquire('..', { + './lib/logger': t.context.logger, + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + const errors = await t.throws(semanticRelease(options)); + + t.deepEqual(Array.from(errors), [error1, error2]); + t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']); + t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']); +}); + test.serial('Dry-run skips publish', async t => { // Create a git repository, set the current working directory at the root of the repo await gitRepo(); diff --git a/test/plugins/pipeline.test.js b/test/plugins/pipeline.test.js index 3b35633c..395b42ab 100644 --- a/test/plugins/pipeline.test.js +++ b/test/plugins/pipeline.test.js @@ -12,24 +12,50 @@ test('Execute each function in series passing the same input', async t => { t.true(step1.calledWith(0)); t.true(step2.calledWith(0)); t.true(step3.calledWith(0)); + + t.true(step1.calledBefore(step2)); + t.true(step2.calledBefore(step3)); }); test('Execute each function in series passing a transformed input', async t => { const step1 = stub().resolves(1); const step2 = stub().resolves(2); const step3 = stub().resolves(3); + const step4 = stub().resolves(4); - const result = await pipeline([step1, step2, step3])(0, (prevResult, result) => prevResult + result); + const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => prevResult + result); - t.deepEqual(result, [1, 2, 3]); + t.deepEqual(result, [1, 2, 3, 4]); + t.true(step1.calledWith(0)); + t.true(step2.calledWith(0 + 1)); + t.true(step3.calledWith(0 + 1 + 2)); + t.true(step4.calledWith(0 + 1 + 2 + 3)); + t.true(step1.calledBefore(step2)); + t.true(step2.calledBefore(step3)); + t.true(step3.calledBefore(step4)); +}); + +test('Execute each function in series passing the result of the previous one', async t => { + const step1 = stub().resolves(1); + const step2 = stub().resolves(2); + const step3 = stub().resolves(3); + const step4 = stub().resolves(4); + + const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => result); + + t.deepEqual(result, [1, 2, 3, 4]); t.true(step1.calledWith(0)); t.true(step2.calledWith(1)); - t.true(step3.calledWith(3)); + t.true(step3.calledWith(2)); + t.true(step4.calledWith(3)); + t.true(step1.calledBefore(step2)); + t.true(step2.calledBefore(step3)); + t.true(step3.calledBefore(step4)); }); test('Stop execution and throw error is a step rejects', async t => { const step1 = stub().resolves(1); - const step2 = stub().throws(new Error('test error')); + const step2 = stub().rejects(new Error('test error')); const step3 = stub().resolves(3); const error = await t.throws(pipeline([step1, step2, step3])(0), Error); @@ -38,3 +64,37 @@ test('Stop execution and throw error is a step rejects', async t => { t.true(step2.calledWith(0)); t.true(step3.notCalled); }); + +test('Execute all even if a Promise rejects', async t => { + const error1 = new Error('test error 1'); + const error2 = new Error('test error 2'); + const step1 = stub().resolves(1); + const step2 = stub().rejects(error1); + const step3 = stub().rejects(error2); + + const errors = await t.throws(pipeline([step1, step2, step3])(0, true)); + + t.deepEqual(Array.from(errors), [error1, error2]); + t.true(step1.calledWith(0)); + t.true(step2.calledWith(0)); + t.true(step3.calledWith(0)); +}); + +test('Execute each function in series passing a transformed input even if a Promise rejects', async t => { + const error2 = new Error('test error 2'); + const error3 = new Error('test error 3'); + const step1 = stub().resolves(1); + const step2 = stub().rejects(error2); + const step3 = stub().rejects(error3); + const step4 = stub().resolves(4); + + const errors = await t.throws( + pipeline([step1, step2, step3, step4])(0, true, (prevResult, result) => prevResult + result) + ); + + t.deepEqual(Array.from(errors), [error2, error3]); + t.true(step1.calledWith(0)); + t.true(step2.calledWith(0 + 1)); + t.true(step3.calledWith(0 + 1 + error2)); + t.true(step4.calledWith(0 + 1 + error2 + error3)); +});