feat: add success and fail notification plugins
				
					
				
			- Allow `publish` plugins to return an `Object` with information related to the releases - Add the `success` plugin hook, called when all `publish` are successful, receiving a list of release - Add the `fail` plugin hook, called when an error happens at any point, receiving a list of errors - Add detailed message for each error
This commit is contained in:
		
							parent
							
								
									9b2f6bfed2
								
							
						
					
					
						commit
						49f5e704ba
					
				| @ -37,6 +37,7 @@ This removes the immediate connection between human emotions and version numbers | |||||||
| - Fully automated release | - Fully automated release | ||||||
| - Enforce [Semantic Versioning](https://semver.org) specification | - Enforce [Semantic Versioning](https://semver.org) specification | ||||||
| - New features and fixes are immediately available to users | - New features and fixes are immediately available to users | ||||||
|  | - Notify maintainers and users of new releases | ||||||
| - Use formalized commit message convention to document changes in the codebase | - Use formalized commit message convention to document changes in the codebase | ||||||
| - Integrate with your [continuous integration workflow](docs/recipes/README.md#ci-configurations) | - Integrate with your [continuous integration workflow](docs/recipes/README.md#ci-configurations) | ||||||
| - Avoid potential errors associated with manual releases | - Avoid potential errors associated with manual releases | ||||||
| @ -86,6 +87,7 @@ After running the tests the command `semantic-release` will execute the followin | |||||||
| | Generate notes    | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release.                     | | | 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                                                                                                                | | | Create Git tag    | Create a Git tag corresponding the new release version                                                                                                                | | ||||||
| | Publish           | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-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.                      | | ||||||
| 
 | 
 | ||||||
| ## Documentation | ## Documentation | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								cli.js
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								cli.js
									
									
									
									
									
								
							| @ -2,7 +2,10 @@ const program = require('commander'); | |||||||
| const {pickBy, isUndefined} = require('lodash'); | const {pickBy, isUndefined} = require('lodash'); | ||||||
| 
 | 
 | ||||||
| function list(values) { | function list(values) { | ||||||
|   return values.split(',').map(value => value.trim()); |   return values | ||||||
|  |     .split(',') | ||||||
|  |     .map(value => value.trim()) | ||||||
|  |     .filter(value => value && value !== 'false'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| module.exports = async () => { | module.exports = async () => { | ||||||
| @ -26,6 +29,8 @@ module.exports = async () => { | |||||||
|     ) |     ) | ||||||
|     .option('--generate-notes <path>', 'Path or package name for the generateNotes plugin') |     .option('--generate-notes <path>', 'Path or package name for the generateNotes plugin') | ||||||
|     .option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list) |     .option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list) | ||||||
|  |     .option('--success <paths>', 'Comma separated list of paths or packages name for the success plugin(s)', list) | ||||||
|  |     .option('--fail <paths>', 'Comma separated list of paths or packages name for the fail plugin(s)', list) | ||||||
|     .option( |     .option( | ||||||
|       '--no-ci', |       '--no-ci', | ||||||
|       'Skip Continuous Integration environment verifications, allowing to make releases from a local machine' |       'Skip Continuous Integration environment verifications, allowing to make releases from a local machine' | ||||||
| @ -48,7 +53,7 @@ module.exports = async () => { | |||||||
|       process.exitCode = 1; |       process.exitCode = 1; | ||||||
|     } else { |     } else { | ||||||
|       const opts = program.opts(); |       const opts = program.opts(); | ||||||
|       // Set the `noCi` options as commander.js sets the `ci` options instead (becasue args starts with `--no`)
 |       // Set the `noCi` options as commander.js sets the `ci` options instead (because args starts with `--no`)
 | ||||||
|       opts.noCi = opts.ci === false ? true : undefined; |       opts.noCi = opts.ci === false ? true : undefined; | ||||||
|       // Remove option with undefined values, as commander.js sets non defined options as `undefined`
 |       // Remove option with undefined values, as commander.js sets non defined options as `undefined`
 | ||||||
|       await require('.')(pickBy(opts, value => !isUndefined(value))); |       await require('.')(pickBy(opts, value => !isUndefined(value))); | ||||||
|  | |||||||
| @ -5,6 +5,8 @@ | |||||||
| - [@semantic-release/github](https://github.com/semantic-release/github) | - [@semantic-release/github](https://github.com/semantic-release/github) | ||||||
|   - [verifyConditions](https://github.com/semantic-release/github#verifyconditions): Verify the presence and the validity of the GitHub authentication and release configuration |   - [verifyConditions](https://github.com/semantic-release/github#verifyconditions): Verify the presence and the validity of the GitHub authentication and release configuration | ||||||
|   - [publish](https://github.com/semantic-release/github#publish): Publish a [GitHub release](https://help.github.com/articles/about-releases) |   - [publish](https://github.com/semantic-release/github#publish): Publish a [GitHub release](https://help.github.com/articles/about-releases) | ||||||
|  |   - [success](https://github.com/semantic-release/github#success): Add a comment to GitHub issues and pull requests resolved in the release | ||||||
|  |   - [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) | - [@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 |   - [verifyConditions](https://github.com/semantic-release/npm#verifyconditions): Verify the presence and the validity of the npm authentication and release configuration | ||||||
|   - [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry |   - [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry | ||||||
| @ -25,6 +27,8 @@ | |||||||
|   - [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release |   - [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. |   - [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 |   - [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note | ||||||
|   - [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish 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 | ||||||
| 
 | 
 | ||||||
| ## Community plugins | ## Community plugins | ||||||
|  | |||||||
| @ -59,7 +59,7 @@ Default: `repository` property in `package.json` or [git origin url](https://git | |||||||
| 
 | 
 | ||||||
| CLI arguments: `-r`, `--repository-url` | CLI arguments: `-r`, `--repository-url` | ||||||
| 
 | 
 | ||||||
| The git repository URL | The git repository URL. | ||||||
| 
 | 
 | ||||||
| Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)). | Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)). | ||||||
| 
 | 
 | ||||||
| @ -75,7 +75,7 @@ CLI arguments: `-t`, `--tag-format` | |||||||
| 
 | 
 | ||||||
| The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by **semantic-release** to identify releases. The tag name is generated with [Lodash template](https://lodash.com/docs#template) and will be compiled with the `version` variable. | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by **semantic-release** to identify releases. The tag name is generated with [Lodash template](https://lodash.com/docs#template) and will be compiled with the `version` variable. | ||||||
| 
 | 
 | ||||||
| **Note**: The `tagFormat` must contain the `version` variable and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description). | **Note**: The `tagFormat` must contain the `version` variable exactly once and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description). | ||||||
| 
 | 
 | ||||||
| ### dryRun | ### dryRun | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -34,6 +34,18 @@ Plugin responsible for publishing the release. | |||||||
| 
 | 
 | ||||||
| Default implementation: [npm](https://github.com/semantic-release/npm#publish) and [github](https://github.com/semantic-release/github#publish). | Default implementation: [npm](https://github.com/semantic-release/npm#publish) and [github](https://github.com/semantic-release/github#publish). | ||||||
| 
 | 
 | ||||||
|  | ### success plugin | ||||||
|  | 
 | ||||||
|  | Plugin responsible for notifying of a new release. | ||||||
|  | 
 | ||||||
|  | Default implementation: [github](https://github.com/semantic-release/github#success). | ||||||
|  | 
 | ||||||
|  | ### fail plugin | ||||||
|  | 
 | ||||||
|  | Plugin responsible for notifying of a failed release. | ||||||
|  | 
 | ||||||
|  | Default implementation: [github](https://github.com/semantic-release/github#fail). | ||||||
|  | 
 | ||||||
| ## Configuration | ## Configuration | ||||||
| 
 | 
 | ||||||
| Plugin can be configured by specifying the plugin's module name or file path directly as a `String` or within the `path` key of an `Object`. | Plugin can be configured by specifying the plugin's module name or file path directly as a `String` or within the `path` key of an `Object`. | ||||||
|  | |||||||
							
								
								
									
										76
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								index.js
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| const {template, isFunction} = require('lodash'); | const {template, isPlainObject, castArray} = require('lodash'); | ||||||
| const marked = require('marked'); | const marked = require('marked'); | ||||||
| const TerminalRenderer = require('marked-terminal'); | const TerminalRenderer = require('marked-terminal'); | ||||||
| const envCi = require('env-ci'); | const envCi = require('env-ci'); | ||||||
| @ -9,13 +9,14 @@ const verify = require('./lib/verify'); | |||||||
| const getNextVersion = require('./lib/get-next-version'); | const getNextVersion = require('./lib/get-next-version'); | ||||||
| const getCommits = require('./lib/get-commits'); | const getCommits = require('./lib/get-commits'); | ||||||
| const getLastRelease = require('./lib/get-last-release'); | const getLastRelease = require('./lib/get-last-release'); | ||||||
|  | const {extractErrors} = require('./lib/utils'); | ||||||
| const logger = require('./lib/logger'); | const logger = require('./lib/logger'); | ||||||
| const {unshallow, gitHead: getGitHead, tag, push, deleteTag} = require('./lib/git'); | const {unshallow, gitHead: getGitHead, tag, push, deleteTag} = require('./lib/git'); | ||||||
| 
 | 
 | ||||||
| async function run(opts) { | marked.setOptions({renderer: new TerminalRenderer()}); | ||||||
|  | 
 | ||||||
|  | async function run(options, plugins) { | ||||||
|   const {isCi, branch, isPr} = envCi(); |   const {isCi, branch, isPr} = envCi(); | ||||||
|   const config = await getConfig(opts, logger); |  | ||||||
|   const {plugins, options} = config; |  | ||||||
| 
 | 
 | ||||||
|   if (!isCi && !options.dryRun && !options.noCi) { |   if (!isCi && !options.dryRun && !options.noCi) { | ||||||
|     logger.log('This run was not triggered in a known CI environment, running in dry-run mode.'); |     logger.log('This run was not triggered in a known CI environment, running in dry-run mode.'); | ||||||
| @ -34,7 +35,7 @@ async function run(opts) { | |||||||
|   logger.log('Run automated release from branch %s', options.branch); |   logger.log('Run automated release from branch %s', options.branch); | ||||||
| 
 | 
 | ||||||
|   logger.log('Call plugin %s', 'verify-conditions'); |   logger.log('Call plugin %s', 'verify-conditions'); | ||||||
|   await plugins.verifyConditions({options, logger}, true); |   await plugins.verifyConditions({options, logger}, {settleAll: true}); | ||||||
| 
 | 
 | ||||||
|   // Unshallow the repo in order to get all the tags
 |   // Unshallow the repo in order to get all the tags
 | ||||||
|   await unshallow(); |   await unshallow(); | ||||||
| @ -57,14 +58,13 @@ async function run(opts) { | |||||||
|   const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})}; |   const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})}; | ||||||
| 
 | 
 | ||||||
|   logger.log('Call plugin %s', 'verify-release'); |   logger.log('Call plugin %s', 'verify-release'); | ||||||
|   await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, true); |   await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, {settleAll: true}); | ||||||
| 
 | 
 | ||||||
|   const generateNotesParam = {options, logger, lastRelease, commits, nextRelease}; |   const generateNotesParam = {options, logger, lastRelease, commits, nextRelease}; | ||||||
| 
 | 
 | ||||||
|   if (options.dryRun) { |   if (options.dryRun) { | ||||||
|     logger.log('Call plugin %s', 'generate-notes'); |     logger.log('Call plugin %s', 'generate-notes'); | ||||||
|     const notes = await plugins.generateNotes(generateNotesParam); |     const notes = await plugins.generateNotes(generateNotesParam); | ||||||
|     marked.setOptions({renderer: new TerminalRenderer()}); |  | ||||||
|     logger.log('Release note for version %s:\n', nextRelease.version); |     logger.log('Release note for version %s:\n', nextRelease.version); | ||||||
|     process.stdout.write(`${marked(notes)}\n`); |     process.stdout.write(`${marked(notes)}\n`); | ||||||
|   } else { |   } else { | ||||||
| @ -77,10 +77,13 @@ async function run(opts) { | |||||||
|     await push(options.repositoryUrl, branch); |     await push(options.repositoryUrl, branch); | ||||||
| 
 | 
 | ||||||
|     logger.log('Call plugin %s', 'publish'); |     logger.log('Call plugin %s', 'publish'); | ||||||
|     await plugins.publish({options, logger, lastRelease, commits, nextRelease}, false, async prevInput => { |     const releases = await plugins.publish( | ||||||
|  |       {options, logger, lastRelease, commits, nextRelease}, | ||||||
|  |       { | ||||||
|  |         getNextInput: async lastResult => { | ||||||
|           const newGitHead = await getGitHead(); |           const newGitHead = await getGitHead(); | ||||||
|           // If previous publish plugin has created a commit (gitHead changed)
 |           // If previous publish plugin has created a commit (gitHead changed)
 | ||||||
|       if (prevInput.nextRelease.gitHead !== newGitHead) { |           if (lastResult.nextRelease.gitHead !== newGitHead) { | ||||||
|             // Delete the previously created tag
 |             // Delete the previously created tag
 | ||||||
|             await deleteTag(options.repositoryUrl, nextRelease.gitTag); |             await deleteTag(options.repositoryUrl, nextRelease.gitTag); | ||||||
|             // Recreate the tag, referencing the new gitHead
 |             // Recreate the tag, referencing the new gitHead
 | ||||||
| @ -95,27 +98,64 @@ async function run(opts) { | |||||||
|           } |           } | ||||||
|           // Call the next publish plugin with the updated `nextRelease`
 |           // Call the next publish plugin with the updated `nextRelease`
 | ||||||
|           return {options, logger, lastRelease, commits, nextRelease}; |           return {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} | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|     logger.log('Published release: %s', nextRelease.version); |     logger.log('Published release: %s', nextRelease.version); | ||||||
|   } |   } | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| module.exports = async opts => { | function logErrors(err) { | ||||||
|   const unhook = hookStd({silent: false}, hideSensitive); |   const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0)); | ||||||
|   try { |  | ||||||
|     const result = await run(opts); |  | ||||||
|     unhook(); |  | ||||||
|     return result; |  | ||||||
|   } catch (err) { |  | ||||||
|     const errors = err && isFunction(err[Symbol.iterator]) ? [...err].sort(error => !error.semanticRelease) : [err]; |  | ||||||
|   for (const error of errors) { |   for (const error of errors) { | ||||||
|     if (error.semanticRelease) { |     if (error.semanticRelease) { | ||||||
|       logger.log(`%s ${error.message}`, error.code); |       logger.log(`%s ${error.message}`, error.code); | ||||||
|  |       if (error.details) { | ||||||
|  |         process.stdout.write(`${marked(error.details)}\n`); | ||||||
|  |       } | ||||||
|     } else { |     } else { | ||||||
|       logger.error('An error occurred while running semantic-release: %O', error); |       logger.error('An error occurred while running semantic-release: %O', error); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function callFail(plugins, options, error) { | ||||||
|  |   const errors = extractErrors(error).filter(error => error.semanticRelease); | ||||||
|  |   if (errors.length > 0) { | ||||||
|  |     try { | ||||||
|  |       await plugins.fail({options, logger, errors}, {settleAll: true}); | ||||||
|  |     } catch (err) { | ||||||
|  |       logErrors(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = async opts => { | ||||||
|  |   const unhook = hookStd({silent: false}, hideSensitive); | ||||||
|  |   try { | ||||||
|  |     const config = await getConfig(opts, logger); | ||||||
|  |     const {plugins, options} = config; | ||||||
|  |     try { | ||||||
|  |       const result = await run(options, plugins); | ||||||
|  |       unhook(); | ||||||
|  |       return result; | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!options.dryRun) { | ||||||
|  |         await callFail(plugins, options, err); | ||||||
|  |       } | ||||||
|  |       throw err; | ||||||
|  |     } | ||||||
|  |   } catch (err) { | ||||||
|  |     logErrors(err); | ||||||
|     unhook(); |     unhook(); | ||||||
|     throw err; |     throw err; | ||||||
|   } |   } | ||||||
|  | |||||||
							
								
								
									
										118
									
								
								lib/definitions/errors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								lib/definitions/errors.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | |||||||
|  | const url = require('url'); | ||||||
|  | const {inspect} = require('util'); | ||||||
|  | const {toLower, isString} = require('lodash'); | ||||||
|  | const pkg = require('../../package.json'); | ||||||
|  | const RELEASE_TYPE = require('./release-types'); | ||||||
|  | 
 | ||||||
|  | const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}}); | ||||||
|  | const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5})); | ||||||
|  | const linkify = file => `${homepage}/blob/caribou/${file}`; | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  |   ENOGITREPO: () => ({ | ||||||
|  |     message: 'Not running from a git repository.', | ||||||
|  |     details: `The \`semantic-release\` command must be executed from a Git repository.
 | ||||||
|  | 
 | ||||||
|  | The current working directory is \`${process.cwd()}\`.
 | ||||||
|  | 
 | ||||||
|  | Please verify your CI configuration to make sure the \`semantic-release\` command is executed from the root of the cloned repository.`, | ||||||
|  |   }), | ||||||
|  |   ENOREPOURL: () => ({ | ||||||
|  |     message: 'The `repositoryUrl` option is required.', | ||||||
|  |     details: `The [repositoryUrl option](${linkify( | ||||||
|  |       'docs/usage/configuration.md#repositoryurl' | ||||||
|  |     )}) cannot be determined from the semantic-release configuration, the \`package.json\` nor the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes).
 | ||||||
|  | 
 | ||||||
|  | Please make sure to add the \`repositoryUrl\` to the [semantic-release configuration] (${linkify( | ||||||
|  |       'docs/usage/configuration.md' | ||||||
|  |     )}).`,
 | ||||||
|  |   }), | ||||||
|  |   EGITNOPERMISSION: ({options}) => ({ | ||||||
|  |     message: 'The push permission to the Git repository is required.', | ||||||
|  |     details: `**semantic-release** cannot push the version tag to the branch \`${ | ||||||
|  |       options.branch | ||||||
|  |     }\` on remote Git repository.
 | ||||||
|  | 
 | ||||||
|  | Please refer to the [authentication configuration documentation](${linkify( | ||||||
|  |       'docs/usage/ci-configuration.md#authentication' | ||||||
|  |     )}) to configure the Git credentials on your CI environment.`,
 | ||||||
|  |   }), | ||||||
|  |   EINVALIDTAGFORMAT: ({tagFormat}) => ({ | ||||||
|  |     message: 'Invalid `tagFormat` option.', | ||||||
|  |     details: `The [tagFormat](${linkify( | ||||||
|  |       'docs/usage/configuration.md#tagformat' | ||||||
|  |     )}) must compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
 | ||||||
|  | 
 | ||||||
|  | Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`, | ||||||
|  |   }), | ||||||
|  |   ETAGNOVERSION: ({tagFormat}) => ({ | ||||||
|  |     message: 'Invalid `tagFormat` option.', | ||||||
|  |     details: `The [tagFormat](${linkify( | ||||||
|  |       'docs/usage/configuration.md#tagformat' | ||||||
|  |     )}) option must contain the variable \`version\` exactly once.
 | ||||||
|  | 
 | ||||||
|  | Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`, | ||||||
|  |   }), | ||||||
|  |   EPLUGINCONF: ({pluginName, pluginConf}) => ({ | ||||||
|  |     message: `The \`${pluginName}\` plugin configuration is invalid.`, | ||||||
|  |     details: `The [${pluginName} plugin configuration](${linkify( | ||||||
|  |       `docs/usage/plugins.md#${toLower(pluginName)}-plugin` | ||||||
|  |     )}) if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a \`path\` property.
 | ||||||
|  | 
 | ||||||
|  |     Your configuration for the \`${pluginName}\` plugin is \`${stringify(pluginConf)}\`.`, | ||||||
|  |   }), | ||||||
|  |   EPLUGIN: ({pluginName, pluginType}) => ({ | ||||||
|  |     message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`, | ||||||
|  |     details: `A valid \`${pluginType}\` **semantic-release** plugin must be a function or an object with a function in the property \`${pluginType}\`.
 | ||||||
|  | 
 | ||||||
|  | The plugin \`${pluginName}\` doesn't have the property \`${pluginType}\` and cannot be used for the \`${pluginType}\` step.
 | ||||||
|  | 
 | ||||||
|  | Please refer to the \`${pluginName}\` and [semantic-release plugins configuration](${linkify( | ||||||
|  |       'docs/usage/plugins.md' | ||||||
|  |     )}) documentation for more details.`,
 | ||||||
|  |   }), | ||||||
|  |   EANALYZEOUTPUT: ({result, pluginName}) => ({ | ||||||
|  |     message: 'The `analyzeCommits` plugin returned an invalid value. It must return a valid semver release type.', | ||||||
|  |     details: `The \`analyzeCommits\` plugin must return a valid [semver](https://semver.org) release type. The valid values are: ${RELEASE_TYPE.map( | ||||||
|  |       type => `\`${type}\`` | ||||||
|  |     ).join(', ')}. | ||||||
|  | 
 | ||||||
|  | The \`analyzeCommits\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
 | ||||||
|  | 
 | ||||||
|  | We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
 | ||||||
|  | - The **semantic-release** version: \`${pkg.version}\` | ||||||
|  | - The **semantic-release** logs from your CI job | ||||||
|  | - The value returned by the plugin: \`${stringify(result)}\` | ||||||
|  | - A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify( | ||||||
|  |       'docs/developer-guide/plugin.md' | ||||||
|  |     )})`,
 | ||||||
|  |   }), | ||||||
|  |   ERELEASENOTESOUTPUT: ({result, pluginName}) => ({ | ||||||
|  |     message: 'The `generateNotes` plugin returned an invalid value. It must return a `String`.', | ||||||
|  |     details: `The \`generateNotes\` plugin must return a \`String\`.
 | ||||||
|  | 
 | ||||||
|  | The \`generateNotes\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
 | ||||||
|  | 
 | ||||||
|  | We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
 | ||||||
|  | - The **semantic-release** version: \`${pkg.version}\` | ||||||
|  | - The **semantic-release** logs from your CI job | ||||||
|  | - The value returned by the plugin: \`${stringify(result)}\` | ||||||
|  | - A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify( | ||||||
|  |       'docs/developer-guide/plugin.md' | ||||||
|  |     )})`,
 | ||||||
|  |   }), | ||||||
|  |   EPUBLISHOUTPUT: ({result, pluginName}) => ({ | ||||||
|  |     message: 'A `publish` plugin returned an invalid value. It must return an `Object`.', | ||||||
|  |     details: `The \`publish\` plugins must return an \`Object\`.
 | ||||||
|  | 
 | ||||||
|  | The \`publish\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
 | ||||||
|  | 
 | ||||||
|  | We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
 | ||||||
|  | - The **semantic-release** version: \`${pkg.version}\` | ||||||
|  | - The **semantic-release** logs from your CI job | ||||||
|  | - The value returned by the plugin: \`${stringify(result)}\` | ||||||
|  | - A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify( | ||||||
|  |       'docs/developer-guide/plugin.md' | ||||||
|  |     )})`,
 | ||||||
|  |   }), | ||||||
|  | }; | ||||||
							
								
								
									
										61
									
								
								lib/definitions/plugins.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								lib/definitions/plugins.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | const {isString, isFunction, isArray, isPlainObject} = require('lodash'); | ||||||
|  | const RELEASE_TYPE = require('./release-types'); | ||||||
|  | 
 | ||||||
|  | const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf); | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  |   verifyConditions: { | ||||||
|  |     default: ['@semantic-release/npm', '@semantic-release/github'], | ||||||
|  |     config: { | ||||||
|  |       validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   analyzeCommits: { | ||||||
|  |     default: '@semantic-release/commit-analyzer', | ||||||
|  |     config: { | ||||||
|  |       validator: conf => Boolean(conf) && validatePluginConfig(conf), | ||||||
|  |     }, | ||||||
|  |     output: { | ||||||
|  |       validator: output => !output || RELEASE_TYPE.includes(output), | ||||||
|  |       error: 'EANALYZEOUTPUT', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   verifyRelease: { | ||||||
|  |     default: false, | ||||||
|  |     config: { | ||||||
|  |       validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   generateNotes: { | ||||||
|  |     default: '@semantic-release/release-notes-generator', | ||||||
|  |     config: { | ||||||
|  |       validator: conf => !conf || validatePluginConfig(conf), | ||||||
|  |     }, | ||||||
|  |     output: { | ||||||
|  |       validator: output => !output || isString(output), | ||||||
|  |       error: 'ERELEASENOTESOUTPUT', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   publish: { | ||||||
|  |     default: ['@semantic-release/npm', '@semantic-release/github'], | ||||||
|  |     config: { | ||||||
|  |       validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), | ||||||
|  |     }, | ||||||
|  |     output: { | ||||||
|  |       validator: output => !output || isPlainObject(output), | ||||||
|  |       error: 'EPUBLISHOUTPUT', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   success: { | ||||||
|  |     default: ['@semantic-release/github'], | ||||||
|  |     config: { | ||||||
|  |       validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   fail: { | ||||||
|  |     default: ['@semantic-release/github'], | ||||||
|  |     config: { | ||||||
|  |       validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								lib/definitions/release-types.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								lib/definitions/release-types.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']; | ||||||
| @ -4,7 +4,7 @@ const cosmiconfig = require('cosmiconfig'); | |||||||
| const resolveFrom = require('resolve-from'); | const resolveFrom = require('resolve-from'); | ||||||
| const debug = require('debug')('semantic-release:config'); | const debug = require('debug')('semantic-release:config'); | ||||||
| const {repoUrl} = require('./git'); | const {repoUrl} = require('./git'); | ||||||
| const PLUGINS_DEFINITION = require('./plugins/definitions'); | const PLUGINS_DEFINITIONS = require('./definitions/plugins'); | ||||||
| const plugins = require('./plugins'); | const plugins = require('./plugins'); | ||||||
| const getGitAuthUrl = require('./get-git-auth-url'); | const getGitAuthUrl = require('./get-git-auth-url'); | ||||||
| 
 | 
 | ||||||
| @ -25,7 +25,7 @@ module.exports = async (opts, logger) => { | |||||||
|         // For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
 |         // For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
 | ||||||
|         // so those plugin will be loaded relatively to the config file
 |         // so those plugin will be loaded relatively to the config file
 | ||||||
|         Object.keys(extendsOpts).reduce((pluginsPath, option) => { |         Object.keys(extendsOpts).reduce((pluginsPath, option) => { | ||||||
|           if (PLUGINS_DEFINITION[option]) { |           if (PLUGINS_DEFINITIONS[option]) { | ||||||
|             castArray(extendsOpts[option]) |             castArray(extendsOpts[option]) | ||||||
|               .filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path))) |               .filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path))) | ||||||
|               .map(plugin => (isString(plugin) ? plugin : plugin.path)) |               .map(plugin => (isString(plugin) ? plugin : plugin.path)) | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								lib/get-error.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/get-error.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | const SemanticReleaseError = require('@semantic-release/error'); | ||||||
|  | const ERROR_DEFINITIONS = require('./definitions/errors'); | ||||||
|  | 
 | ||||||
|  | module.exports = (code, ctx = {}) => { | ||||||
|  |   const {message, details} = ERROR_DEFINITIONS[code](ctx); | ||||||
|  |   return new SemanticReleaseError(message, code, details); | ||||||
|  | }; | ||||||
| @ -120,7 +120,6 @@ async function push(origin, branch) { | |||||||
|  * |  * | ||||||
|  * @param {String} origin The remote repository URL. |  * @param {String} origin The remote repository URL. | ||||||
|  * @param {String} tagName The tag name to delete. |  * @param {String} tagName The tag name to delete. | ||||||
|  * @throws {SemanticReleaseError} if the remote tag exists and references a commit that is not the local head commit. |  | ||||||
|  */ |  */ | ||||||
| async function deleteTag(origin, tagName) { | async function deleteTag(origin, tagName) { | ||||||
|   // Delete the local tag
 |   // Delete the local tag
 | ||||||
|  | |||||||
| @ -1,55 +0,0 @@ | |||||||
| const {isString, isFunction, isArray} = require('lodash'); |  | ||||||
| 
 |  | ||||||
| const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']; |  | ||||||
| const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf); |  | ||||||
| 
 |  | ||||||
| module.exports = { |  | ||||||
|   verifyConditions: { |  | ||||||
|     default: ['@semantic-release/npm', '@semantic-release/github'], |  | ||||||
|     config: { |  | ||||||
|       validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), |  | ||||||
|       message: |  | ||||||
|         'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.', |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   analyzeCommits: { |  | ||||||
|     default: '@semantic-release/commit-analyzer', |  | ||||||
|     config: { |  | ||||||
|       validator: conf => Boolean(conf) && validatePluginConfig(conf), |  | ||||||
|       message: |  | ||||||
|         'The "analyzeCommits" plugin is mandatory, and must be a single plugin definition. A plugin definition is either a string or an object with a path property.', |  | ||||||
|     }, |  | ||||||
|     output: { |  | ||||||
|       validator: output => !output || RELEASE_TYPE.includes(output), |  | ||||||
|       message: 'The "analyzeCommits" plugin output, if defined, must be a valid semver release type.', |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   verifyRelease: { |  | ||||||
|     default: false, |  | ||||||
|     config: { |  | ||||||
|       validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), |  | ||||||
|       message: |  | ||||||
|         'The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.', |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   generateNotes: { |  | ||||||
|     default: '@semantic-release/release-notes-generator', |  | ||||||
|     config: { |  | ||||||
|       validator: conf => !conf || validatePluginConfig(conf), |  | ||||||
|       message: |  | ||||||
|         'The "generateNotes" plugin, if defined, must be a single plugin definition. A plugin definition is either a string or an object with a path property.', |  | ||||||
|     }, |  | ||||||
|     output: { |  | ||||||
|       validator: output => !output || isString(output), |  | ||||||
|       message: 'The "generateNotes" plugin output, if defined, must be a string.', |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   publish: { |  | ||||||
|     default: ['@semantic-release/npm', '@semantic-release/github'], |  | ||||||
|     config: { |  | ||||||
|       validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)), |  | ||||||
|       message: |  | ||||||
|         'The "publish" plugin is mandatory, and must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.', |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @ -1,34 +1,35 @@ | |||||||
| const {isArray, isObject, omit} = require('lodash'); | const {isArray, isObject, omit, castArray, isUndefined} = require('lodash'); | ||||||
| const AggregateError = require('aggregate-error'); | const AggregateError = require('aggregate-error'); | ||||||
| const SemanticReleaseError = require('@semantic-release/error'); | const getError = require('../get-error'); | ||||||
| const PLUGINS_DEFINITION = require('./definitions'); | const PLUGINS_DEFINITIONS = require('../definitions/plugins'); | ||||||
| const pipeline = require('./pipeline'); | const pipeline = require('./pipeline'); | ||||||
| const normalize = require('./normalize'); | const normalize = require('./normalize'); | ||||||
| 
 | 
 | ||||||
| module.exports = (options, pluginsPath, logger) => { | module.exports = (options, pluginsPath, logger) => { | ||||||
|   const errors = []; |   const errors = []; | ||||||
|   const plugins = Object.keys(PLUGINS_DEFINITION).reduce((plugins, pluginType) => { |   const plugins = Object.keys(PLUGINS_DEFINITIONS).reduce((plugins, pluginType) => { | ||||||
|     const {config, output, default: def} = PLUGINS_DEFINITION[pluginType]; |     const {config, default: def} = PLUGINS_DEFINITIONS[pluginType]; | ||||||
|     let pluginConfs; |     let pluginConfs; | ||||||
|     if (options[pluginType]) { | 
 | ||||||
|  |     if (isUndefined(options[pluginType])) { | ||||||
|  |       pluginConfs = def; | ||||||
|  |     } else { | ||||||
|       // If an object is passed and the path is missing, set the default one for single plugins
 |       // If an object is passed and the path is missing, set the default one for single plugins
 | ||||||
|       if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) { |       if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) { | ||||||
|         options[pluginType].path = def; |         options[pluginType].path = def; | ||||||
|       } |       } | ||||||
|       if (config && !config.validator(options[pluginType])) { |       if (config && !config.validator(options[pluginType])) { | ||||||
|         errors.push(new SemanticReleaseError(config.message, 'EPLUGINCONF')); |         errors.push(getError('EPLUGINCONF', {pluginType, pluginConf: options[pluginType]})); | ||||||
|         return plugins; |         return plugins; | ||||||
|       } |       } | ||||||
|       pluginConfs = options[pluginType]; |       pluginConfs = options[pluginType]; | ||||||
|     } else { |  | ||||||
|       pluginConfs = def; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITION)); |     const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS)); | ||||||
| 
 | 
 | ||||||
|     plugins[pluginType] = isArray(pluginConfs) |     plugins[pluginType] = pipeline( | ||||||
|       ? pipeline(pluginConfs.map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger, output))) |       castArray(pluginConfs).map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger)) | ||||||
|       : normalize(pluginType, pluginsPath, globalOpts, pluginConfs, logger, output); |     ); | ||||||
| 
 | 
 | ||||||
|     return plugins; |     return plugins; | ||||||
|   }, {}); |   }, {}); | ||||||
|  | |||||||
| @ -1,15 +1,18 @@ | |||||||
| const {dirname} = require('path'); | const {dirname} = require('path'); | ||||||
| const {inspect} = require('util'); | const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash'); | ||||||
| const SemanticReleaseError = require('@semantic-release/error'); |  | ||||||
| const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash'); |  | ||||||
| const resolveFrom = require('resolve-from'); | const resolveFrom = require('resolve-from'); | ||||||
|  | const getError = require('../get-error'); | ||||||
|  | const {extractErrors} = require('../utils'); | ||||||
|  | const PLUGINS_DEFINITIONS = require('../definitions/plugins'); | ||||||
| 
 | 
 | ||||||
| module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger, validator) => { | module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => { | ||||||
|   if (!pluginOpts) { |   if (!pluginOpts) { | ||||||
|     return noop; |     return noop; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const {path, ...config} = isString(pluginOpts) || isFunction(pluginOpts) ? {path: pluginOpts} : pluginOpts; |   const {path, ...config} = isString(pluginOpts) || isFunction(pluginOpts) ? {path: pluginOpts} : pluginOpts; | ||||||
|  |   const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path; | ||||||
|  | 
 | ||||||
|   if (!isFunction(pluginOpts)) { |   if (!isFunction(pluginOpts)) { | ||||||
|     if (pluginsPath[path]) { |     if (pluginsPath[path]) { | ||||||
|       logger.log('Load plugin %s from %s in shareable config %s', pluginType, path, pluginsPath[path]); |       logger.log('Load plugin %s from %s in shareable config %s', pluginType, path, pluginsPath[path]); | ||||||
| @ -28,21 +31,27 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger, valid | |||||||
|   let func; |   let func; | ||||||
|   if (isFunction(plugin)) { |   if (isFunction(plugin)) { | ||||||
|     func = plugin.bind(null, cloneDeep({...globalOpts, ...config})); |     func = plugin.bind(null, cloneDeep({...globalOpts, ...config})); | ||||||
|   } else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) { |   } else if (isPlainObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) { | ||||||
|     func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config})); |     func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config})); | ||||||
|   } else { |   } else { | ||||||
|     throw new SemanticReleaseError( |     throw getError('EPLUGIN', {pluginType, pluginName}); | ||||||
|       `The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.`, |  | ||||||
|       'EPLUGINCONF' |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return async input => { |   return Object.defineProperty( | ||||||
|  |     async input => { | ||||||
|  |       const definition = PLUGINS_DEFINITIONS[pluginType]; | ||||||
|  |       try { | ||||||
|         const result = await func(cloneDeep(input)); |         const result = await func(cloneDeep(input)); | ||||||
| 
 |         if (definition && definition.output && !definition.output.validator(result)) { | ||||||
|     if (validator && !validator.validator(result)) { |           throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName}); | ||||||
|       throw new Error(`${validator.message} Received: ${inspect(result)}`); |  | ||||||
|         } |         } | ||||||
|         return result; |         return result; | ||||||
|   }; |       } catch (err) { | ||||||
|  |         extractErrors(err).forEach(err => Object.assign(err, {pluginName})); | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     'pluginName', | ||||||
|  |     {value: pluginName, writable: false, enumerable: true} | ||||||
|  |   ); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,37 +1,55 @@ | |||||||
| const {identity, isFunction} = require('lodash'); | const {identity} = require('lodash'); | ||||||
| const pReflect = require('p-reflect'); |  | ||||||
| const pReduce = require('p-reduce'); | const pReduce = require('p-reduce'); | ||||||
| const AggregateError = require('aggregate-error'); | const AggregateError = require('aggregate-error'); | ||||||
|  | const {extractErrors} = require('../utils'); | ||||||
| 
 | 
 | ||||||
| module.exports = steps => async (input, settleAll = false, getNextInput = identity) => { | /** | ||||||
|  |  * A Function that execute a list of function sequencially. If at least one Function ins the pipeline throw an Error or rejects, the pipeline function rejects as well. | ||||||
|  |  * | ||||||
|  |  * @typedef {Function} Pipeline | ||||||
|  |  * @param {Any} input Argument to pass to the first step in the pipeline. | ||||||
|  |  * @param {Object} options Pipeline options. | ||||||
|  |  * @param {Boolean} [options.settleAll=false] If `true` all the steps in the pipeline are executed, even if one rejects, if `false` the execution stops after a steps rejects. | ||||||
|  |  * @param {Function} [options.getNextInput=identity] Function called after each step is executed, with the last and current step results; the returned value will be used as the argument of the next step. | ||||||
|  |  * @param {Function} [options.transform=identity] Function called after each step is executed, with the current step result and the step function; the returned value will be saved in the pipeline results. | ||||||
|  |  * | ||||||
|  |  * @return {Array<*>|*} An Array with the result of each step in the pipeline; if there is only 1 step in the pipeline, the result of this step is returned directly. | ||||||
|  |  * | ||||||
|  |  * @throws {AggregateError|Error} An AggregateError with the errors of each step in the pipeline that rejected; if there is only 1 step in the pipeline, the error of this step is thrown directly. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Create a Pipeline with a list of Functions. | ||||||
|  |  * | ||||||
|  |  * @param {Array<Function>} steps The list of Function to execute. | ||||||
|  |  * @return {Pipeline} A Function that execute the `steps` sequencially | ||||||
|  |  */ | ||||||
|  | module.exports = steps => async (input, {settleAll = false, getNextInput = identity, transform = identity} = {}) => { | ||||||
|   const results = []; |   const results = []; | ||||||
|   const errors = []; |   const errors = []; | ||||||
|   await pReduce( |   await pReduce( | ||||||
|     steps, |     steps, | ||||||
|     async (prevResult, nextStep) => { |     async (lastResult, step) => { | ||||||
|       let result; |       let result; | ||||||
| 
 |       try { | ||||||
|       // Call the next step with the input computed at the end of the previous iteration and save intermediary result
 |         // Call the step with the input computed at the end of the previous iteration and save intermediary result
 | ||||||
|  |         result = await transform(await step(lastResult), step); | ||||||
|  |         results.push(result); | ||||||
|  |       } catch (err) { | ||||||
|         if (settleAll) { |         if (settleAll) { | ||||||
|         const {isFulfilled, value, reason} = await pReflect(nextStep(prevResult)); |           errors.push(...extractErrors(err)); | ||||||
|         result = isFulfilled ? value : reason; |           result = err; | ||||||
|         if (isFulfilled) { |  | ||||||
|           results.push(result); |  | ||||||
|         } else { |         } else { | ||||||
|           errors.push(...(result && isFunction(result[Symbol.iterator]) ? result : [result])); |           throw err; | ||||||
|         } |         } | ||||||
|       } else { |  | ||||||
|         result = await nextStep(prevResult); |  | ||||||
|         results.push(result); |  | ||||||
|       } |       } | ||||||
| 
 |       // Prepare input for the next step, passing the result of the last iteration (or initial parameter for the first iteration) and the current one
 | ||||||
|       // Prepare input for next step, passing the result of the previous iteration and the current one
 |       return getNextInput(lastResult, result); | ||||||
|       return getNextInput(prevResult, result); |  | ||||||
|     }, |     }, | ||||||
|     input |     input | ||||||
|   ); |   ); | ||||||
|   if (errors.length > 0) { |   if (errors.length > 0) { | ||||||
|     throw new AggregateError(errors); |     throw errors.length === 1 ? errors[0] : new AggregateError(errors); | ||||||
|   } |   } | ||||||
|   return results; |   return results.length <= 1 ? results[0] : results; | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								lib/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/utils.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | const {isFunction} = require('lodash'); | ||||||
|  | 
 | ||||||
|  | function extractErrors(err) { | ||||||
|  |   return err && isFunction(err[Symbol.iterator]) ? [...err] : [err]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = {extractErrors}; | ||||||
| @ -1,44 +1,29 @@ | |||||||
| const {template} = require('lodash'); | const {template} = require('lodash'); | ||||||
| const SemanticReleaseError = require('@semantic-release/error'); |  | ||||||
| const AggregateError = require('aggregate-error'); | const AggregateError = require('aggregate-error'); | ||||||
| const {isGitRepo, verifyAuth, verifyTagName} = require('./git'); | const {isGitRepo, verifyAuth, verifyTagName} = require('./git'); | ||||||
|  | const getError = require('./get-error'); | ||||||
| 
 | 
 | ||||||
| module.exports = async (options, branch, logger) => { | module.exports = async (options, branch, logger) => { | ||||||
|   const errors = []; |   const errors = []; | ||||||
| 
 | 
 | ||||||
|   if (!await isGitRepo()) { |   if (!await isGitRepo()) { | ||||||
|     logger.error('Semantic-release must run from a git repository.'); |     errors.push(getError('ENOGITREPO')); | ||||||
|     return false; |   } else if (!options.repositoryUrl) { | ||||||
|   } |     errors.push(getError('ENOREPOURL')); | ||||||
| 
 |  | ||||||
|   if (!options.repositoryUrl) { |  | ||||||
|     errors.push(new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL')); |  | ||||||
|   } else if (!await verifyAuth(options.repositoryUrl, options.branch)) { |   } else if (!await verifyAuth(options.repositoryUrl, options.branch)) { | ||||||
|     errors.push( |     errors.push(getError('EGITNOPERMISSION', {options})); | ||||||
|       new SemanticReleaseError( |  | ||||||
|         `The git credentials doesn't allow to push on the branch ${options.branch}.`, |  | ||||||
|         'EGITNOPERMISSION' |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Verify that compiling the `tagFormat` produce a valid Git tag
 |   // Verify that compiling the `tagFormat` produce a valid Git tag
 | ||||||
|   if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) { |   if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) { | ||||||
|     errors.push( |     errors.push(getError('EINVALIDTAGFORMAT', {tagFormat: options.tagFormat})); | ||||||
|       new SemanticReleaseError('The tagFormat template must compile to a valid Git tag format', 'EINVALIDTAGFORMAT') |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Verify the `tagFormat` contains the variable `version` by compiling the `tagFormat` template
 |   // Verify the `tagFormat` contains the variable `version` by compiling the `tagFormat` template
 | ||||||
|   // with a space as the `version` value and verify the result contains the space.
 |   // with a space as the `version` value and verify the result contains the space.
 | ||||||
|   // The space is used as it's an invalid tag character, so it's guaranteed to no be present in the `tagFormat`.
 |   // The space is used as it's an invalid tag character, so it's guaranteed to no be present in the `tagFormat`.
 | ||||||
|   if ((template(options.tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) { |   if ((template(options.tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) { | ||||||
|     errors.push( |     errors.push(getError('ETAGNOVERSION', {tagFormat: options.tagFormat})); | ||||||
|       new SemanticReleaseError( |  | ||||||
|         `The tagFormat template must contain the variable "\${version}" exactly once`, |  | ||||||
|         'ETAGNOVERSION' |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (errors.length > 0) { |   if (errors.length > 0) { | ||||||
|  | |||||||
| @ -20,9 +20,9 @@ | |||||||
|   ], |   ], | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@semantic-release/commit-analyzer": "^5.0.0", |     "@semantic-release/commit-analyzer": "^5.0.0", | ||||||
|     "@semantic-release/error": "^2.1.0", |     "@semantic-release/error": "^2.2.0", | ||||||
|     "@semantic-release/github": "^4.0.2", |     "@semantic-release/github": "^4.1.0", | ||||||
|     "@semantic-release/npm": "^3.0.0", |     "@semantic-release/npm": "^3.1.0", | ||||||
|     "@semantic-release/release-notes-generator": "^6.0.0", |     "@semantic-release/release-notes-generator": "^6.0.0", | ||||||
|     "aggregate-error": "^1.0.0", |     "aggregate-error": "^1.0.0", | ||||||
|     "chalk": "^2.3.0", |     "chalk": "^2.3.0", | ||||||
| @ -40,7 +40,6 @@ | |||||||
|     "marked-terminal": "^2.0.0", |     "marked-terminal": "^2.0.0", | ||||||
|     "p-locate": "^2.0.0", |     "p-locate": "^2.0.0", | ||||||
|     "p-reduce": "^1.0.0", |     "p-reduce": "^1.0.0", | ||||||
|     "p-reflect": "^1.0.0", |  | ||||||
|     "read-pkg-up": "^3.0.0", |     "read-pkg-up": "^3.0.0", | ||||||
|     "resolve-from": "^4.0.0", |     "resolve-from": "^4.0.0", | ||||||
|     "semver": "^5.4.1" |     "semver": "^5.4.1" | ||||||
|  | |||||||
							
								
								
									
										122
									
								
								test/definitions/plugins.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								test/definitions/plugins.test.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,122 @@ | |||||||
|  | import test from 'ava'; | ||||||
|  | import plugins from '../../lib/definitions/plugins'; | ||||||
|  | import errors from '../../lib/definitions/errors'; | ||||||
|  | 
 | ||||||
|  | test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => { | ||||||
|  |   t.false(plugins.verifyConditions.config.validator({})); | ||||||
|  |   t.false(plugins.verifyConditions.config.validator({path: null})); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.verifyConditions.config.validator({path: 'plugin-path.js'})); | ||||||
|  |   t.true(plugins.verifyConditions.config.validator()); | ||||||
|  |   t.true(plugins.verifyConditions.config.validator('plugin-path.js')); | ||||||
|  |   t.true(plugins.verifyConditions.config.validator(() => {})); | ||||||
|  |   t.true(plugins.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => { | ||||||
|  |   t.false(plugins.analyzeCommits.config.validator({})); | ||||||
|  |   t.false(plugins.analyzeCommits.config.validator({path: null})); | ||||||
|  |   t.false(plugins.analyzeCommits.config.validator([])); | ||||||
|  |   t.false(plugins.analyzeCommits.config.validator()); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.analyzeCommits.config.validator({path: 'plugin-path.js'})); | ||||||
|  |   t.true(plugins.analyzeCommits.config.validator('plugin-path.js')); | ||||||
|  |   t.true(plugins.analyzeCommits.config.validator(() => {})); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "verifyRelease" 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 "generateNotes" plugin, if defined, must be a single plugin definition', t => { | ||||||
|  |   t.false(plugins.generateNotes.config.validator({})); | ||||||
|  |   t.false(plugins.generateNotes.config.validator({path: null})); | ||||||
|  |   t.false(plugins.generateNotes.config.validator([])); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.generateNotes.config.validator()); | ||||||
|  |   t.true(plugins.generateNotes.config.validator({path: 'plugin-path.js'})); | ||||||
|  |   t.true(plugins.generateNotes.config.validator('plugin-path.js')); | ||||||
|  |   t.true(plugins.generateNotes.config.validator(() => {})); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 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})); | ||||||
|  |   t.false(plugins.publish.config.validator()); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.publish.config.validator({path: 'plugin-path.js'})); | ||||||
|  |   t.true(plugins.publish.config.validator('plugin-path.js')); | ||||||
|  |   t.true(plugins.publish.config.validator(() => {})); | ||||||
|  |   t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => { | ||||||
|  |   t.false(plugins.success.config.validator({})); | ||||||
|  |   t.false(plugins.success.config.validator({path: null})); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.success.config.validator({path: 'plugin-path.js'})); | ||||||
|  |   t.true(plugins.success.config.validator()); | ||||||
|  |   t.true(plugins.success.config.validator('plugin-path.js')); | ||||||
|  |   t.true(plugins.success.config.validator(() => {})); | ||||||
|  |   t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => { | ||||||
|  |   t.false(plugins.fail.config.validator({})); | ||||||
|  |   t.false(plugins.fail.config.validator({path: null})); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.fail.config.validator({path: 'plugin-path.js'})); | ||||||
|  |   t.true(plugins.fail.config.validator()); | ||||||
|  |   t.true(plugins.fail.config.validator('plugin-path.js')); | ||||||
|  |   t.true(plugins.fail.config.validator(() => {})); | ||||||
|  |   t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => { | ||||||
|  |   t.false(plugins.analyzeCommits.output.validator('invalid')); | ||||||
|  |   t.false(plugins.analyzeCommits.output.validator(1)); | ||||||
|  |   t.false(plugins.analyzeCommits.output.validator({})); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.analyzeCommits.output.validator()); | ||||||
|  |   t.true(plugins.analyzeCommits.output.validator(null)); | ||||||
|  |   t.true(plugins.analyzeCommits.output.validator('major')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "generateNotes" plugin output, if defined, must be a string', t => { | ||||||
|  |   t.false(plugins.generateNotes.output.validator(1)); | ||||||
|  |   t.false(plugins.generateNotes.output.validator({})); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.generateNotes.output.validator()); | ||||||
|  |   t.true(plugins.generateNotes.output.validator(null)); | ||||||
|  |   t.true(plugins.generateNotes.output.validator('')); | ||||||
|  |   t.true(plugins.generateNotes.output.validator('string')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "publish" plugin output, if defined, must be an object', t => { | ||||||
|  |   t.false(plugins.publish.output.validator(1)); | ||||||
|  |   t.false(plugins.publish.output.validator('string')); | ||||||
|  | 
 | ||||||
|  |   t.true(plugins.publish.output.validator({})); | ||||||
|  |   t.true(plugins.publish.output.validator()); | ||||||
|  |   t.true(plugins.publish.output.validator(null)); | ||||||
|  |   t.true(plugins.publish.output.validator('')); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "analyzeCommits" plugin output definition return an existing error code', t => { | ||||||
|  |   t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error)); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "generateNotes" plugin output definition return an existing error code', t => { | ||||||
|  |   t.true(Object.keys(errors).includes(plugins.generateNotes.output.error)); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('The "publish" plugin output definition return an existing error code', t => { | ||||||
|  |   t.true(Object.keys(errors).includes(plugins.publish.output.error)); | ||||||
|  | }); | ||||||
							
								
								
									
										5
									
								
								test/fixtures/plugin-errors.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								test/fixtures/plugin-errors.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | const AggregateError = require('aggregate-error'); | ||||||
|  | 
 | ||||||
|  | module.exports = () => { | ||||||
|  |   throw new AggregateError([new Error('a'), new Error('b')]); | ||||||
|  | }; | ||||||
							
								
								
									
										1
									
								
								test/fixtures/plugin-identity.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/fixtures/plugin-identity.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | module.exports = (pluginConfig, options) => options; | ||||||
| @ -1,11 +1,10 @@ | |||||||
| import test from 'ava'; | import test from 'ava'; | ||||||
| import proxyquire from 'proxyquire'; | import proxyquire from 'proxyquire'; | ||||||
| import {stub} from 'sinon'; | import {spy, stub} from 'sinon'; | ||||||
| import tempy from 'tempy'; |  | ||||||
| import clearModule from 'clear-module'; | import clearModule from 'clear-module'; | ||||||
| import AggregateError from 'aggregate-error'; | import AggregateError from 'aggregate-error'; | ||||||
| import SemanticReleaseError from '@semantic-release/error'; | import SemanticReleaseError from '@semantic-release/error'; | ||||||
| import DEFINITIONS from '../lib/plugins/definitions'; | import DEFINITIONS from '../lib/definitions/plugins'; | ||||||
| import { | import { | ||||||
|   gitHead as getGitHead, |   gitHead as getGitHead, | ||||||
|   gitTagHead, |   gitTagHead, | ||||||
| @ -21,10 +20,10 @@ import { | |||||||
| const envBackup = Object.assign({}, process.env); | const envBackup = Object.assign({}, process.env); | ||||||
| // Save the current working diretory
 | // Save the current working diretory
 | ||||||
| const cwd = process.cwd(); | const cwd = process.cwd(); | ||||||
|  | const pluginNoop = require.resolve('./fixtures/plugin-noop'); | ||||||
| 
 | 
 | ||||||
| test.beforeEach(t => { | test.beforeEach(t => { | ||||||
|   clearModule('../lib/hide-sensitive'); |   clearModule('../lib/hide-sensitive'); | ||||||
| 
 |  | ||||||
|   // Delete environment variables that could have been set on the machine running the tests
 |   // Delete environment variables that could have been set on the machine running the tests
 | ||||||
|   delete process.env.GIT_CREDENTIALS; |   delete process.env.GIT_CREDENTIALS; | ||||||
|   delete process.env.GH_TOKEN; |   delete process.env.GH_TOKEN; | ||||||
| @ -32,8 +31,8 @@ test.beforeEach(t => { | |||||||
|   delete process.env.GL_TOKEN; |   delete process.env.GL_TOKEN; | ||||||
|   delete process.env.GITLAB_TOKEN; |   delete process.env.GITLAB_TOKEN; | ||||||
|   // Stub the logger functions
 |   // Stub the logger functions
 | ||||||
|   t.context.log = stub(); |   t.context.log = spy(); | ||||||
|   t.context.error = stub(); |   t.context.error = spy(); | ||||||
|   t.context.logger = {log: t.context.log, error: t.context.error}; |   t.context.logger = {log: t.context.log, error: t.context.error}; | ||||||
|   t.context.stdout = stub(process.stdout, 'write'); |   t.context.stdout = stub(process.stdout, 'write'); | ||||||
|   t.context.stderr = stub(process.stderr, 'write'); |   t.context.stderr = stub(process.stderr, 'write'); | ||||||
| @ -67,7 +66,9 @@ test.serial('Plugins are called with expected values', async t => { | |||||||
|   const analyzeCommits = stub().resolves(nextRelease.type); |   const analyzeCommits = stub().resolves(nextRelease.type); | ||||||
|   const verifyRelease = stub().resolves(); |   const verifyRelease = stub().resolves(); | ||||||
|   const generateNotes = stub().resolves(notes); |   const generateNotes = stub().resolves(notes); | ||||||
|   const publish = stub().resolves(); |   const release1 = {name: 'Release 1', url: 'https://release1.com'}; | ||||||
|  |   const publish1 = stub().resolves(release1); | ||||||
|  |   const success = stub().resolves(); | ||||||
| 
 | 
 | ||||||
|   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`}; |   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`}; | ||||||
|   const options = { |   const options = { | ||||||
| @ -76,7 +77,8 @@ test.serial('Plugins are called with expected values', async t => { | |||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|     verifyRelease, |     verifyRelease, | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish, |     publish: [publish1, pluginNoop], | ||||||
|  |     success, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -117,14 +119,27 @@ 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].commits[0].message, commits[0].message); | ||||||
|   t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease); |   t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease); | ||||||
| 
 | 
 | ||||||
|   t.is(publish.callCount, 1); |   t.is(publish1.callCount, 1); | ||||||
|   t.deepEqual(publish.args[0][0], config); |   t.deepEqual(publish1.args[0][0], config); | ||||||
|   t.deepEqual(publish.args[0][1].options, options); |   t.deepEqual(publish1.args[0][1].options, options); | ||||||
|   t.deepEqual(publish.args[0][1].logger, t.context.logger); |   t.deepEqual(publish1.args[0][1].logger, t.context.logger); | ||||||
|   t.deepEqual(publish.args[0][1].lastRelease, lastRelease); |   t.deepEqual(publish1.args[0][1].lastRelease, lastRelease); | ||||||
|   t.deepEqual(publish.args[0][1].commits[0].hash, commits[0].hash); |   t.deepEqual(publish1.args[0][1].commits[0].hash, commits[0].hash); | ||||||
|   t.deepEqual(publish.args[0][1].commits[0].message, commits[0].message); |   t.deepEqual(publish1.args[0][1].commits[0].message, commits[0].message); | ||||||
|   t.deepEqual(publish.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes})); |   t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}}); | ||||||
|  | 
 | ||||||
|  |   t.is(success.callCount, 1); | ||||||
|  |   t.deepEqual(success.args[0][0], config); | ||||||
|  |   t.deepEqual(success.args[0][1].options, options); | ||||||
|  |   t.deepEqual(success.args[0][1].logger, t.context.logger); | ||||||
|  |   t.deepEqual(success.args[0][1].lastRelease, lastRelease); | ||||||
|  |   t.deepEqual(success.args[0][1].commits[0].hash, commits[0].hash); | ||||||
|  |   t.deepEqual(success.args[0][1].commits[0].message, commits[0].message); | ||||||
|  |   t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, ...{notes}}); | ||||||
|  |   t.deepEqual(success.args[0][1].releases, [ | ||||||
|  |     {...release1, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}}, | ||||||
|  |     {...nextRelease, ...{notes}, ...{pluginName: pluginNoop}}, | ||||||
|  |   ]); | ||||||
| 
 | 
 | ||||||
|   // Verify the tag has been created on the local and remote repo and reference the gitHead
 |   // Verify the tag has been created on the local and remote repo and reference the gitHead
 | ||||||
|   t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead); |   t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead); | ||||||
| @ -139,20 +154,16 @@ test.serial('Use custom tag format', async t => { | |||||||
| 
 | 
 | ||||||
|   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'}; |   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'}; | ||||||
|   const notes = 'Release notes'; |   const notes = 'Release notes'; | ||||||
|   const verifyConditions = stub().resolves(); |  | ||||||
|   const analyzeCommits = stub().resolves(nextRelease.type); |  | ||||||
|   const verifyRelease = stub().resolves(); |  | ||||||
|   const generateNotes = stub().resolves(notes); |  | ||||||
|   const publish = stub().resolves(); |  | ||||||
| 
 |  | ||||||
|   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`}; |   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`}; | ||||||
|   const options = { |   const options = { | ||||||
|     ...config, |     ...config, | ||||||
|     verifyConditions, |     verifyConditions: stub().resolves(), | ||||||
|     analyzeCommits, |     analyzeCommits: stub().resolves(nextRelease.type), | ||||||
|     verifyRelease, |     verifyRelease: stub().resolves(), | ||||||
|     generateNotes, |     generateNotes: stub().resolves(notes), | ||||||
|     publish, |     publish: stub().resolves(), | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -193,6 +204,8 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre | |||||||
|     verifyRelease: stub().resolves(), |     verifyRelease: stub().resolves(), | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish: [publish1, publish2], |     publish: [publish1, publish2], | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -205,19 +218,69 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre | |||||||
|   t.is(generateNotes.callCount, 2); |   t.is(generateNotes.callCount, 2); | ||||||
|   t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease); |   t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease); | ||||||
|   t.is(publish1.callCount, 1); |   t.is(publish1.callCount, 1); | ||||||
|   t.deepEqual(publish1.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes})); |   t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}}); | ||||||
| 
 | 
 | ||||||
|   nextRelease.gitHead = await getGitHead(); |   nextRelease.gitHead = await getGitHead(); | ||||||
| 
 | 
 | ||||||
|   t.deepEqual(generateNotes.secondCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes})); |   t.deepEqual(generateNotes.secondCall.args[1].nextRelease, {...nextRelease, ...{notes}}); | ||||||
|   t.is(publish2.callCount, 1); |   t.is(publish2.callCount, 1); | ||||||
|   t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes})); |   t.deepEqual(publish2.args[0][1].nextRelease, {...nextRelease, ...{notes}}); | ||||||
| 
 | 
 | ||||||
|   // Verify the tag has been created on the local and remote repo and reference the last gitHead
 |   // 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); |   t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash); | ||||||
|   t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), commits[0].hash); |   t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), commits[0].hash); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test.serial('Call all "success" plugins even if one errors out', 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
 | ||||||
|  |   await gitCommits(['First']); | ||||||
|  |   // Create the tag corresponding to version 1.0.0
 | ||||||
|  |   await gitTagVersion('v1.0.0'); | ||||||
|  |   // Add new commits to the master branch
 | ||||||
|  |   await gitCommits(['Second']); | ||||||
|  | 
 | ||||||
|  |   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; | ||||||
|  |   const notes = 'Release notes'; | ||||||
|  |   const verifyConditions1 = stub().resolves(); | ||||||
|  |   const verifyConditions2 = stub().resolves(); | ||||||
|  |   const analyzeCommits = stub().resolves(nextRelease.type); | ||||||
|  |   const generateNotes = stub().resolves(notes); | ||||||
|  |   const release = {name: 'Release', url: 'https://release.com'}; | ||||||
|  |   const publish = stub().resolves(release); | ||||||
|  |   const success1 = stub().rejects(); | ||||||
|  |   const success2 = stub().resolves(); | ||||||
|  | 
 | ||||||
|  |   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`}; | ||||||
|  |   const options = { | ||||||
|  |     ...config, | ||||||
|  |     verifyConditions: [verifyConditions1, verifyConditions2], | ||||||
|  |     analyzeCommits, | ||||||
|  |     generateNotes, | ||||||
|  |     publish, | ||||||
|  |     success: [success1, success2], | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const semanticRelease = proxyquire('..', { | ||||||
|  |     './lib/logger': t.context.logger, | ||||||
|  |     'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   await t.throws(semanticRelease(options)); | ||||||
|  | 
 | ||||||
|  |   t.is(success1.callCount, 1); | ||||||
|  |   t.deepEqual(success1.args[0][0], config); | ||||||
|  |   t.deepEqual(success1.args[0][1].releases, [ | ||||||
|  |     {...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}}, | ||||||
|  |   ]); | ||||||
|  | 
 | ||||||
|  |   t.is(success2.callCount, 1); | ||||||
|  |   t.deepEqual(success2.args[0][1].releases, [ | ||||||
|  |     {...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}}, | ||||||
|  |   ]); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test.serial('Log all "verifyConditions" errors', async t => { | test.serial('Log all "verifyConditions" errors', async t => { | ||||||
|   // Create a git repository, set the current working directory at the root of the repo
 |   // Create a git repository, set the current working directory at the root of the repo
 | ||||||
|   const repositoryUrl = await gitRepo(true); |   const repositoryUrl = await gitRepo(true); | ||||||
| @ -227,10 +290,12 @@ test.serial('Log all "verifyConditions" errors', async t => { | |||||||
|   const error1 = new Error('error 1'); |   const error1 = new Error('error 1'); | ||||||
|   const error2 = new SemanticReleaseError('error 2', 'ERR2'); |   const error2 = new SemanticReleaseError('error 2', 'ERR2'); | ||||||
|   const error3 = new SemanticReleaseError('error 3', 'ERR3'); |   const error3 = new SemanticReleaseError('error 3', 'ERR3'); | ||||||
|  |   const fail = stub().resolves(); | ||||||
|  |   const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`}; | ||||||
|   const options = { |   const options = { | ||||||
|     branch: 'master', |     ...config, | ||||||
|     repositoryUrl, |  | ||||||
|     verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)], |     verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)], | ||||||
|  |     fail, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -247,6 +312,11 @@ test.serial('Log all "verifyConditions" errors', async t => { | |||||||
|     error1, |     error1, | ||||||
|   ]); |   ]); | ||||||
|   t.true(t.context.error.calledAfter(t.context.log)); |   t.true(t.context.error.calledAfter(t.context.log)); | ||||||
|  |   t.is(fail.callCount, 1); | ||||||
|  |   t.deepEqual(fail.args[0][0], config); | ||||||
|  |   t.deepEqual(fail.args[0][1].options, options); | ||||||
|  |   t.deepEqual(fail.args[0][1].logger, t.context.logger); | ||||||
|  |   t.deepEqual(fail.args[0][1].errors, [error2, error3]); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Log all "verifyRelease" errors', async t => { | test.serial('Log all "verifyRelease" errors', async t => { | ||||||
| @ -261,12 +331,14 @@ test.serial('Log all "verifyRelease" errors', async t => { | |||||||
| 
 | 
 | ||||||
|   const error1 = new SemanticReleaseError('error 1', 'ERR1'); |   const error1 = new SemanticReleaseError('error 1', 'ERR1'); | ||||||
|   const error2 = new SemanticReleaseError('error 2', 'ERR2'); |   const error2 = new SemanticReleaseError('error 2', 'ERR2'); | ||||||
|  |   const fail = stub().resolves(); | ||||||
|  |   const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`}; | ||||||
|   const options = { |   const options = { | ||||||
|     branch: 'master', |     ...config, | ||||||
|     repositoryUrl, |  | ||||||
|     verifyConditions: stub().resolves(), |     verifyConditions: stub().resolves(), | ||||||
|     analyzeCommits: stub().resolves('major'), |     analyzeCommits: stub().resolves('major'), | ||||||
|     verifyRelease: [stub().rejects(error1), stub().rejects(error2)], |     verifyRelease: [stub().rejects(error1), stub().rejects(error2)], | ||||||
|  |     fail, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -278,9 +350,12 @@ test.serial('Log all "verifyRelease" errors', async t => { | |||||||
|   t.deepEqual(Array.from(errors), [error1, error2]); |   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 - 2], ['%s error 1', 'ERR1']); | ||||||
|   t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']); |   t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']); | ||||||
|  |   t.is(fail.callCount, 1); | ||||||
|  |   t.deepEqual(fail.args[0][0], config); | ||||||
|  |   t.deepEqual(fail.args[0][1].errors, [error1, error2]); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Dry-run skips publish', async t => { | test.serial('Dry-run skips publish and success', async t => { | ||||||
|   // Create a git repository, set the current working directory at the root of the repo
 |   // Create a git repository, set the current working directory at the root of the repo
 | ||||||
|   const repositoryUrl = await gitRepo(true); |   const repositoryUrl = await gitRepo(true); | ||||||
|   // Add commits to the master branch
 |   // Add commits to the master branch
 | ||||||
| @ -298,6 +373,7 @@ test.serial('Dry-run skips publish', async t => { | |||||||
|   const verifyRelease = stub().resolves(); |   const verifyRelease = stub().resolves(); | ||||||
|   const generateNotes = stub().resolves(notes); |   const generateNotes = stub().resolves(notes); | ||||||
|   const publish = stub().resolves(); |   const publish = stub().resolves(); | ||||||
|  |   const success = stub().resolves(); | ||||||
| 
 | 
 | ||||||
|   const options = { |   const options = { | ||||||
|     dryRun: true, |     dryRun: true, | ||||||
| @ -308,6 +384,7 @@ test.serial('Dry-run skips publish', async t => { | |||||||
|     verifyRelease, |     verifyRelease, | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish, |     publish, | ||||||
|  |     success, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -322,6 +399,41 @@ test.serial('Dry-run skips publish', async t => { | |||||||
|   t.is(verifyRelease.callCount, 1); |   t.is(verifyRelease.callCount, 1); | ||||||
|   t.is(generateNotes.callCount, 1); |   t.is(generateNotes.callCount, 1); | ||||||
|   t.is(publish.callCount, 0); |   t.is(publish.callCount, 0); | ||||||
|  |   t.is(success.callCount, 0); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test.serial('Dry-run skips fail', 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
 | ||||||
|  |   await gitCommits(['First']); | ||||||
|  |   // Create the tag corresponding to version 1.0.0
 | ||||||
|  |   await gitTagVersion('v1.0.0'); | ||||||
|  |   // Add new commits to the master branch
 | ||||||
|  |   await gitCommits(['Second']); | ||||||
|  | 
 | ||||||
|  |   const error1 = new SemanticReleaseError('error 1', 'ERR1'); | ||||||
|  |   const error2 = new SemanticReleaseError('error 2', 'ERR2'); | ||||||
|  |   const fail = stub().resolves(); | ||||||
|  | 
 | ||||||
|  |   const options = { | ||||||
|  |     dryRun: true, | ||||||
|  |     branch: 'master', | ||||||
|  |     repositoryUrl, | ||||||
|  |     verifyConditions: [stub().rejects(error1), stub().rejects(error2)], | ||||||
|  |     fail, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   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']); | ||||||
|  |   t.is(fail.callCount, 0); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t => { | test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t => { | ||||||
| @ -342,6 +454,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a | |||||||
|   const verifyRelease = stub().resolves(); |   const verifyRelease = stub().resolves(); | ||||||
|   const generateNotes = stub().resolves(notes); |   const generateNotes = stub().resolves(notes); | ||||||
|   const publish = stub().resolves(); |   const publish = stub().resolves(); | ||||||
|  |   const success = stub().resolves(); | ||||||
| 
 | 
 | ||||||
|   const options = { |   const options = { | ||||||
|     dryRun: false, |     dryRun: false, | ||||||
| @ -352,6 +465,8 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a | |||||||
|     verifyRelease, |     verifyRelease, | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish, |     publish, | ||||||
|  |     success, | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -366,6 +481,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a | |||||||
|   t.is(verifyRelease.callCount, 1); |   t.is(verifyRelease.callCount, 1); | ||||||
|   t.is(generateNotes.callCount, 1); |   t.is(generateNotes.callCount, 1); | ||||||
|   t.is(publish.callCount, 0); |   t.is(publish.callCount, 0); | ||||||
|  |   t.is(success.callCount, 0); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Allow local releases with "noCi" option', async t => { | test.serial('Allow local releases with "noCi" option', async t => { | ||||||
| @ -386,6 +502,7 @@ test.serial('Allow local releases with "noCi" option', async t => { | |||||||
|   const verifyRelease = stub().resolves(); |   const verifyRelease = stub().resolves(); | ||||||
|   const generateNotes = stub().resolves(notes); |   const generateNotes = stub().resolves(notes); | ||||||
|   const publish = stub().resolves(); |   const publish = stub().resolves(); | ||||||
|  |   const success = stub().resolves(); | ||||||
| 
 | 
 | ||||||
|   const options = { |   const options = { | ||||||
|     noCi: true, |     noCi: true, | ||||||
| @ -396,6 +513,8 @@ test.serial('Allow local releases with "noCi" option', async t => { | |||||||
|     verifyRelease, |     verifyRelease, | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish, |     publish, | ||||||
|  |     success, | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -414,6 +533,7 @@ test.serial('Allow local releases with "noCi" option', async t => { | |||||||
|   t.is(verifyRelease.callCount, 1); |   t.is(verifyRelease.callCount, 1); | ||||||
|   t.is(generateNotes.callCount, 1); |   t.is(generateNotes.callCount, 1); | ||||||
|   t.is(publish.callCount, 1); |   t.is(publish.callCount, 1); | ||||||
|  |   t.is(success.callCount, 1); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Accept "undefined" value returned by the "generateNotes" plugins', async t => { | test.serial('Accept "undefined" value returned by the "generateNotes" plugins', async t => { | ||||||
| @ -428,7 +548,6 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins', | |||||||
| 
 | 
 | ||||||
|   const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; |   const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; | ||||||
|   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; |   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; | ||||||
|   const verifyConditions = stub().resolves(); |  | ||||||
|   const analyzeCommits = stub().resolves(nextRelease.type); |   const analyzeCommits = stub().resolves(nextRelease.type); | ||||||
|   const verifyRelease = stub().resolves(); |   const verifyRelease = stub().resolves(); | ||||||
|   const generateNotes = stub().resolves(); |   const generateNotes = stub().resolves(); | ||||||
| @ -437,11 +556,13 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins', | |||||||
|   const options = { |   const options = { | ||||||
|     branch: 'master', |     branch: 'master', | ||||||
|     repositoryUrl, |     repositoryUrl, | ||||||
|     verifyConditions: [verifyConditions], |     verifyConditions: stub().resolves(), | ||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|     verifyRelease, |     verifyRelease, | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish, |     publish, | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -464,18 +585,6 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins', | |||||||
|   t.falsy(publish.args[0][1].nextRelease.notes); |   t.falsy(publish.args[0][1].nextRelease.notes); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Returns falsy value if not running from a git repository', async t => { |  | ||||||
|   // Set the current working directory to a temp directory
 |  | ||||||
|   process.chdir(tempy.directory()); |  | ||||||
| 
 |  | ||||||
|   const semanticRelease = proxyquire('..', { |  | ||||||
|     './lib/logger': t.context.logger, |  | ||||||
|     'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), |  | ||||||
|   }); |  | ||||||
|   t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'})); |  | ||||||
|   t.is(t.context.error.args[0][0], 'Semantic-release must run from a git repository.'); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test.serial('Returns falsy value if triggered by a PR', async t => { | test.serial('Returns falsy value if triggered by a PR', async t => { | ||||||
|   // Create a git repository, set the current working directory at the root of the repo
 |   // Create a git repository, set the current working directory at the root of the repo
 | ||||||
|   const repositoryUrl = await gitRepo(true); |   const repositoryUrl = await gitRepo(true); | ||||||
| @ -487,7 +596,7 @@ test.serial('Returns falsy value if triggered by a PR', async t => { | |||||||
| 
 | 
 | ||||||
|   t.falsy(await semanticRelease({repositoryUrl})); |   t.falsy(await semanticRelease({repositoryUrl})); | ||||||
|   t.is( |   t.is( | ||||||
|     t.context.log.args[6][0], |     t.context.log.args[8][0], | ||||||
|     "This run was triggered by a pull request and therefore a new version won't be published." |     "This run was triggered by a pull request and therefore a new version won't be published." | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
| @ -495,21 +604,16 @@ test.serial('Returns falsy value if triggered by a PR', async t => { | |||||||
| test.serial('Returns falsy value if not running from the configured branch', async t => { | test.serial('Returns falsy value if not running from the configured branch', async t => { | ||||||
|   // Create a git repository, set the current working directory at the root of the repo
 |   // Create a git repository, set the current working directory at the root of the repo
 | ||||||
|   const repositoryUrl = await gitRepo(true); |   const repositoryUrl = await gitRepo(true); | ||||||
| 
 |  | ||||||
|   const verifyConditions = stub().resolves(); |  | ||||||
|   const analyzeCommits = stub().resolves(); |  | ||||||
|   const verifyRelease = stub().resolves(); |  | ||||||
|   const generateNotes = stub().resolves(); |  | ||||||
|   const publish = stub().resolves(); |  | ||||||
| 
 |  | ||||||
|   const options = { |   const options = { | ||||||
|     branch: 'master', |     branch: 'master', | ||||||
|     repositoryUrl, |     repositoryUrl, | ||||||
|     verifyConditions: [verifyConditions], |     verifyConditions: stub().resolves(), | ||||||
|     analyzeCommits, |     analyzeCommits: stub().resolves(), | ||||||
|     verifyRelease, |     verifyRelease: stub().resolves(), | ||||||
|     generateNotes, |     generateNotes: stub().resolves(), | ||||||
|     publish, |     publish: stub().resolves(), | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -530,7 +634,6 @@ test.serial('Returns falsy value if there is no relevant changes', async t => { | |||||||
|   // Add commits to the master branch
 |   // Add commits to the master branch
 | ||||||
|   await gitCommits(['First']); |   await gitCommits(['First']); | ||||||
| 
 | 
 | ||||||
|   const verifyConditions = stub().resolves(); |  | ||||||
|   const analyzeCommits = stub().resolves(); |   const analyzeCommits = stub().resolves(); | ||||||
|   const verifyRelease = stub().resolves(); |   const verifyRelease = stub().resolves(); | ||||||
|   const generateNotes = stub().resolves(); |   const generateNotes = stub().resolves(); | ||||||
| @ -539,11 +642,13 @@ test.serial('Returns falsy value if there is no relevant changes', async t => { | |||||||
|   const options = { |   const options = { | ||||||
|     branch: 'master', |     branch: 'master', | ||||||
|     repositoryUrl, |     repositoryUrl, | ||||||
|     verifyConditions: [verifyConditions], |     verifyConditions: [stub().resolves()], | ||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|     verifyRelease, |     verifyRelease, | ||||||
|     generateNotes, |     generateNotes, | ||||||
|     publish, |     publish, | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -573,22 +678,17 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis | |||||||
|     'Test commit\n\n commit body\n[skip release]', |     'Test commit\n\n commit body\n[skip release]', | ||||||
|     'Test commit\n\n commit body\n[release skip]', |     'Test commit\n\n commit body\n[release skip]', | ||||||
|   ]); |   ]); | ||||||
| 
 |  | ||||||
|   const verifyConditions1 = stub().resolves(); |  | ||||||
|   const verifyConditions2 = stub().resolves(); |  | ||||||
|   const analyzeCommits = stub().resolves(); |   const analyzeCommits = stub().resolves(); | ||||||
|   const verifyRelease = stub().resolves(); |  | ||||||
|   const generateNotes = stub().resolves(); |  | ||||||
|   const publish = stub().resolves(); |  | ||||||
| 
 |  | ||||||
|   const config = {branch: 'master', repositoryUrl, globalOpt: 'global'}; |   const config = {branch: 'master', repositoryUrl, globalOpt: 'global'}; | ||||||
|   const options = { |   const options = { | ||||||
|     ...config, |     ...config, | ||||||
|     verifyConditions: [verifyConditions1, verifyConditions2], |     verifyConditions: [stub().resolves(), stub().resolves()], | ||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|     verifyRelease, |     verifyRelease: stub().resolves(), | ||||||
|     generateNotes, |     generateNotes: stub().resolves(), | ||||||
|     publish, |     publish: stub().resolves(), | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -623,12 +723,60 @@ test.serial('Hide sensitive environment variable values from the logs', async t | |||||||
| 
 | 
 | ||||||
|   await t.throws(semanticRelease(options)); |   await t.throws(semanticRelease(options)); | ||||||
| 
 | 
 | ||||||
|   t.regex(t.context.stdout.args[6][0], /Console: The token \[secure\] is invalid/); |   t.regex(t.context.stdout.args[8][0], /Console: The token \[secure\] is invalid/); | ||||||
|   t.regex(t.context.stdout.args[7][0], /Log: The token \[secure\] is invalid/); |   t.regex(t.context.stdout.args[9][0], /Log: The token \[secure\] is invalid/); | ||||||
|   t.regex(t.context.stderr.args[0][0], /Error: The token \[secure\] is invalid/); |   t.regex(t.context.stderr.args[0][0], /Error: The token \[secure\] is invalid/); | ||||||
|   t.regex(t.context.stderr.args[1][0], /Invalid token \[secure\]/); |   t.regex(t.context.stderr.args[1][0], /Invalid token \[secure\]/); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test.serial('Log both plugins errors and errors thrown by "fail" plugin', async t => { | ||||||
|  |   process.env.MY_TOKEN = 'secret token'; | ||||||
|  |   const repositoryUrl = await gitRepo(true); | ||||||
|  |   const pluginError = new SemanticReleaseError('Plugin error', 'ERR'); | ||||||
|  |   const failError1 = new Error('Fail error 1'); | ||||||
|  |   const failError2 = new Error('Fail error 2'); | ||||||
|  | 
 | ||||||
|  |   const options = { | ||||||
|  |     branch: 'master', | ||||||
|  |     repositoryUrl, | ||||||
|  |     verifyConditions: stub().rejects(pluginError), | ||||||
|  |     fail: [stub().rejects(failError1), stub().rejects(failError2)], | ||||||
|  |   }; | ||||||
|  |   const semanticRelease = proxyquire('..', { | ||||||
|  |     './lib/logger': t.context.logger, | ||||||
|  |     'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   await t.throws(semanticRelease(options)); | ||||||
|  | 
 | ||||||
|  |   t.is(t.context.error.args[t.context.error.args.length - 2][1], failError1); | ||||||
|  |   t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2); | ||||||
|  |   t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s Plugin error', 'ERR']); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test.serial('Call "fail" only if a plugin returns a SemanticReleaseError', async t => { | ||||||
|  |   process.env.MY_TOKEN = 'secret token'; | ||||||
|  |   const repositoryUrl = await gitRepo(true); | ||||||
|  |   const pluginError = new Error('Plugin error'); | ||||||
|  |   const fail = stub().resolves(); | ||||||
|  | 
 | ||||||
|  |   const options = { | ||||||
|  |     branch: 'master', | ||||||
|  |     repositoryUrl, | ||||||
|  |     verifyConditions: stub().rejects(pluginError), | ||||||
|  |     fail, | ||||||
|  |   }; | ||||||
|  |   const semanticRelease = proxyquire('..', { | ||||||
|  |     './lib/logger': t.context.logger, | ||||||
|  |     'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   await t.throws(semanticRelease(options)); | ||||||
|  | 
 | ||||||
|  |   t.true(fail.notCalled); | ||||||
|  |   t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => { | test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => { | ||||||
|   // Create a git repository, set the current working directory at the root of the repo
 |   // Create a git repository, set the current working directory at the root of the repo
 | ||||||
|   await gitRepo(); |   await gitRepo(); | ||||||
| @ -662,6 +810,8 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => { | |||||||
|     repositoryUrl, |     repositoryUrl, | ||||||
|     verifyConditions: [verifyConditions], |     verifyConditions: [verifyConditions], | ||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
| @ -672,7 +822,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => { | |||||||
| 
 | 
 | ||||||
|   // Verify error message
 |   // Verify error message
 | ||||||
|   t.regex(error.message, new RegExp(DEFINITIONS.analyzeCommits.output.message)); |   t.regex(error.message, new RegExp(DEFINITIONS.analyzeCommits.output.message)); | ||||||
|   t.regex(error.message, /Received: 'string'/); |   t.regex(error.details, /string/); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Get all commits including the ones not in the shallow clone', async t => { | test.serial('Get all commits including the ones not in the shallow clone', async t => { | ||||||
| @ -685,20 +835,18 @@ test.serial('Get all commits including the ones not in the shallow clone', async | |||||||
| 
 | 
 | ||||||
|   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; |   const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; | ||||||
|   const notes = 'Release notes'; |   const notes = 'Release notes'; | ||||||
|   const verifyConditions = stub().resolves(); |  | ||||||
|   const analyzeCommits = stub().resolves(nextRelease.type); |   const analyzeCommits = stub().resolves(nextRelease.type); | ||||||
|   const verifyRelease = stub().resolves(); |  | ||||||
|   const generateNotes = stub().resolves(notes); |  | ||||||
|   const publish = stub().resolves(); |  | ||||||
| 
 | 
 | ||||||
|   const config = {branch: 'master', repositoryUrl, globalOpt: 'global'}; |   const config = {branch: 'master', repositoryUrl, globalOpt: 'global'}; | ||||||
|   const options = { |   const options = { | ||||||
|     ...config, |     ...config, | ||||||
|     verifyConditions, |     verifyConditions: stub().resolves(), | ||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|     verifyRelease, |     verifyRelease: stub().resolves(), | ||||||
|     generateNotes, |     generateNotes: stub().resolves(notes), | ||||||
|     publish, |     publish: stub().resolves(), | ||||||
|  |     success: stub().resolves(), | ||||||
|  |     fail: stub().resolves(), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const semanticRelease = proxyquire('..', { |   const semanticRelease = proxyquire('..', { | ||||||
|  | |||||||
| @ -101,6 +101,7 @@ test.serial('Release patch, minor and major versions', async t => { | |||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: repositoryUrl}, |     repository: {url: repositoryUrl}, | ||||||
|     publishConfig: {registry: npmRegistry.url}, |     publishConfig: {registry: npmRegistry.url}, | ||||||
|  |     release: {success: false, fail: false}, | ||||||
|   }); |   }); | ||||||
|   // Create a npm-shrinkwrap.json file
 |   // Create a npm-shrinkwrap.json file
 | ||||||
|   await execa('npm', ['shrinkwrap'], {env: testEnv}); |   await execa('npm', ['shrinkwrap'], {env: testEnv}); | ||||||
| @ -298,7 +299,7 @@ test.serial('Exit with 1 if a plugin is not found', async t => { | |||||||
|     name: packageName, |     name: packageName, | ||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: `git+https://github.com/${owner}/${packageName}`}, |     repository: {url: `git+https://github.com/${owner}/${packageName}`}, | ||||||
|     release: {analyzeCommits: 'non-existing-path'}, |     release: {analyzeCommits: 'non-existing-path', success: false, fail: false}, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const {code, stderr} = await t.throws(execa(cli, [], {env})); |   const {code, stderr} = await t.throws(execa(cli, [], {env})); | ||||||
| @ -316,7 +317,7 @@ test.serial('Exit with 1 if a shareable config is not found', async t => { | |||||||
|     name: packageName, |     name: packageName, | ||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: `git+https://github.com/${owner}/${packageName}`}, |     repository: {url: `git+https://github.com/${owner}/${packageName}`}, | ||||||
|     release: {extends: 'non-existing-path'}, |     release: {extends: 'non-existing-path', success: false, fail: false}, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const {code, stderr} = await t.throws(execa(cli, [], {env})); |   const {code, stderr} = await t.throws(execa(cli, [], {env})); | ||||||
| @ -336,7 +337,7 @@ test.serial('Exit with 1 if a shareable config reference a not found plugin', as | |||||||
|     name: packageName, |     name: packageName, | ||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: `git+https://github.com/${owner}/${packageName}`}, |     repository: {url: `git+https://github.com/${owner}/${packageName}`}, | ||||||
|     release: {extends: './shareable.json'}, |     release: {extends: './shareable.json', success: false, fail: false}, | ||||||
|   }); |   }); | ||||||
|   await writeJson('./shareable.json', shareable); |   await writeJson('./shareable.json', shareable); | ||||||
| 
 | 
 | ||||||
| @ -357,6 +358,7 @@ test.serial('Dry-run', async t => { | |||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: repositoryUrl}, |     repository: {url: repositoryUrl}, | ||||||
|     publishConfig: {registry: npmRegistry.url}, |     publishConfig: {registry: npmRegistry.url}, | ||||||
|  |     release: {success: false, fail: false}, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   /* Initial release */ |   /* Initial release */ | ||||||
| @ -394,6 +396,7 @@ test.serial('Allow local releases with "noCi" option', async t => { | |||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: repositoryUrl}, |     repository: {url: repositoryUrl}, | ||||||
|     publishConfig: {registry: npmRegistry.url}, |     publishConfig: {registry: npmRegistry.url}, | ||||||
|  |     release: {success: false, fail: false}, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   /* Initial release */ |   /* Initial release */ | ||||||
| @ -459,7 +462,17 @@ test.serial('Pass options via CLI arguments', async t => { | |||||||
|   t.log('$ semantic-release'); |   t.log('$ semantic-release'); | ||||||
|   const {stdout, code} = await execa( |   const {stdout, code} = await execa( | ||||||
|     cli, |     cli, | ||||||
|     ['--verify-conditions', '@semantic-release/npm', '--publish', '@semantic-release/npm', '--debug'], |     [ | ||||||
|  |       '--verify-conditions', | ||||||
|  |       '@semantic-release/npm', | ||||||
|  |       '--publish', | ||||||
|  |       '@semantic-release/npm', | ||||||
|  |       `--success`, | ||||||
|  |       false, | ||||||
|  |       `--fail`, | ||||||
|  |       false, | ||||||
|  |       '--debug', | ||||||
|  |     ], | ||||||
|     {env} |     {env} | ||||||
|   ); |   ); | ||||||
|   t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`)); |   t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`)); | ||||||
| @ -515,7 +528,7 @@ test.serial('Run via JS API', async t => { | |||||||
|   t.log('Commit a feature'); |   t.log('Commit a feature'); | ||||||
|   await gitCommits(['feat: Initial commit']); |   await gitCommits(['feat: Initial commit']); | ||||||
|   t.log('$ Call semantic-release via API'); |   t.log('$ Call semantic-release via API'); | ||||||
|   await semanticRelease(); |   await semanticRelease({fail: false, success: false}); | ||||||
| 
 | 
 | ||||||
|   // Verify package.json and has been updated
 |   // Verify package.json and has been updated
 | ||||||
|   t.is((await readJson('./package.json')).version, version); |   t.is((await readJson('./package.json')).version, version); | ||||||
| @ -545,7 +558,7 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => { | |||||||
|     name: packageName, |     name: packageName, | ||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: repositoryUrl}, |     repository: {url: repositoryUrl}, | ||||||
|     release: {verifyConditions: pluginError}, |     release: {verifyConditions: pluginError, fail: false, success: false}, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   /* Initial release */ |   /* Initial release */ | ||||||
| @ -572,7 +585,7 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 1', async | |||||||
|     name: packageName, |     name: packageName, | ||||||
|     version: '0.0.0-dev', |     version: '0.0.0-dev', | ||||||
|     repository: {url: repositoryUrl}, |     repository: {url: repositoryUrl}, | ||||||
|     release: {verifyConditions: pluginInheritedError}, |     release: {verifyConditions: pluginInheritedError, fail: false, success: false}, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   /* Initial release */ |   /* Initial release */ | ||||||
|  | |||||||
| @ -1,77 +0,0 @@ | |||||||
| import test from 'ava'; |  | ||||||
| import definitions from '../../lib/plugins/definitions'; |  | ||||||
| 
 |  | ||||||
| test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => { |  | ||||||
|   t.false(definitions.verifyConditions.config.validator({})); |  | ||||||
|   t.false(definitions.verifyConditions.config.validator({path: null})); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.verifyConditions.config.validator({path: 'plugin-path.js'})); |  | ||||||
|   t.true(definitions.verifyConditions.config.validator()); |  | ||||||
|   t.true(definitions.verifyConditions.config.validator('plugin-path.js')); |  | ||||||
|   t.true(definitions.verifyConditions.config.validator(() => {})); |  | ||||||
|   t.true(definitions.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => { |  | ||||||
|   t.false(definitions.analyzeCommits.config.validator({})); |  | ||||||
|   t.false(definitions.analyzeCommits.config.validator({path: null})); |  | ||||||
|   t.false(definitions.analyzeCommits.config.validator([])); |  | ||||||
|   t.false(definitions.analyzeCommits.config.validator()); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.analyzeCommits.config.validator({path: 'plugin-path.js'})); |  | ||||||
|   t.true(definitions.analyzeCommits.config.validator('plugin-path.js')); |  | ||||||
|   t.true(definitions.analyzeCommits.config.validator(() => {})); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => { |  | ||||||
|   t.false(definitions.verifyRelease.config.validator({})); |  | ||||||
|   t.false(definitions.verifyRelease.config.validator({path: null})); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.verifyRelease.config.validator({path: 'plugin-path.js'})); |  | ||||||
|   t.true(definitions.verifyRelease.config.validator()); |  | ||||||
|   t.true(definitions.verifyRelease.config.validator('plugin-path.js')); |  | ||||||
|   t.true(definitions.verifyRelease.config.validator(() => {})); |  | ||||||
|   t.true(definitions.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => { |  | ||||||
|   t.false(definitions.generateNotes.config.validator({})); |  | ||||||
|   t.false(definitions.generateNotes.config.validator({path: null})); |  | ||||||
|   t.false(definitions.generateNotes.config.validator([])); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.generateNotes.config.validator()); |  | ||||||
|   t.true(definitions.generateNotes.config.validator({path: 'plugin-path.js'})); |  | ||||||
|   t.true(definitions.generateNotes.config.validator('plugin-path.js')); |  | ||||||
|   t.true(definitions.generateNotes.config.validator(() => {})); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => { |  | ||||||
|   t.false(definitions.publish.config.validator({})); |  | ||||||
|   t.false(definitions.publish.config.validator({path: null})); |  | ||||||
|   t.false(definitions.publish.config.validator()); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.publish.config.validator({path: 'plugin-path.js'})); |  | ||||||
|   t.true(definitions.publish.config.validator('plugin-path.js')); |  | ||||||
|   t.true(definitions.publish.config.validator(() => {})); |  | ||||||
|   t.true(definitions.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => { |  | ||||||
|   t.false(definitions.analyzeCommits.output.validator('invalid')); |  | ||||||
|   t.false(definitions.analyzeCommits.output.validator(1)); |  | ||||||
|   t.false(definitions.analyzeCommits.output.validator({})); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.analyzeCommits.output.validator()); |  | ||||||
|   t.true(definitions.analyzeCommits.output.validator(null)); |  | ||||||
|   t.true(definitions.analyzeCommits.output.validator('major')); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test('The "generateNotes" plugin output, if defined, must be a string', t => { |  | ||||||
|   t.false(definitions.generateNotes.output.validator(1)); |  | ||||||
|   t.false(definitions.generateNotes.output.validator({})); |  | ||||||
| 
 |  | ||||||
|   t.true(definitions.generateNotes.output.validator()); |  | ||||||
|   t.true(definitions.generateNotes.output.validator(null)); |  | ||||||
|   t.true(definitions.generateNotes.output.validator('')); |  | ||||||
|   t.true(definitions.generateNotes.output.validator('string')); |  | ||||||
| }); |  | ||||||
| @ -12,6 +12,7 @@ test.beforeEach(t => { | |||||||
| test('Normalize and load plugin from string', t => { | test('Normalize and load plugin from string', t => { | ||||||
|   const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/plugin-noop', t.context.logger); |   const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/plugin-noop', t.context.logger); | ||||||
| 
 | 
 | ||||||
|  |   t.is(plugin.pluginName, './test/fixtures/plugin-noop'); | ||||||
|   t.is(typeof plugin, 'function'); |   t.is(typeof plugin, 'function'); | ||||||
|   t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']); |   t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']); | ||||||
| }); | }); | ||||||
| @ -19,6 +20,7 @@ test('Normalize and load plugin from string', t => { | |||||||
| test('Normalize and load plugin from object', t => { | test('Normalize and load plugin from object', t => { | ||||||
|   const plugin = normalize('publish', {}, {}, {path: './test/fixtures/plugin-noop'}, t.context.logger); |   const plugin = normalize('publish', {}, {}, {path: './test/fixtures/plugin-noop'}, t.context.logger); | ||||||
| 
 | 
 | ||||||
|  |   t.is(plugin.pluginName, './test/fixtures/plugin-noop'); | ||||||
|   t.is(typeof plugin, 'function'); |   t.is(typeof plugin, 'function'); | ||||||
|   t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']); |   t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']); | ||||||
| }); | }); | ||||||
| @ -32,6 +34,7 @@ test('Normalize and load plugin from a base file path', t => { | |||||||
|     t.context.logger |     t.context.logger | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|  |   t.is(plugin.pluginName, './plugin-noop'); | ||||||
|   t.is(typeof plugin, 'function'); |   t.is(typeof plugin, 'function'); | ||||||
|   t.deepEqual(t.context.log.args[0], [ |   t.deepEqual(t.context.log.args[0], [ | ||||||
|     'Load plugin %s from %s in shareable config %s', |     'Load plugin %s from %s in shareable config %s', | ||||||
| @ -41,9 +44,40 @@ test('Normalize and load plugin from a base file path', t => { | |||||||
|   ]); |   ]); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Normalize and load plugin from function', t => { | test('Wrap plugin in a function that add the "pluginName" to the error"', async t => { | ||||||
|   const plugin = normalize('', {}, {}, () => {}, t.context.logger); |   const plugin = normalize( | ||||||
|  |     'verifyConditions', | ||||||
|  |     {'./plugin-error': './test/fixtures'}, | ||||||
|  |     {}, | ||||||
|  |     './plugin-error', | ||||||
|  |     t.context.logger | ||||||
|  |   ); | ||||||
| 
 | 
 | ||||||
|  |   const error = await t.throws(plugin()); | ||||||
|  | 
 | ||||||
|  |   t.is(error.pluginName, './plugin-error'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async t => { | ||||||
|  |   const plugin = normalize( | ||||||
|  |     'verifyConditions', | ||||||
|  |     {'./plugin-errors': './test/fixtures'}, | ||||||
|  |     {}, | ||||||
|  |     './plugin-errors', | ||||||
|  |     t.context.logger | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const errors = [...(await t.throws(plugin()))]; | ||||||
|  |   for (const error of errors) { | ||||||
|  |     t.is(error.pluginName, './plugin-errors'); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Normalize and load plugin from function', t => { | ||||||
|  |   const pluginFunction = () => {}; | ||||||
|  |   const plugin = normalize('', {}, {}, pluginFunction, t.context.logger); | ||||||
|  | 
 | ||||||
|  |   t.is(plugin.pluginName, '[Function: pluginFunction]'); | ||||||
|   t.is(typeof plugin, 'function'); |   t.is(typeof plugin, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| @ -54,18 +88,42 @@ test('Normalize and load plugin that retuns multiple functions', t => { | |||||||
|   t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']); |   t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Wrap plugin in a function that validate the output of the plugin', async t => { | test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => { | ||||||
|   const pluginFunction = stub().resolves(1); |   const analyzeCommits = stub().resolves(2); | ||||||
|   const plugin = normalize('', {}, {}, pluginFunction, t.context.logger, { |   const plugin = normalize('analyzeCommits', {}, {}, analyzeCommits, t.context.logger); | ||||||
|     validator: output => output === 1, | 
 | ||||||
|     message: 'The output must be 1.', |   const error = await t.throws(plugin()); | ||||||
|  | 
 | ||||||
|  |   t.is(error.code, 'EANALYZEOUTPUT'); | ||||||
|  |   t.is(error.name, 'SemanticReleaseError'); | ||||||
|  |   t.regex(error.details, /2/); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|   await t.notThrows(plugin()); | test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => { | ||||||
|  |   const generateNotes = stub().resolves(2); | ||||||
|  |   const plugin = normalize('generateNotes', {}, {}, generateNotes, t.context.logger); | ||||||
| 
 | 
 | ||||||
|   pluginFunction.resolves(2); |   const error = await t.throws(plugin()); | ||||||
|   const error = await t.throws(plugin(), Error); | 
 | ||||||
|   t.is(error.message, 'The output must be 1. Received: 2'); |   t.is(error.code, 'ERELEASENOTESOUTPUT'); | ||||||
|  |   t.is(error.name, 'SemanticReleaseError'); | ||||||
|  |   t.regex(error.details, /2/); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => { | ||||||
|  |   const plugin = normalize( | ||||||
|  |     'publish', | ||||||
|  |     {'./plugin-identity': './test/fixtures'}, | ||||||
|  |     {}, | ||||||
|  |     './plugin-identity', | ||||||
|  |     t.context.logger | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const error = await t.throws(plugin(2)); | ||||||
|  | 
 | ||||||
|  |   t.is(error.code, 'EPUBLISHOUTPUT'); | ||||||
|  |   t.is(error.name, 'SemanticReleaseError'); | ||||||
|  |   t.regex(error.details, /2/); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => { | test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => { | ||||||
| @ -127,12 +185,8 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async | |||||||
| test('Throws an error if the plugin return an object without the expected plugin function', t => { | test('Throws an error if the plugin return an object without the expected plugin function', t => { | ||||||
|   const error = t.throws(() => normalize('inexistantPlugin', {}, {}, './test/fixtures/multi-plugin', t.context.logger)); |   const error = t.throws(() => normalize('inexistantPlugin', {}, {}, './test/fixtures/multi-plugin', t.context.logger)); | ||||||
| 
 | 
 | ||||||
|   t.is(error.code, 'EPLUGINCONF'); |   t.is(error.code, 'EPLUGIN'); | ||||||
|   t.is(error.name, 'SemanticReleaseError'); |   t.is(error.name, 'SemanticReleaseError'); | ||||||
|   t.is( |  | ||||||
|     error.message, |  | ||||||
|     'The inexistantPlugin plugin must be a function, or an object with a function in the property inexistantPlugin.' |  | ||||||
|   ); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Throws an error if the plugin is not found', t => { | test('Throws an error if the plugin is not found', t => { | ||||||
|  | |||||||
| @ -18,13 +18,32 @@ test('Execute each function in series passing the same input', async t => { | |||||||
|   t.true(step2.calledBefore(step3)); |   t.true(step2.calledBefore(step3)); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Execute each function in series passing a transformed input', async t => { | test('With one step, returns the step values rather than an Array ', async t => { | ||||||
|  |   const step1 = stub().resolves(1); | ||||||
|  | 
 | ||||||
|  |   const result = await pipeline([step1])(0); | ||||||
|  | 
 | ||||||
|  |   t.deepEqual(result, 1); | ||||||
|  |   t.true(step1.calledWith(0)); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('With one step, throws the error rather than an AggregateError ', async t => { | ||||||
|  |   const error = new Error('test error 1'); | ||||||
|  |   const step1 = stub().rejects(error); | ||||||
|  | 
 | ||||||
|  |   const thrown = await t.throws(pipeline([step1])(0)); | ||||||
|  | 
 | ||||||
|  |   t.is(error, thrown); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Execute each function in series passing a transformed input from "getNextInput"', async t => { | ||||||
|   const step1 = stub().resolves(1); |   const step1 = stub().resolves(1); | ||||||
|   const step2 = stub().resolves(2); |   const step2 = stub().resolves(2); | ||||||
|   const step3 = stub().resolves(3); |   const step3 = stub().resolves(3); | ||||||
|   const step4 = stub().resolves(4); |   const step4 = stub().resolves(4); | ||||||
|  |   const getNextInput = (lastResult, result) => lastResult + result; | ||||||
| 
 | 
 | ||||||
|   const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => prevResult + result); |   const result = await pipeline([step1, step2, step3, step4])(0, {settleAll: false, getNextInput}); | ||||||
| 
 | 
 | ||||||
|   t.deepEqual(result, [1, 2, 3, 4]); |   t.deepEqual(result, [1, 2, 3, 4]); | ||||||
|   t.true(step1.calledWith(0)); |   t.true(step1.calledWith(0)); | ||||||
| @ -36,22 +55,45 @@ test('Execute each function in series passing a transformed input', async t => { | |||||||
|   t.true(step3.calledBefore(step4)); |   t.true(step3.calledBefore(step4)); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Execute each function in series passing the result of the previous one', async t => { | test('Execute each function in series passing the "lastResult" and "result" to "getNextInput"', async t => { | ||||||
|   const step1 = stub().resolves(1); |   const step1 = stub().resolves(1); | ||||||
|   const step2 = stub().resolves(2); |   const step2 = stub().resolves(2); | ||||||
|   const step3 = stub().resolves(3); |   const step3 = stub().resolves(3); | ||||||
|   const step4 = stub().resolves(4); |   const step4 = stub().resolves(4); | ||||||
|  |   const getNextInput = stub().returnsArg(0); | ||||||
| 
 | 
 | ||||||
|   const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => result); |   const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: false, getNextInput}); | ||||||
| 
 | 
 | ||||||
|   t.deepEqual(result, [1, 2, 3, 4]); |   t.deepEqual(result, [1, 2, 3, 4]); | ||||||
|   t.true(step1.calledWith(0)); |   t.deepEqual(getNextInput.args, [[5, 1], [5, 2], [5, 3], [5, 4]]); | ||||||
|   t.true(step2.calledWith(1)); | }); | ||||||
|   t.true(step3.calledWith(2)); | 
 | ||||||
|   t.true(step4.calledWith(3)); | test('Execute each function in series calling "transform" to modify the results', async t => { | ||||||
|   t.true(step1.calledBefore(step2)); |   const step1 = stub().resolves(1); | ||||||
|   t.true(step2.calledBefore(step3)); |   const step2 = stub().resolves(2); | ||||||
|   t.true(step3.calledBefore(step4)); |   const step3 = stub().resolves(3); | ||||||
|  |   const step4 = stub().resolves(4); | ||||||
|  |   const getNextInput = stub().returnsArg(0); | ||||||
|  |   const transform = stub().callsFake(result => result + 1); | ||||||
|  | 
 | ||||||
|  |   const result = await pipeline([step1, step2, step3, step4])(5, {getNextInput, transform}); | ||||||
|  | 
 | ||||||
|  |   t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]); | ||||||
|  |   t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Execute each function in series calling "transform" to modify the results with "settleAll"', async t => { | ||||||
|  |   const step1 = stub().resolves(1); | ||||||
|  |   const step2 = stub().resolves(2); | ||||||
|  |   const step3 = stub().resolves(3); | ||||||
|  |   const step4 = stub().resolves(4); | ||||||
|  |   const getNextInput = stub().returnsArg(0); | ||||||
|  |   const transform = stub().callsFake(result => result + 1); | ||||||
|  | 
 | ||||||
|  |   const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: true, getNextInput, transform}); | ||||||
|  | 
 | ||||||
|  |   t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]); | ||||||
|  |   t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Stop execution and throw error is a step rejects', async t => { | test('Stop execution and throw error is a step rejects', async t => { | ||||||
| @ -89,7 +131,7 @@ test('Execute all even if a Promise rejects', async t => { | |||||||
|   const step2 = stub().rejects(error1); |   const step2 = stub().rejects(error1); | ||||||
|   const step3 = stub().rejects(error2); |   const step3 = stub().rejects(error2); | ||||||
| 
 | 
 | ||||||
|   const errors = await t.throws(pipeline([step1, step2, step3])(0, true)); |   const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true})); | ||||||
| 
 | 
 | ||||||
|   t.deepEqual(Array.from(errors), [error1, error2]); |   t.deepEqual(Array.from(errors), [error1, error2]); | ||||||
|   t.true(step1.calledWith(0)); |   t.true(step1.calledWith(0)); | ||||||
| @ -105,7 +147,7 @@ test('Throw all errors from all steps throwing an AggregateError', async t => { | |||||||
|   const step1 = stub().rejects(new AggregateError([error1, error2])); |   const step1 = stub().rejects(new AggregateError([error1, error2])); | ||||||
|   const step2 = stub().rejects(new AggregateError([error3, error4])); |   const step2 = stub().rejects(new AggregateError([error3, error4])); | ||||||
| 
 | 
 | ||||||
|   const errors = await t.throws(pipeline([step1, step2])(0, true)); |   const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true})); | ||||||
| 
 | 
 | ||||||
|   t.deepEqual(Array.from(errors), [error1, error2, error3, error4]); |   t.deepEqual(Array.from(errors), [error1, error2, error3, error4]); | ||||||
|   t.true(step1.calledWith(0)); |   t.true(step1.calledWith(0)); | ||||||
| @ -119,10 +161,9 @@ test('Execute each function in series passing a transformed input even if a step | |||||||
|   const step2 = stub().rejects(error2); |   const step2 = stub().rejects(error2); | ||||||
|   const step3 = stub().rejects(error3); |   const step3 = stub().rejects(error3); | ||||||
|   const step4 = stub().resolves(4); |   const step4 = stub().resolves(4); | ||||||
|  |   const getNextInput = (prevResult, result) => prevResult + result; | ||||||
| 
 | 
 | ||||||
|   const errors = await t.throws( |   const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput})); | ||||||
|     pipeline([step1, step2, step3, step4])(0, true, (prevResult, result) => prevResult + result) |  | ||||||
|   ); |  | ||||||
| 
 | 
 | ||||||
|   t.deepEqual(Array.from(errors), [error2, error3]); |   t.deepEqual(Array.from(errors), [error2, error3]); | ||||||
|   t.true(step1.calledWith(0)); |   t.true(step1.calledWith(0)); | ||||||
|  | |||||||
| @ -28,6 +28,8 @@ test('Export default plugins', t => { | |||||||
|   t.is(typeof plugins.verifyRelease, 'function'); |   t.is(typeof plugins.verifyRelease, 'function'); | ||||||
|   t.is(typeof plugins.generateNotes, 'function'); |   t.is(typeof plugins.generateNotes, 'function'); | ||||||
|   t.is(typeof plugins.publish, 'function'); |   t.is(typeof plugins.publish, 'function'); | ||||||
|  |   t.is(typeof plugins.success, 'function'); | ||||||
|  |   t.is(typeof plugins.fail, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Export plugins based on config', t => { | test('Export plugins based on config', t => { | ||||||
| @ -48,6 +50,8 @@ test('Export plugins based on config', t => { | |||||||
|   t.is(typeof plugins.verifyRelease, 'function'); |   t.is(typeof plugins.verifyRelease, 'function'); | ||||||
|   t.is(typeof plugins.generateNotes, 'function'); |   t.is(typeof plugins.generateNotes, 'function'); | ||||||
|   t.is(typeof plugins.publish, 'function'); |   t.is(typeof plugins.publish, 'function'); | ||||||
|  |   t.is(typeof plugins.success, 'function'); | ||||||
|  |   t.is(typeof plugins.fail, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Export plugins loaded from the dependency of a shareable config module', async t => { | test.serial('Export plugins loaded from the dependency of a shareable config module', async t => { | ||||||
| @ -76,6 +80,8 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod | |||||||
|   t.is(typeof plugins.verifyRelease, 'function'); |   t.is(typeof plugins.verifyRelease, 'function'); | ||||||
|   t.is(typeof plugins.generateNotes, 'function'); |   t.is(typeof plugins.generateNotes, 'function'); | ||||||
|   t.is(typeof plugins.publish, 'function'); |   t.is(typeof plugins.publish, 'function'); | ||||||
|  |   t.is(typeof plugins.success, 'function'); | ||||||
|  |   t.is(typeof plugins.fail, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Export plugins loaded from the dependency of a shareable config file', async t => { | test.serial('Export plugins loaded from the dependency of a shareable config file', async t => { | ||||||
| @ -101,6 +107,8 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil | |||||||
|   t.is(typeof plugins.verifyRelease, 'function'); |   t.is(typeof plugins.verifyRelease, 'function'); | ||||||
|   t.is(typeof plugins.generateNotes, 'function'); |   t.is(typeof plugins.generateNotes, 'function'); | ||||||
|   t.is(typeof plugins.publish, 'function'); |   t.is(typeof plugins.publish, 'function'); | ||||||
|  |   t.is(typeof plugins.success, 'function'); | ||||||
|  |   t.is(typeof plugins.fail, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Use default when only options are passed for a single plugin', t => { | test('Use default when only options are passed for a single plugin', t => { | ||||||
| @ -128,22 +136,10 @@ test('Merge global options with plugin options', async t => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => { | test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => { | ||||||
|   const errors = Array.from( |   const errors = Array.from(t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger))); | ||||||
|     t.throws(() => getPlugins({verifyConditions: {}, verifyRelease: {}}, {}, t.context.logger)) |  | ||||||
|   ); |  | ||||||
| 
 | 
 | ||||||
|   t.is(errors[0].name, 'SemanticReleaseError'); |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[0].code, 'EPLUGINCONF'); |   t.is(errors[0].code, 'EPLUGINCONF'); | ||||||
|   t.is( |  | ||||||
|     errors[0].message, |  | ||||||
|     'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.' |  | ||||||
|   ); |  | ||||||
|   t.is(errors[1].name, 'SemanticReleaseError'); |  | ||||||
|   t.is(errors[1].code, 'EPLUGINCONF'); |  | ||||||
|   t.is( |  | ||||||
|     errors[1].message, |  | ||||||
|     'The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.' |  | ||||||
|   ); |  | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => { | test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => { | ||||||
| @ -153,8 +149,4 @@ test('Throw an error if an array of plugin configuration is missing a path for p | |||||||
| 
 | 
 | ||||||
|   t.is(errors[0].name, 'SemanticReleaseError'); |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[0].code, 'EPLUGINCONF'); |   t.is(errors[0].code, 'EPLUGINCONF'); | ||||||
|   t.is( |  | ||||||
|     errors[0].message, |  | ||||||
|     'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.' |  | ||||||
|   ); |  | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -29,29 +29,29 @@ test.afterEach.always(() => { | |||||||
|   process.chdir(cwd); |   process.chdir(cwd); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Return "false" if does not run on a git repository', async t => { |  | ||||||
|   const dir = tempy.directory(); |  | ||||||
|   process.chdir(dir); |  | ||||||
| 
 |  | ||||||
|   t.false(await verify({}, 'master', t.context.logger)); |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| test.serial('Throw a AggregateError', async t => { | test.serial('Throw a AggregateError', async t => { | ||||||
|   await gitRepo(); |   await gitRepo(); | ||||||
| 
 | 
 | ||||||
|   const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger))); |   const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger))); | ||||||
| 
 | 
 | ||||||
|   t.is(errors[0].name, 'SemanticReleaseError'); |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[0].message, 'The repositoryUrl option is required'); |  | ||||||
|   t.is(errors[0].code, 'ENOREPOURL'); |   t.is(errors[0].code, 'ENOREPOURL'); | ||||||
|   t.is(errors[1].name, 'SemanticReleaseError'); |   t.is(errors[1].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[1].message, 'The tagFormat template must compile to a valid Git tag format'); |  | ||||||
|   t.is(errors[1].code, 'EINVALIDTAGFORMAT'); |   t.is(errors[1].code, 'EINVALIDTAGFORMAT'); | ||||||
|   t.is(errors[2].name, 'SemanticReleaseError'); |   t.is(errors[2].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[2].message, `The tagFormat template must contain the variable "\${version}" exactly once`); |  | ||||||
|   t.is(errors[2].code, 'ETAGNOVERSION'); |   t.is(errors[2].code, 'ETAGNOVERSION'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test.serial('Throw a SemanticReleaseError if does not run on a git repository', async t => { | ||||||
|  |   const dir = tempy.directory(); | ||||||
|  |   process.chdir(dir); | ||||||
|  | 
 | ||||||
|  |   const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger))); | ||||||
|  | 
 | ||||||
|  |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|  |   t.is(errors[0].code, 'ENOGITREPO'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => { | test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => { | ||||||
|   const repositoryUrl = await gitRepo(true); |   const repositoryUrl = await gitRepo(true); | ||||||
|   const options = {repositoryUrl, tagFormat: `?\${version}`}; |   const options = {repositoryUrl, tagFormat: `?\${version}`}; | ||||||
| @ -59,7 +59,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', asyn | |||||||
|   const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger))); |   const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger))); | ||||||
| 
 | 
 | ||||||
|   t.is(errors[0].name, 'SemanticReleaseError'); |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[0].message, 'The tagFormat template must compile to a valid Git tag format'); |  | ||||||
|   t.is(errors[0].code, 'EINVALIDTAGFORMAT'); |   t.is(errors[0].code, 'EINVALIDTAGFORMAT'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| @ -70,7 +69,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains t | |||||||
|   const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger))); |   const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger))); | ||||||
| 
 | 
 | ||||||
|   t.is(errors[0].name, 'SemanticReleaseError'); |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`); |  | ||||||
|   t.is(errors[0].code, 'ETAGNOVERSION'); |   t.is(errors[0].code, 'ETAGNOVERSION'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| @ -81,7 +79,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple " | |||||||
|   const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger))); |   const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger))); | ||||||
| 
 | 
 | ||||||
|   t.is(errors[0].name, 'SemanticReleaseError'); |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`); |  | ||||||
|   t.is(errors[0].code, 'ETAGNOVERSION'); |   t.is(errors[0].code, 'ETAGNOVERSION'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user