Compare commits

...
8 Commits
Author SHA1 Message Date
Pierre Vanduynslager c2beb643fa feat: add the prepare plugin hook
BREAKING CHANGE: Committing or creating files in the `publish` plugin hook is not supported anymore and now must be done in the `prepare` hook

Plugins with a `publish` hook that makes a commit or create a file that can be committed must use the `prepare` hook.
2018-02-19 00:28:50 -05:00
Pierre Vanduynslager 20246c02b1 fix: exclude empty env var value from replacement 2018-02-19 00:01:06 -05:00
Pierre Vanduynslager 857d4180e9 fix: allow boolean option to be set in config file 2018-02-16 00:51:58 -05:00
Pierre Vanduynslager 4d0490122c style: lint 2018-02-15 23:19:57 -05:00
Pierre Vanduynslager aa724e80f7 chore(package): simplify xo configuration 2018-02-15 23:19:57 -05:00
greenkeeper[bot]andPierre Vanduynslager ce15b75edf chore(package): update xo to version 0.20.0 2018-02-15 23:19:57 -05:00
Pierre VanduynslagerandGregor Martynus 1966f0e3e2 fix: verify branch first 2018-02-15 18:35:37 +00:00
Pierre Vanduynslager 305f4ee8eb fix: do not transform repositoryUrl if it allow to push
Even the user set Git credentials via environment variable, use the configured URL (with authentication) if it works.
This allow users to push tags and commits via ssh while still using the GitHub/GitLab API.
2018-02-14 10:45:44 -05:00
22 changed files with 203 additions and 139 deletions
+1
View File
@@ -86,6 +86,7 @@ After running the tests the command `semantic-release` will execute the followin
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
| Create Git tag | Create a Git tag corresponding the new release version |
| Prepare | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin). |
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
| Notify | Notify of new releases or errors with the [success](docs/usage/plugins.md#success-plugin) and [fail](docs/usage/plugins.md#fail-plugin) plugins. |
+10 -8
View File
@@ -20,23 +20,26 @@ Usage:
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', default: true, type: 'boolean', group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', default: undefined, type: 'boolean', group: 'Options'})
.option('verify-conditions', {...stringList, group: 'Plugins'})
.option('analyze-commits', {type: 'string', group: 'Plugins'})
.option('verify-release', {...stringList, group: 'Plugins'})
.option('generate-notes', {type: 'string', group: 'Plugins'})
.option('prepare', {...stringList, group: 'Plugins'})
.option('publish', {...stringList, group: 'Plugins'})
.option('success', {...stringList, group: 'Plugins'})
.option('fail', {...stringList, group: 'Plugins'})
.option('debug', {describe: 'Output debugging information', default: false, type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: false, type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', group: 'Options'})
.option('v', {alias: 'version', group: 'Options'})
.option('debug', {describe: 'Output debugging information', default: undefined, type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: undefined, type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', default: undefined, group: 'Options'})
.option('v', {alias: 'version', default: undefined, group: 'Options'})
.strict(false)
.exitProcess(false);
try {
const {help, version, ...opts} = cli.argv;
// Remove option with undefined values, as yargs sets non defined options as `undefined`
const {help, version, ...opts} = pickBy(cli.argv, value => !isUndefined(value));
if (Boolean(help) || Boolean(version)) {
process.exitCode = 0;
return;
@@ -52,8 +55,7 @@ Usage:
require('debug').enable('semantic-release:*');
}
// Remove option with undefined values, as yargs sets non defined options as `undefined`
await require('.')(pickBy(opts, value => !isUndefined(value)));
await require('.')(opts);
process.exitCode = 0;
} catch (err) {
if (err.name !== 'YError') {
+4 -2
View File
@@ -9,6 +9,7 @@
- [fail](https://github.com/semantic-release/github#fail): Open a GitHub issue when a release fails
- [@semantic-release/npm](https://github.com/semantic-release/npm)
- [verifyConditions](https://github.com/semantic-release/npm#verifyconditions): Verify the presence and the validity of the npm authentication and release configuration
- [prepare](https://github.com/semantic-release/npm#prepare): Update the package.json version and create the npm package tarball
- [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry
## Official plugins
@@ -18,15 +19,16 @@
- [publish](https://github.com/semantic-release/gitlab#publish): Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
- [@semantic-release/git](https://github.com/semantic-release/git)
- [verifyConditions](https://github.com/semantic-release/git#verifyconditions): Verify the presence and the validity of the Git authentication and release configuration
- [publish](https://github.com/semantic-release/git#publish): Push a release commit and tag, including configurable files
- [prepare](https://github.com/semantic-release/git#prepare): Push a release commit and tag, including configurable files
- [@semantic-release/changelog](https://github.com/semantic-release/changelog)
- [verifyConditions](https://github.com/semantic-release/changelog#verifyconditions): Verify the presence and the validity of the configuration
- [publish](https://github.com/semantic-release/changelog#publish): Create or update the changelog file in the local project repository
- [prepare](https://github.com/semantic-release/changelog#prepare): Create or update the changelog file in the local project repository
- [@semantic-release/exec](https://github.com/semantic-release/exec)
- [verifyConditions](https://github.com/semantic-release/exec#verifyconditions): Execute a shell command to verify if the release should happen
- [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release
- [verifyRelease](https://github.com/semantic-release/exec#verifyrelease): Execute a shell command to verifying a release that was determined before and is about to be published.
- [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note
- [prepare](https://github.com/semantic-release/exec#prepare): Execute a shell command to prepare the release
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release
- [success](https://github.com/semantic-release/exec#success): Execute a shell command to notify of a new release
- [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
+12
View File
@@ -155,6 +155,18 @@ Define the [generate notes plugin](plugins.md#generatenotes-plugin).
See [Plugins configuration](plugins.md#configuration) for more details.
### prepare
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/npm']`
CLI argument: `--prepare`
Define the list of [prepare plugins](plugins.md#prepare-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### publish
Type: `Array`, `String`, `Object`
+8
View File
@@ -28,6 +28,14 @@ Plugin responsible for generating release notes.
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
### prepare plugin
Plugin responsible for preparing the release, including:
- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
- Create and push commits
Default implementation: [npm](https://github.com/semantic-release/npm#prepare).
### publish plugin
Plugin responsible for publishing the release.
+26 -21
View File
@@ -12,7 +12,7 @@ const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const {extractErrors} = require('./lib/utils');
const logger = require('./lib/logger');
const {unshallow, gitHead: getGitHead, tag, push, deleteTag} = require('./lib/git');
const {unshallow, gitHead: getGitHead, tag, push} = require('./lib/git');
marked.setOptions({renderer: new TerminalRenderer()});
@@ -29,12 +29,19 @@ async function run(options, plugins) {
return;
}
if (!await verify(options, branch, logger)) {
return;
if (branch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version won’t be published.`
);
return false;
}
logger.log('Run automated release from branch %s', options.branch);
await verify(options);
logger.log('Run automated release from branch %s', options.branch);
console.log(options);
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger}, {settleAll: true});
@@ -72,26 +79,14 @@ async function run(options, plugins) {
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
const releases = await plugins.publish(
logger.log('Call plugin %s', 'prepare');
await plugins.prepare(
{options, logger, lastRelease, commits, nextRelease},
{
getNextInput: async lastResult => {
const newGitHead = await getGitHead();
// If previous publish plugin has created a commit (gitHead changed)
// If previous prepare plugin has created a commit (gitHead changed)
if (lastResult.nextRelease.gitHead !== newGitHead) {
// Delete the previously created tag
await deleteTag(options.repositoryUrl, nextRelease.gitTag);
// Recreate the tag, referencing the new gitHead
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
nextRelease.gitHead = newGitHead;
// Regenerate the release notes
logger.log('Call plugin %s', 'generateNotes');
@@ -100,11 +95,21 @@ async function run(options, plugins) {
// Call the next publish plugin with the updated `nextRelease`
return {options, logger, lastRelease, commits, nextRelease};
},
// Add nextRelease and plugin properties to published release
transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step}),
}
);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
const releases = await plugins.publish(
{options, logger, lastRelease, commits, nextRelease},
// Add nextRelease and plugin properties to published release
{transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step})}
);
await plugins.success(
{options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
{settleAll: true}
+6
View File
@@ -36,6 +36,12 @@ module.exports = {
error: 'ERELEASENOTESOUTPUT',
},
},
prepare: {
default: ['@semantic-release/npm'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
},
publish: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
+4 -2
View File
@@ -3,7 +3,7 @@ const readPkgUp = require('read-pkg-up');
const cosmiconfig = require('cosmiconfig');
const resolveFrom = require('resolve-from');
const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
const {repoUrl, verifyAuth} = require('./git');
const PLUGINS_DEFINITIONS = require('./definitions/plugins');
const plugins = require('./plugins');
const getGitAuthUrl = require('./get-git-auth-url');
@@ -51,7 +51,9 @@ module.exports = async (opts, logger) => {
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
};
options.repositoryUrl = options.repositoryUrl ? getGitAuthUrl(options.repositoryUrl) : options.repositoryUrl;
if (!await verifyAuth(options.repositoryUrl, options.branch)) {
options.repositoryUrl = options.repositoryUrl ? getGitAuthUrl(options.repositoryUrl) : options.repositoryUrl;
}
debug('options values: %O', options);
-17
View File
@@ -115,22 +115,6 @@ async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
}
/**
* Delete a tag locally and remotely.
*
* @param {String} origin The remote repository URL.
* @param {String} tagName The tag name to delete.
*/
async function deleteTag(origin, tagName) {
// Delete the local tag
let shell = await execa('git', ['tag', '-d', tagName], {reject: false});
debug('delete local tag', shell);
// Delete the tag remotely
shell = await execa('git', ['push', '--delete', origin, tagName], {reject: false});
debug('delete remote tag', shell);
}
/**
* Verify a tag name is a valid Git reference.
*
@@ -157,6 +141,5 @@ module.exports = {
verifyAuth,
tag,
push,
deleteTag,
verifyTagName,
};
+4 -1
View File
@@ -1,6 +1,9 @@
const {escapeRegExp} = require('lodash');
const toReplace = Object.keys(process.env).filter(envVar => /token|password|credential|secret|private/i.test(envVar));
const toReplace = Object.keys(process.env).filter(
envVar => /token|password|credential|secret|private/i.test(envVar) && process.env[envVar].trim()
);
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(process.env[envVar])).join('|'), 'g');
module.exports = output => {
+1 -12
View File
@@ -3,7 +3,7 @@ const AggregateError = require('aggregate-error');
const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
const getError = require('./get-error');
module.exports = async (options, branch, logger) => {
module.exports = async options => {
const errors = [];
if (!await isGitRepo()) {
@@ -29,15 +29,4 @@ module.exports = async (options, branch, logger) => {
if (errors.length > 0) {
throw new AggregateError(errors);
}
if (branch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version won’t be published.`
);
return false;
}
return true;
};
+5 -18
View File
@@ -22,7 +22,7 @@
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/error": "^2.2.0",
"@semantic-release/github": "^4.1.0",
"@semantic-release/npm": "^3.1.0",
"@semantic-release/npm": "^3.2.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"aggregate-error": "^1.0.0",
"chalk": "^2.3.0",
@@ -53,8 +53,6 @@
"cz-conventional-changelog": "^2.0.0",
"delay": "^2.0.0",
"dockerode": "^2.5.2",
"eslint-config-prettier": "^2.5.0",
"eslint-plugin-prettier": "^2.3.0",
"file-url": "^2.0.2",
"fs-extra": "^5.0.0",
"got": "^8.0.0",
@@ -63,11 +61,10 @@
"nock": "^9.0.2",
"nyc": "^11.2.1",
"p-retry": "^1.0.0",
"prettier": "~1.10.0",
"proxyquire": "^1.8.0",
"sinon": "^4.0.0",
"tempy": "^0.2.1",
"xo": "^0.18.2"
"xo": "^0.20.0"
},
"engines": {
"node": ">=8.3"
@@ -107,10 +104,7 @@
"all": true
},
"prettier": {
"printWidth": 120,
"singleQuote": true,
"bracketSpacing": false,
"trailingComma": "es5"
"printWidth": 120
},
"publishConfig": {
"tag": "next"
@@ -131,14 +125,7 @@
"test": "nyc ava -v"
},
"xo": {
"extends": [
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
"prettier": true,
"space": true
}
}
+20
View File
@@ -53,6 +53,9 @@ test.serial('Pass options to semantic-release API', async t => {
'verify2',
'--generate-notes',
'notes',
'--prepare',
'prepare1',
'prepare2',
'--publish',
'publish1',
'publish2',
@@ -76,6 +79,7 @@ test.serial('Pass options to semantic-release API', async t => {
t.is(run.args[0][0].analyzeCommits, 'analyze');
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']);
t.is(run.args[0][0].generateNotes, 'notes');
t.deepEqual(run.args[0][0].prepare, ['prepare1', 'prepare2']);
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
t.deepEqual(run.args[0][0].success, ['success1', 'success2']);
t.deepEqual(run.args[0][0].fail, ['fail1', 'fail2']);
@@ -153,6 +157,22 @@ test.serial('Pass empty Array to semantic-release API for list option set to "fa
t.is(process.exitCode, 0);
});
test.serial('Do not set properties in option for which arg is not in command line', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '-b', 'master'];
await cli();
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'ci'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'd'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'dry-run'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'debug'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'r'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 't'));
});
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
+11
View File
@@ -46,6 +46,17 @@ test('The "generateNotes" plugin, if defined, must be a single plugin definition
t.true(plugins.generateNotes.config.validator(() => {}));
});
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
t.false(plugins.publish.config.validator({}));
t.false(plugins.publish.config.validator({path: null}));
+13
View File
@@ -80,6 +80,19 @@ test.serial('Default values, reading repositoryUrl (http url) from package.json
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
const pkg = {repository: repositoryUrl};
// Create package.json in repository root
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
t.is(options.repositoryUrl, repositoryUrl);
});
test.serial('Read options from package.json', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
-13
View File
@@ -10,7 +10,6 @@ import {
push,
gitTags,
isGitRepo,
deleteTag,
verifyTagName,
} from '../lib/git';
import {
@@ -139,18 +138,6 @@ test.serial('Add tag on head commit', async t => {
await t.is(await gitCommitTag(commits[0].hash), 'tag_name');
});
test.serial('Delete a tag', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
await gitCommits(['Test commit']);
await tag('tag_name');
await push(repo, 'master');
await deleteTag(repo, 'tag_name');
t.falsy(await gitTagHead('tag_name'));
t.falsy(await gitRemoteTagHead(repo, 'tag_name'));
});
test.serial('Push tag and commit to remote repository', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
+15
View File
@@ -40,3 +40,18 @@ test.serial('Accept "undefined" input', t => {
test.serial('Return same string if no environment variable has to be replaced', t => {
t.is(require('../lib/hide-sensitive')('test'), 'test');
});
test.serial('Exclude empty environment variables from the regexp', t => {
process.env.SOME_PASSWORD = 'password';
process.env.SOME_TOKEN = '';
t.is(
require('../lib/hide-sensitive')(`https://user:${process.env.SOME_PASSWORD}@host.com?token=`),
'https://user:[secure]@host.com?token='
);
});
test.serial('Exclude empty environment variables from the regexp if there is only empty ones', t => {
process.env.SOME_PASSWORD = '';
process.env.SOME_TOKEN = ' \n ';
t.is(require('../lib/hide-sensitive')(`https://host.com?token=`), 'https://host.com?token=');
});
+42 -16
View File
@@ -67,6 +67,7 @@ test.serial('Plugins are called with expected values', async t => {
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const release1 = {name: 'Release 1', url: 'https://release1.com'};
const prepare = stub().resolves();
const publish1 = stub().resolves(release1);
const success = stub().resolves();
@@ -77,6 +78,7 @@ test.serial('Plugins are called with expected values', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare,
publish: [publish1, pluginNoop],
success,
};
@@ -119,6 +121,15 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(prepare.callCount, 1);
t.deepEqual(prepare.args[0][0], config);
t.deepEqual(prepare.args[0][1].options, options);
t.deepEqual(prepare.args[0][1].logger, t.context.logger);
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish1.callCount, 1);
t.deepEqual(publish1.args[0][0], config);
t.deepEqual(publish1.args[0][1].options, options);
@@ -161,6 +172,7 @@ test.serial('Use custom tag format', async t => {
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
@@ -177,7 +189,7 @@ test.serial('Use custom tag format', async t => {
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
});
test.serial('Use new gitHead, and recreate release notes if a publish plugin create a commit', async t => {
test.serial('Use new gitHead, and recreate release notes if a prepare plugin create a commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
@@ -191,10 +203,11 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
const notes = 'Release notes';
const generateNotes = stub().resolves(notes);
const publish1 = stub().callsFake(async () => {
const prepare1 = stub().callsFake(async () => {
commits = (await gitCommits(['Third'])).concat(commits);
});
const publish2 = stub().resolves();
const prepare2 = stub().resolves();
const publish = stub().resolves();
const options = {
branch: 'master',
@@ -203,7 +216,8 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes,
publish: [publish1, publish2],
prepare: [prepare1, prepare2],
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
@@ -217,14 +231,17 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
t.is(generateNotes.callCount, 2);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(publish1.callCount, 1);
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(prepare1.callCount, 1);
t.deepEqual(prepare1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
nextRelease.gitHead = await getGitHead();
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish2.callCount, 1);
t.deepEqual(publish2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(generateNotes.args[1][1].nextRelease, {...nextRelease, ...{notes}});
t.is(prepare2.callCount, 1);
t.deepEqual(prepare2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][1].nextRelease, {...nextRelease, ...{notes}});
// Verify the tag has been created on the local and remote repo and reference the last gitHead
t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash);
@@ -258,6 +275,7 @@ test.serial('Call all "success" plugins even if one errors out', async t => {
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
generateNotes,
prepare: stub().resolves(),
publish,
success: [success1, success2],
};
@@ -302,9 +320,9 @@ test.serial('Log all "verifyConditions" errors', async t => {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2, error3]);
t.deepEqual(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], [
@@ -345,9 +363,9 @@ test.serial('Log all "verifyRelease" errors', async t => {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual(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']);
t.is(fail.callCount, 1);
@@ -383,6 +401,7 @@ test.serial('Dry-run skips publish and success', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
};
@@ -428,9 +447,9 @@ test.serial('Dry-run skips fail', async t => {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual(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']);
t.is(fail.callCount, 0);
@@ -464,6 +483,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
@@ -512,6 +532,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
@@ -560,6 +581,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
@@ -611,6 +633,7 @@ test.serial('Returns falsy value if not running from the configured branch', asy
analyzeCommits: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
@@ -646,6 +669,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
@@ -686,6 +710,7 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
analyzeCommits,
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
@@ -785,7 +810,7 @@ test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot b
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = Array.from(await t.throws(semanticRelease()));
const errors = [...(await t.throws(semanticRelease()))];
// Verify error code and type
t.is(errors[0].code, 'ENOREPOURL');
@@ -844,6 +869,7 @@ test.serial('Get all commits including the ones not in the shallow clone', async
analyzeCommits,
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
+2 -2
View File
@@ -603,11 +603,11 @@ test.serial('Exit with 1 if missing permission to push to the remote repository'
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
const {repositoryUrl} = await gitbox.createRepo(packageName);
await gitbox.createRepo(packageName);
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
repository: {url: 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'},
});
/* Initial release */
+4 -4
View File
@@ -118,7 +118,7 @@ test('Throw all errors from the first step throwing an AggregateError', async t
const errors = await t.throws(pipeline([step1, step2, step3])(0));
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.notCalled);
@@ -133,7 +133,7 @@ test('Execute all even if a Promise rejects', async t => {
const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true}));
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.calledWith(0));
@@ -149,7 +149,7 @@ test('Throw all errors from all steps throwing an AggregateError', async t => {
const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true}));
t.deepEqual(Array.from(errors), [error1, error2, error3, error4]);
t.deepEqual([...errors], [error1, error2, error3, error4]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
});
@@ -165,7 +165,7 @@ test('Execute each function in series passing a transformed input even if a step
const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput}));
t.deepEqual(Array.from(errors), [error2, error3]);
t.deepEqual([...errors], [error2, error3]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0 + 1));
t.true(step3.calledWith(0 + 1 + error2));
+8 -4
View File
@@ -27,6 +27,7 @@ test('Export default plugins', t => {
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -49,6 +50,7 @@ test('Export plugins based on config', t => {
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -79,6 +81,7 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -106,6 +109,7 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -136,16 +140,16 @@ test('Merge global options with plugin options', async t => {
});
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
const errors = Array.from(t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger)));
const errors = [...t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
const errors = Array.from(
t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger))
);
const errors = [
...t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)),
];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
+7 -19
View File
@@ -1,5 +1,4 @@
import test from 'ava';
import {stub} from 'sinon';
import tempy from 'tempy';
import verify from '../lib/verify';
import {gitRepo} from './helpers/git-utils';
@@ -9,17 +8,13 @@ const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
test.beforeEach(() => {
// Delete environment variables that could have been set on the machine running the tests
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
// Stub the logger functions
t.context.log = stub();
t.context.error = stub();
t.context.logger = {log: t.context.log, error: t.context.error};
});
test.afterEach.always(() => {
@@ -32,7 +27,7 @@ test.afterEach.always(() => {
test.serial('Throw a AggregateError', async t => {
await gitRepo();
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
const errors = [...(await t.throws(verify({})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOREPOURL');
@@ -46,7 +41,7 @@ test.serial('Throw a SemanticReleaseError if does not run on a git repository',
const dir = tempy.directory();
process.chdir(dir);
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
const errors = [...(await t.throws(verify({})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOGITREPO');
@@ -56,7 +51,7 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', asyn
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `?\${version}`};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
@@ -66,7 +61,7 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains t
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: 'test'};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
@@ -76,22 +71,15 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
const errors = [...(await t.throws(verify(options)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
});
test.serial('Return "false" if the current branch is not the once configured', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
t.false(await verify(options, 'other', t.context.logger));
});
test.serial('Return "true" if all verification pass', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
t.true(await verify(options, 'master', t.context.logger));
await t.notThrows(verify(options));
});