Compare commits

...
5 Commits
10 changed files with 180 additions and 47 deletions
+4 -3
View File
@@ -15,9 +15,9 @@
}
},
"dependencies": {
"@semantic-release/commit-analyzer": "^3.0.1",
"@semantic-release/commit-analyzer": "^4.0.0",
"@semantic-release/condition-travis": "^6.0.0",
"@semantic-release/error": "^2.0.0",
"@semantic-release/error": "^2.1.0",
"@semantic-release/last-release-npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^5.0.0",
"chalk": "^2.3.0",
@@ -25,7 +25,9 @@
"debug": "^3.1.0",
"execa": "^0.8.0",
"fs-extra": "^4.0.2",
"get-stream": "^3.0.0",
"git-head": "^1.2.1",
"git-log-parser": "^1.2.0",
"github": "^12.0.0",
"lodash": "^4.0.0",
"marked": "^0.3.6",
@@ -52,7 +54,6 @@
"eslint-plugin-prettier": "^2.3.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"get-stream": "^3.0.0",
"mockserver-client": "^1.0.16",
"nock": "^9.0.2",
"npm-registry-couchapp": "^2.6.12",
+2 -3
View File
@@ -1,5 +1,4 @@
const program = require('commander');
const SemanticReleaseError = require('@semantic-release/error');
const logger = require('./lib/logger');
function list(values) {
@@ -49,11 +48,11 @@ module.exports = async () => {
} catch (err) {
// If error is a SemanticReleaseError then it's an expected exception case (no release to be done, running on a PR etc..) and the cli will return with 0
// Otherwise it's an unexpected error (configuration issue, code issue, plugin issue etc...) and the cli will return 1
if (err instanceof SemanticReleaseError) {
if (err.semanticRelease) {
logger.log(`%s ${err.message}`, err.code);
} else {
process.exitCode = 1;
logger.error(err);
logger.error('An error occurred while running semantic-release: %O', err);
}
}
};
+13 -22
View File
@@ -1,4 +1,6 @@
const execa = require('execa');
const gitLogParser = require('git-log-parser');
const getStream = require('get-stream');
const debug = require('debug')('semantic-release:get-commits');
const getVersionHead = require('./get-version-head');
const {debugShell} = require('./debug');
@@ -65,28 +67,17 @@ module.exports = async ({version, gitHead}, branch) => {
debugShell('Unshallow repo', shell, debug);
}
try {
const shell = await execa('git', [
'log',
'--format=format:%H==SPLIT==%B==END==',
`${gitHead ? gitHead + '..' : ''}HEAD`,
]);
debugShell('Get commits', shell, debug);
const commits = shell.stdout
.split('==END==')
.filter(raw => !!raw.trim())
.map(raw => {
const [hash, message] = raw.trim().split('==SPLIT==');
return {hash, message};
});
logger.log('Found %s commits since last release', commits.length);
debug('Parsed commits: %o', commits);
return {commits, lastRelease: {version, gitHead}};
} catch (err) {
debug(err);
logger.log('Found no commit since last release');
return {commits: [], lastRelease: {version, gitHead}};
}
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
const commits = (await getStream.array(
gitLogParser.parse({_: `${gitHead ? gitHead + '..' : ''}HEAD`})
)).map(commit => {
commit.message = commit.message.trim();
commit.gitTags = commit.gitTags.trim();
return commit;
});
logger.log('Found %s commits since last release', commits.length);
debug('Parsed commits: %o', commits);
return {commits, lastRelease: {version, gitHead}};
};
function noGitHeadMessage(branch, version) {
+8 -3
View File
@@ -6,13 +6,18 @@ const chalk = require('chalk');
module.exports = {
log(...args) {
const [format, ...rest] = args;
console.log(`${chalk.grey('[Semantic release]:')} ${format}`, ...rest.map(arg => chalk.magenta(arg)));
console.log(
`${chalk.grey('[Semantic release]:')}${typeof format === 'string'
? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}`
: ''}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
error(...args) {
const [format, ...rest] = args;
console.error(
`${chalk.grey('[Semantic release]:')} ${chalk.red(format instanceof Error ? format.stack : format)}`,
...rest.map(arg => chalk.red(arg instanceof Error ? arg.stack : arg))
`${chalk.grey('[Semantic release]:')}${typeof format === 'string' ? ` ${chalk.red(format)}` : ''}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
};
+3 -1
View File
@@ -1,3 +1,5 @@
module.exports = function(config, options, cb) {
cb(new Error('a'));
const error = new Error('a');
error.errorProperty = 'errorProperty';
cb(error);
};
+14
View File
@@ -0,0 +1,14 @@
const SemanticReleaseError = require('@semantic-release/error');
class InheritedError extends SemanticReleaseError {
constructor(message, code, newProperty) {
super(message);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.code = code;
}
}
module.exports = function(config, options, cb) {
cb(new InheritedError('Inherited error', 'EINHERITED'));
};
+82
View File
@@ -42,14 +42,48 @@ test.serial('Get all commits when there is no last release', async t => {
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.falsy(result.lastRelease.gitHead);
t.falsy(result.lastRelease.version);
});
test.serial('Get all commits with gitTags', 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);
// Retrieve the commits with the commits module
const result = await t.context.getCommits({}, 'master');
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[0].gitTags, '(HEAD -> master)');
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
t.is(result.commits[1].gitTags, '(tag: v1.0.0)');
});
test.serial('Get all commits when there is no last release, including the ones not in the shallow clone', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
@@ -68,8 +102,14 @@ test.serial('Get all commits when there is no last release, including the ones n
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.falsy(result.lastRelease.gitHead);
@@ -89,8 +129,14 @@ test.serial('Get all commits since gitHead (from lastRelease)', async t => {
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead, commits[commits.length - 1].hash);
@@ -112,6 +158,9 @@ test.serial('Get all commits since gitHead (from lastRelease) on a detached head
t.is(result.commits.length, 1);
t.is(result.commits[0].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[0].message, commits[1].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead, commits[commits.length - 1].hash);
@@ -135,8 +184,14 @@ test.serial('Get all commits since gitHead (from tag) ', async t => {
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
@@ -162,6 +217,9 @@ test.serial('Get all commits since gitHead (from tag) on a detached head repo',
t.is(result.commits.length, 1);
t.is(result.commits[0].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[0].message, commits[1].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
@@ -185,8 +243,14 @@ test.serial('Get all commits since gitHead (from tag formatted like v<version>)
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
@@ -210,8 +274,14 @@ test.serial('Get commits when last release gitHead is missing but a tag match th
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
@@ -233,8 +303,14 @@ test.serial('Get all commits since gitHead, when gitHead are mising from the sha
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
@@ -263,8 +339,14 @@ test.serial('Get all commits since gitHead from tag, when tags are mising from t
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
+31 -3
View File
@@ -17,6 +17,7 @@ const env = {
const cli = require.resolve('../bin/semantic-release');
const noop = require.resolve('../src/lib/plugin-noop');
const pluginError = require.resolve('./fixtures/plugin-error-a');
const pluginInheritedError = require.resolve('./fixtures/plugin-error-inherited');
test.before(async t => {
await mockServer.start();
@@ -35,7 +36,6 @@ test.beforeEach(async t => {
});
test.afterEach.always(async t => {
console.log();
// Restore process.env
process.env = Object.assign({}, t.context.env);
// Restore the current working directory
@@ -574,7 +574,7 @@ test.serial('Run via JS API', async t => {
await semanticRelease({githubToken, verifyConditions: [noop, noop], debug: true});
t.true(t.context.log.calledWithMatch(/Published Github release: /, new RegExp(`release-url/${version}`)));
t.true(t.context.log.calledWithMatch(/Publishing version %s to npm registry %s/, version, registry.uri));
t.true(t.context.log.calledWithMatch(/Publishing version .* to npm registry/, version, registry.uri));
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
@@ -615,7 +615,7 @@ test.serial('Returns and error code if NPM token is invalid', async t => {
t.is(code, 1);
});
test.serial('Log unexpected errors from plugins', async t => {
test.serial('Log unexpected errors from plugins and exit with 1', async t => {
const packageName = 'test-module-9';
const repo = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
@@ -634,11 +634,39 @@ test.serial('Log unexpected errors from plugins', async t => {
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stderr, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stderr, /Error: a/);
// Verify the the stacktrace is logged
t.regex(stderr, new RegExp(pluginError));
// Verify the Error properties are logged
t.regex(stderr, /errorProperty: 'errorProperty'/);
t.is(code, 1);
});
test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async t => {
const packageName = 'test-module-10';
const repo = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {githubUrl: mockServer.url, verifyConditions: pluginInheritedError},
});
/** Initial release **/
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stdout, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stdout, /EINHERITED Inherited error/);
t.is(code, 0);
});
test.serial('CLI returns error code and prints help if called with a command', async t => {
t.log('$ semantic-release pre');
let {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
+19 -8
View File
@@ -1,5 +1,5 @@
import test from 'ava';
import {stub} from 'sinon';
import {stub, match} from 'sinon';
import logger from '../src/lib/logger';
test.beforeEach(t => {
@@ -20,18 +20,29 @@ test.serial('Basic log', t => {
t.true(t.context.error.calledWithMatch(/.*test error/));
});
test.serial('Log object', t => {
const obj = {a: 1, b: '2'};
logger.log(obj);
logger.error(obj);
t.true(t.context.log.calledWithMatch(match.string, obj));
t.true(t.context.error.calledWithMatch(match.string, obj));
});
test.serial('Log with string formatting', t => {
logger.log('test log %s', 'log value');
logger.error('test error %s', 'error value');
t.true(t.context.log.calledWithMatch(/.*test log %s/, 'log value'));
t.true(t.context.error.calledWithMatch(/.*test error %s/, 'error value'));
t.true(t.context.log.calledWithMatch(/.*test log/, 'log value'));
t.true(t.context.error.calledWithMatch(/.*test error/, 'error value'));
});
test.serial('Log with error stacktrace', t => {
logger.error(new Error('error message'));
logger.error('test error %s', new Error('other error message'));
test.serial('Log with error stacktrace and properties', t => {
const error = new Error('error message');
logger.error(error);
const otherError = new Error('other error message');
logger.error('test error %O', otherError);
t.true(t.context.error.calledWithMatch(/.*test error %s/, /Error: other error message(\s|.)*?logger\.test\.js/));
t.true(t.context.error.calledWithMatch(/Error: error message(\s|.)*?logger\.test\.js/));
t.true(t.context.error.calledWithMatch(match.string, error));
t.true(t.context.error.calledWithMatch(/.*test error/, otherError));
});
+4 -4
View File
@@ -2,12 +2,12 @@ import test from 'ava';
import SemanticReleaseError from '@semantic-release/error';
import verify from '../src/lib/verify-pkg';
test.only('Verify name and repository', t => {
test('Verify name and repository', t => {
// Call the verify module with package
t.notThrows(() => verify({name: 'package', repository: {url: 'http://github.com/whats/up.git'}}));
});
test.only('Return error for missing package name', t => {
test('Return error for missing package name', t => {
// Call the verify module with package
const error = t.throws(() => verify({repository: {url: 'http://github.com/whats/up.git'}}));
// Verify error code and type
@@ -15,7 +15,7 @@ test.only('Return error for missing package name', t => {
t.true(error instanceof SemanticReleaseError);
});
test.only('Return error for missing repository', t => {
test('Return error for missing repository', t => {
// Call the verify module with package
const error = t.throws(() => verify({name: 'package'}));
// Verify error code and type
@@ -23,7 +23,7 @@ test.only('Return error for missing repository', t => {
t.true(error instanceof SemanticReleaseError);
});
test.only('Return error for missing repository url', t => {
test('Return error for missing repository url', t => {
// Call the verify module with package
const error = t.throws(() => verify({name: 'package', repository: {}}));
// Verify error code and type