feat: add new plugins option
				
					
				
			This commit is contained in:
		
							parent
							
								
									9930dac69e
								
							
						
					
					
						commit
						5ba5010c80
					
				
							
								
								
									
										24
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								README.md
									
									
									
									
									
								
							| @ -81,19 +81,19 @@ If you need more control over the timing of releases you have a couple of option | |||||||
| 
 | 
 | ||||||
| ### Release steps | ### Release steps | ||||||
| 
 | 
 | ||||||
| After running the tests the command `semantic-release` will execute the following steps: | After running the tests, the command `semantic-release` will execute the following steps: | ||||||
| 
 | 
 | ||||||
| | Step              | Description                                                                                                                                                           | | | Step              | Description                                                                                                                     | | ||||||
| |-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |-------------------|---------------------------------------------------------------------------------------------------------------------------------| | ||||||
| | Verify Conditions | Verify all the conditions to proceed with the release with the [verify conditions plugins](docs/usage/plugins.md#verifyconditions-plugin).                            | | | Verify Conditions | Verify all the conditions to proceed with the release.                                                                          | | ||||||
| | Get last release  | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging).                                       | | | Get last release  | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). | | ||||||
| | Analyze commits   | Determine the type of release with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release.       | | | Analyze commits   | Determine the type of release based on the commits added since the last release.                                                | | ||||||
| | Verify release    | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin).                                                          | | | Verify release    | Verify the release conformity.                                                                                                  | | ||||||
| | 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 for the commits added since the last release.                                                            | | ||||||
| | Create Git tag    | Create a Git tag corresponding to the new release version                                                                                                             | | | Create Git tag    | Create a Git tag corresponding to the new release version.                                                                      | | ||||||
| | Prepare           | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin).                                                                                 | | | Prepare           | Prepare the release.                                                                                                            | | ||||||
| | Publish           | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin).                                                                                 | | | Publish           | Publish the release.                                                                                                            | | ||||||
| | 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.                      | | | Notify            | Notify of new releases or errors.                                                                                               | | ||||||
| 
 | 
 | ||||||
| ## Documentation | ## Documentation | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								cli.js
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								cli.js
									
									
									
									
									
								
							| @ -22,6 +22,7 @@ Usage: | |||||||
|     .option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'}) |     .option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'}) | ||||||
|     .option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'}) |     .option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'}) | ||||||
|     .option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'}) |     .option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'}) | ||||||
|  |     .option('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'}) | ||||||
|     .option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'}) |     .option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'}) | ||||||
|     .option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'}) |     .option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'}) | ||||||
|     .option('verify-conditions', {...stringList, group: 'Plugins'}) |     .option('verify-conditions', {...stringList, group: 'Plugins'}) | ||||||
|  | |||||||
| @ -92,6 +92,18 @@ The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by | |||||||
| 
 | 
 | ||||||
| **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). | **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). | ||||||
| 
 | 
 | ||||||
|  | ### plugins | ||||||
|  | 
 | ||||||
|  | Type: `Array`<br> | ||||||
|  | Default: `['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/npm', '@semantic-release/github']`<br> | ||||||
|  | CLI arguments: `-p`, `--plugins` | ||||||
|  | 
 | ||||||
|  | Define the list of plugins to use. Plugins will run in series, in the order defined, for each [steps](../../README.md#release-steps) if they implement it. | ||||||
|  | 
 | ||||||
|  | Plugins configuration can defined by wrapping the name and an options object in an array. | ||||||
|  | 
 | ||||||
|  | See [Plugins configuration](plugins.md#configuration) for more details. | ||||||
|  | 
 | ||||||
| ### dryRun | ### dryRun | ||||||
| 
 | 
 | ||||||
| Type: `Boolean`<br> | Type: `Boolean`<br> | ||||||
|  | |||||||
| @ -1,114 +1,78 @@ | |||||||
| # Plugins | # Plugins | ||||||
| 
 | 
 | ||||||
| Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms. | Each [release step](../../README.md#release-steps) is implemented by configurable plugins. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms. | ||||||
| 
 | 
 | ||||||
| See [plugins list](../extending/plugins-list.md). | A plugin is a npm module that can implement one or more of the following steps: | ||||||
| 
 | 
 | ||||||
| ## Plugin types | | Step               | Accept multiple | Required | Description                                                                                                                                                                                   | | ||||||
|  | |--------------------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||||||
|  | | `verifyConditions` | Yes             | No       | Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...                                                  | | ||||||
|  | | `analyzeCommits`   | No              | Yes      | Responsible for determining the type of the next release (`major`, `minor` or `patch`).                                                                                                       | | ||||||
|  | | `verifyRelease`    | Yes             | No       | Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published.                                                                       | | ||||||
|  | | `generateNotes`    | Yes             | No       | Responsible for generating the content of the release note. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of each plugin output. | | ||||||
|  | | `prepare`          | Yes             | No       | Responsible for preparing the release, for example creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets and pushing a commit.                  | | ||||||
|  | | `publish`          | Yes             | No       | Responsible for publishing the release.                                                                                                                                                       | | ||||||
|  | | `success`          | Yes             | No       | Responsible for notifying of a new release.                                                                                                                                                   | | ||||||
|  | | `fail`             | Yes             | No       | Responsible for notifying of a failed release.                                                                                                                                                | | ||||||
| 
 | 
 | ||||||
| ### verifyConditions plugin | See [available plugins](../extending/plugins-list.md). | ||||||
| 
 | 
 | ||||||
| Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc... | ## Plugins configuration | ||||||
| 
 | 
 | ||||||
| Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).<br> | Each plugin must be installed and configured with the [`plugins` options](./configuration.md#plugins) by specifying the list of plugins by npm module name. | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 | 
 | ||||||
| ### analyzeCommits plugin | ```bash | ||||||
|  | $ npm install @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm -D | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| Responsible for determining the type of the next release (`major`, `minor` or `patch`). |  | ||||||
| 
 |  | ||||||
| Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).<br> |  | ||||||
| Required.<br> |  | ||||||
| Accept only one plugin. |  | ||||||
| 
 |  | ||||||
| ### verifyRelease plugin |  | ||||||
| 
 |  | ||||||
| Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`. |  | ||||||
| 
 |  | ||||||
| Default implementation: none.<br> |  | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 |  | ||||||
| ### generateNotes plugin |  | ||||||
| 
 |  | ||||||
| Responsible for generating release notes. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of plugin output. |  | ||||||
| 
 |  | ||||||
| Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).<br> |  | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 |  | ||||||
| ### prepare plugin |  | ||||||
| 
 |  | ||||||
| Responsible for preparing the release, including: |  | ||||||
| - Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets. |  | ||||||
| - Create and push commits |  | ||||||
| 
 |  | ||||||
| Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).<br> |  | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 |  | ||||||
| ### publish plugin |  | ||||||
| 
 |  | ||||||
| Responsible for publishing the release. |  | ||||||
| 
 |  | ||||||
| Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).<br> |  | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 |  | ||||||
| ### success plugin |  | ||||||
| 
 |  | ||||||
| Responsible for notifying of a new release. |  | ||||||
| 
 |  | ||||||
| Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).<br> |  | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 |  | ||||||
| ### fail plugin |  | ||||||
| 
 |  | ||||||
| Responsible for notifying of a failed release. |  | ||||||
| 
 |  | ||||||
| Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).<br> |  | ||||||
| Optional.<br> |  | ||||||
| Accept multiple plugins. |  | ||||||
| 
 |  | ||||||
| ## 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`. |  | ||||||
| 
 |  | ||||||
| Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along with the other **semantic-release** [options](configuration.md#options) will apply to all plugins. Options defined within the plugin `Object` will apply to that specific plugin. |  | ||||||
| 
 |  | ||||||
| For example: |  | ||||||
| ```json | ```json | ||||||
| { | { | ||||||
|   "release": { |   "plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm"] | ||||||
|     "verifyConditions": [ | } | ||||||
|       { | ``` | ||||||
|         "path": "@semantic-release/exec", | 
 | ||||||
|         "cmd": "verify-conditions.sh" | ## Plugin ordering | ||||||
|       }, | 
 | ||||||
|       "@semantic-release/npm", | For each [release step](../../README.md#release-steps) the plugins that implement that step will be executed in the order in which the are defined. | ||||||
|       "@semantic-release/github" | 
 | ||||||
|     ], | ```json | ||||||
|     "analyzeCommits": "custom-plugin", | { | ||||||
|     "verifyRelease": [ |   "plugins": [ | ||||||
|       { |     "@semantic-release/commit-analyzer", | ||||||
|         "path": "@semantic-release/exec", |     "@semantic-release/release-notes-generator", | ||||||
|         "cmd": "verify-release.sh" |     "@semantic-release/npm", | ||||||
|       }, |     "@semantic-release/git" | ||||||
|     ], |   ] | ||||||
|     "generateNotes": "./build/my-plugin.js", | } | ||||||
|     "githubUrl": "https://my-ghe.com", | ``` | ||||||
|     "githubApiPathPrefix": "/api-prefix" | 
 | ||||||
|   } | With this configuration **semantic-release** will: | ||||||
|  | - execute the `verifyConditions` implementation of `@semantic-release/npm` then `@semantic-release/git` | ||||||
|  | - execute the `analyzeCommits` implementation of `@semantic-release/commit-analyzer` | ||||||
|  | - execute the `prepare` implementation of `@semantic-release/npm` then `@semantic-release/git` | ||||||
|  | - execute the `generateNotes` implementation of `@semantic-release/release-notes-generator` | ||||||
|  | - execute the `publish` implementation of `@semantic-release/npm` | ||||||
|  | 
 | ||||||
|  | ## Plugin options | ||||||
|  | 
 | ||||||
|  | A plugin options can specified by wrapping the name and an options object in an array. Options configured this way will be passed only to that specific plugin. | ||||||
|  | 
 | ||||||
|  | Global plugin options can defined at the root of the **semantic-release** configuration object. Options configured this way will be passed to all plugins. | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "plugins": [ | ||||||
|  |     "@semantic-release/commit-analyzer", | ||||||
|  |     "@semantic-release/release-notes-generator", | ||||||
|  |     ["@semantic-release/github", { | ||||||
|  |       "assets": ["dist/**"] | ||||||
|  |       }], | ||||||
|  |     "@semantic-release/git" | ||||||
|  |   ], | ||||||
|  |   "preset": "angular" | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| With this configuration: | With this configuration: | ||||||
| - the `custom-plugin` npm module will be used to [analyze commits](#analyzecommits-plugin) | - All plugins will receive the `preset` option, which will be used by both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` (and ignored by `@semantic-release/github` and `@semantic-release/git`) | ||||||
| - the `./build/my-plugin.js` script will be used to [generate release notes](#generatenotes-plugin) | - The `@semantic-release/github` plugin will receive the `assets` options (`@semantic-release/git` will not receive it and therefore will use it's default value for that option) | ||||||
| - the [`@semantic-release/exec`](https://github.com/semantic-release/exec),  [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/github`](https://github.com/semantic-release/github) plugins will be used to [verify conditions](#verifyconditions-plugin) |  | ||||||
| - the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin will be used to [verify the release](#verifyrelease-plugin) |  | ||||||
| - the `cmd` option will be set to `verify-conditions.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify conditions](#verifyconditions-plugin) |  | ||||||
| - the `cmd` option will be set to `verify-release.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify the release](#verifyrelease-plugin) |  | ||||||
| - the `githubUrl` and `githubApiPathPrefix` options will be set to respectively `https://my-ghe.com` and `/api-prefix` for all plugins |  | ||||||
|  | |||||||
| @ -61,9 +61,17 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.` | |||||||
|       required ? 'is required and ' : '' |       required ? 'is required and ' : '' | ||||||
|     }must be ${ |     }must be ${ | ||||||
|       multiple ? 'a single or an array of plugins' : 'a single plugin' |       multiple ? 'a single or an array of plugins' : 'a single plugin' | ||||||
|     } definition. A plugin definition is either a string or an object with a \`path\` property.
 |     } definition. A plugin definition is an npm module name, optionnaly wrapped in an array with an object. | ||||||
| 
 | 
 | ||||||
| Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`, | Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`, | ||||||
|  |   }), | ||||||
|  |   EPLUGINSCONF: ({plugin}) => ({ | ||||||
|  |     message: 'The `plugins` configuration is invalid.', | ||||||
|  |     details: `The [plugins](${linkify( | ||||||
|  |       'docs/usage/configuration.md#plugins' | ||||||
|  |     )}) option must be an array of plugin definions. A plugin definition is an npm module name, optionnaly wrapped in an array with an object. | ||||||
|  | 
 | ||||||
|  | The invalid configuration is \`${stringify(plugin)}\`.`, | ||||||
|   }), |   }), | ||||||
|   EPLUGIN: ({pluginName, type}) => ({ |   EPLUGIN: ({pluginName, type}) => ({ | ||||||
|     message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`, |     message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`, | ||||||
|  | |||||||
| @ -6,13 +6,12 @@ const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants'); | |||||||
| 
 | 
 | ||||||
| module.exports = { | module.exports = { | ||||||
|   verifyConditions: { |   verifyConditions: { | ||||||
|     default: ['@semantic-release/npm', '@semantic-release/github'], |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     pipelineConfig: () => ({settleAll: true}), |     pipelineConfig: () => ({settleAll: true}), | ||||||
|   }, |   }, | ||||||
|   analyzeCommits: { |   analyzeCommits: { | ||||||
|     default: '@semantic-release/commit-analyzer', |     default: ['@semantic-release/commit-analyzer'], | ||||||
|     multiple: false, |     multiple: false, | ||||||
|     required: true, |     required: true, | ||||||
|     outputValidator: output => !output || RELEASE_TYPE.includes(output), |     outputValidator: output => !output || RELEASE_TYPE.includes(output), | ||||||
| @ -23,13 +22,11 @@ module.exports = { | |||||||
|     postprocess: ([result]) => result, |     postprocess: ([result]) => result, | ||||||
|   }, |   }, | ||||||
|   verifyRelease: { |   verifyRelease: { | ||||||
|     default: false, |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     pipelineConfig: () => ({settleAll: true}), |     pipelineConfig: () => ({settleAll: true}), | ||||||
|   }, |   }, | ||||||
|   generateNotes: { |   generateNotes: { | ||||||
|     default: ['@semantic-release/release-notes-generator'], |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     outputValidator: output => !output || isString(output), |     outputValidator: output => !output || isString(output), | ||||||
| @ -45,7 +42,6 @@ module.exports = { | |||||||
|     postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)), |     postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)), | ||||||
|   }, |   }, | ||||||
|   prepare: { |   prepare: { | ||||||
|     default: ['@semantic-release/npm'], |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     pipelineConfig: ({generateNotes}, logger) => ({ |     pipelineConfig: ({generateNotes}, logger) => ({ | ||||||
| @ -64,7 +60,6 @@ module.exports = { | |||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|   publish: { |   publish: { | ||||||
|     default: ['@semantic-release/npm', '@semantic-release/github'], |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     outputValidator: output => !output || isPlainObject(output), |     outputValidator: output => !output || isPlainObject(output), | ||||||
| @ -78,14 +73,12 @@ module.exports = { | |||||||
|     }), |     }), | ||||||
|   }, |   }, | ||||||
|   success: { |   success: { | ||||||
|     default: ['@semantic-release/github'], |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     pipelineConfig: () => ({settleAll: true}), |     pipelineConfig: () => ({settleAll: true}), | ||||||
|     preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}), |     preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}), | ||||||
|   }, |   }, | ||||||
|   fail: { |   fail: { | ||||||
|     default: ['@semantic-release/github'], |  | ||||||
|     multiple: true, |     multiple: true, | ||||||
|     required: false, |     required: false, | ||||||
|     pipelineConfig: () => ({settleAll: true}), |     pipelineConfig: () => ({settleAll: true}), | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = require('lodash'); | const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash'); | ||||||
| const readPkgUp = require('read-pkg-up'); | const readPkgUp = require('read-pkg-up'); | ||||||
| const cosmiconfig = require('cosmiconfig'); | const cosmiconfig = require('cosmiconfig'); | ||||||
| const resolveFrom = require('resolve-from'); | const resolveFrom = require('resolve-from'); | ||||||
| @ -35,7 +35,7 @@ module.exports = async (context, opts) => { | |||||||
|         // 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.entries(extendsOpts).reduce((pluginsPath, [option, value]) => { |         Object.entries(extendsOpts).reduce((pluginsPath, [option, value]) => { | ||||||
|           if (PLUGINS_DEFINITIONS[option]) { |           if (PLUGINS_DEFINITIONS[option] || option === 'plugins') { | ||||||
|             castArray(value) |             castArray(value) | ||||||
|               .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)) | ||||||
| @ -57,8 +57,14 @@ module.exports = async (context, opts) => { | |||||||
|     branch: 'master', |     branch: 'master', | ||||||
|     repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})), |     repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})), | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: [ | ||||||
|  |       '@semantic-release/commit-analyzer', | ||||||
|  |       '@semantic-release/release-notes-generator', | ||||||
|  |       '@semantic-release/npm', | ||||||
|  |       '@semantic-release/github', | ||||||
|  |     ], | ||||||
|     // Remove `null` and `undefined` options so they can be replaced with default ones
 |     // Remove `null` and `undefined` options so they can be replaced with default ones
 | ||||||
|     ...pickBy(options, option => !isUndefined(option) && !isNull(option)), |     ...pickBy(options, option => !isNil(option)), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   debug('options values: %O', options); |   debug('options values: %O', options); | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| const {parse, format} = require('url'); | const {parse, format} = require('url'); | ||||||
| const {isUndefined} = require('lodash'); | const {isNil} = require('lodash'); | ||||||
| const gitUrlParse = require('git-url-parse'); | const gitUrlParse = require('git-url-parse'); | ||||||
| const hostedGitInfo = require('hosted-git-info'); | const hostedGitInfo = require('hosted-git-info'); | ||||||
| const {verifyAuth} = require('./git'); | const {verifyAuth} = require('./git'); | ||||||
| @ -44,7 +44,7 @@ module.exports = async ({cwd, env, options: {repositoryUrl, branch}}) => { | |||||||
|   try { |   try { | ||||||
|     await verifyAuth(repositoryUrl, branch, {cwd, env}); |     await verifyAuth(repositoryUrl, branch, {cwd, env}); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(env[envVar])); |     const envVar = Object.keys(GIT_TOKENS).find(envVar => !isNil(env[envVar])); | ||||||
|     const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`; |     const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`; | ||||||
|     const {protocols, ...parsed} = gitUrlParse(repositoryUrl); |     const {protocols, ...parsed} = gitUrlParse(repositoryUrl); | ||||||
|     const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https'; |     const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https'; | ||||||
|  | |||||||
| @ -1,52 +1,89 @@ | |||||||
| const {identity, isPlainObject, omit, castArray, isUndefined} = require('lodash'); | const {identity, isPlainObject, omit, castArray, isNil, isString} = require('lodash'); | ||||||
| const AggregateError = require('aggregate-error'); | const AggregateError = require('aggregate-error'); | ||||||
| const getError = require('../get-error'); | const getError = require('../get-error'); | ||||||
| const PLUGINS_DEFINITIONS = require('../definitions/plugins'); | const PLUGINS_DEFINITIONS = require('../definitions/plugins'); | ||||||
| const {validateConfig} = require('./utils'); | const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('./utils'); | ||||||
| const pipeline = require('./pipeline'); | const pipeline = require('./pipeline'); | ||||||
| const normalize = require('./normalize'); | const normalize = require('./normalize'); | ||||||
| 
 | 
 | ||||||
| module.exports = (context, pluginsPath) => { | module.exports = (context, pluginsPath) => { | ||||||
|   const {options, logger} = context; |   let {options, logger} = context; | ||||||
|   const errors = []; |   const errors = []; | ||||||
|   const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce( | 
 | ||||||
|  |   const plugins = options.plugins | ||||||
|  |     ? castArray(options.plugins).reduce((plugins, plugin) => { | ||||||
|  |         if (validatePlugin(plugin)) { | ||||||
|  |           const [name, config] = parseConfig(plugin); | ||||||
|  |           plugin = isString(name) ? loadPlugin(context, name, pluginsPath) : name; | ||||||
|  | 
 | ||||||
|  |           if (isPlainObject(plugin)) { | ||||||
|  |             Object.entries(plugin).forEach(([type, func]) => { | ||||||
|  |               if (PLUGINS_DEFINITIONS[type]) { | ||||||
|  |                 plugins[type] = [...(plugins[type] || []), [func, config]]; | ||||||
|  |               } | ||||||
|  |             }); | ||||||
|  |           } else { | ||||||
|  |             errors.push(getError('EPLUGINSCONF', {plugin})); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           errors.push(getError('EPLUGINSCONF', {plugin})); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return plugins; | ||||||
|  |       }, {}) | ||||||
|  |     : []; | ||||||
|  | 
 | ||||||
|  |   if (errors.length > 0) { | ||||||
|  |     throw new AggregateError(errors); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   options = {...plugins, ...options}; | ||||||
|  | 
 | ||||||
|  |   const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce( | ||||||
|     ( |     ( | ||||||
|       plugins, |       pluginsConf, | ||||||
|       [type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}] |       [type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}] | ||||||
|     ) => { |     ) => { | ||||||
|       let pluginOpts; |       let pluginOpts; | ||||||
| 
 | 
 | ||||||
|       if (isUndefined(options[type])) { |       if (isNil(options[type]) && def) { | ||||||
|         pluginOpts = def; |         pluginOpts = def; | ||||||
|       } else { |       } else { | ||||||
|         const defaultPaths = castArray(def); |         // If an object is passed and the path is missing, merge it with step options
 | ||||||
|         // If an object is passed and the path is missing, set the default one for single plugins
 |         if (isPlainObject(options[type]) && !options[type].path) { | ||||||
|         if (isPlainObject(options[type]) && !options[type].path && defaultPaths.length === 1) { |           options[type] = castArray(plugins[type]).map( | ||||||
|           [options[type].path] = defaultPaths; |             plugin => (plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin) | ||||||
|  |           ); | ||||||
|         } |         } | ||||||
|         if (!validateConfig({multiple, required}, options[type])) { |         if (!validateStep({multiple, required}, options[type])) { | ||||||
|           errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]})); |           errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]})); | ||||||
|           return plugins; |           return pluginsConf; | ||||||
|         } |         } | ||||||
|         pluginOpts = options[type]; |         pluginOpts = options[type]; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const steps = castArray(pluginOpts).map(pluginOpt => |       const steps = castArray(pluginOpts).map(pluginOpt => | ||||||
|         normalize({...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS))}, type, pluginOpt, pluginsPath) |         normalize( | ||||||
|  |           {...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')}, | ||||||
|  |           type, | ||||||
|  |           pluginOpt, | ||||||
|  |           pluginsPath | ||||||
|  |         ) | ||||||
|       ); |       ); | ||||||
| 
 | 
 | ||||||
|       plugins[type] = async input => |       pluginsConf[type] = async input => | ||||||
|         postprocess( |         postprocess( | ||||||
|           await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)), |           await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)), | ||||||
|           input |           input | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|       return plugins; |       return pluginsConf; | ||||||
|     }, |     }, | ||||||
|     {} |     plugins | ||||||
|   ); |   ); | ||||||
|   if (errors.length > 0) { |   if (errors.length > 0) { | ||||||
|     throw new AggregateError(errors); |     throw new AggregateError(errors); | ||||||
|   } |   } | ||||||
|   return plugins; | 
 | ||||||
|  |   return pluginsConf; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,22 +1,18 @@ | |||||||
| const {dirname} = require('path'); | const {isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash'); | ||||||
| const {isString, isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash'); |  | ||||||
| const resolveFrom = require('resolve-from'); |  | ||||||
| const getError = require('../get-error'); | const getError = require('../get-error'); | ||||||
| const {extractErrors} = require('../utils'); | const {extractErrors} = require('../utils'); | ||||||
| const PLUGINS_DEFINITIONS = require('../definitions/plugins'); | const PLUGINS_DEFINITIONS = require('../definitions/plugins'); | ||||||
|  | const {loadPlugin, parseConfig} = require('./utils'); | ||||||
| 
 | 
 | ||||||
| module.exports = ({cwd, stdout, stderr, options, logger}, type, pluginOpt, pluginsPath) => { | module.exports = (context, type, pluginOpt, pluginsPath) => { | ||||||
|  |   const {stdout, stderr, options, logger} = context; | ||||||
|   if (!pluginOpt) { |   if (!pluginOpt) { | ||||||
|     return noop; |     return noop; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt; |   const [path, config] = parseConfig(pluginOpt); | ||||||
|   const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path; |   const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path; | ||||||
| 
 |   const plugin = loadPlugin(context, path, pluginsPath); | ||||||
|   const basePath = pluginsPath[path] |  | ||||||
|     ? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path])) |  | ||||||
|     : __dirname; |  | ||||||
|   const plugin = isFunction(path) ? path : require(resolveFrom.silent(basePath, path) || resolveFrom(cwd, path)); |  | ||||||
| 
 | 
 | ||||||
|   let func; |   let func; | ||||||
|   if (isFunction(plugin)) { |   if (isFunction(plugin)) { | ||||||
|  | |||||||
| @ -1,18 +1,68 @@ | |||||||
| const {isString, isFunction, castArray} = require('lodash'); | const {dirname} = require('path'); | ||||||
|  | const {isString, isFunction, castArray, isArray, isPlainObject, isNil} = require('lodash'); | ||||||
|  | const resolveFrom = require('resolve-from'); | ||||||
| 
 | 
 | ||||||
| const validateSingleConfig = conf => { | const validateStepArrayDefinition = conf => | ||||||
|  |   isArray(conf) && | ||||||
|  |   (conf.length === 1 || conf.length === 2) && | ||||||
|  |   (isString(conf[0]) || isFunction(conf[0])) && | ||||||
|  |   (isNil(conf[1]) || isPlainObject(conf[1])); | ||||||
|  | 
 | ||||||
|  | const validateSingleStep = conf => { | ||||||
|  |   if (validateStepArrayDefinition(conf)) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|   conf = castArray(conf); |   conf = castArray(conf); | ||||||
|   return conf.length === 1 && (isString(conf[0]) || isString(conf[0].path) || isFunction(conf[0])); | 
 | ||||||
|  |   if (conf.length !== 1) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const [path, config] = parseConfig(conf[0]); | ||||||
|  |   return (isString(path) || isFunction(path)) && isPlainObject(config); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const validateMultipleConfig = conf => castArray(conf).every(conf => validateSingleConfig(conf)); | const validateMultipleStep = conf => { | ||||||
|  |   return conf.every(conf => validateSingleStep(conf)); | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| const validateConfig = ({multiple, required}, conf) => { | function validatePlugin(conf) { | ||||||
|  |   return ( | ||||||
|  |     isString(conf) || | ||||||
|  |     (isArray(conf) && | ||||||
|  |       (conf.length === 1 || conf.length === 2) && | ||||||
|  |       (isString(conf[0]) || isPlainObject(conf[0])) && | ||||||
|  |       (isNil(conf[1]) || isPlainObject(conf[1]))) || | ||||||
|  |     (isPlainObject(conf) && (isNil(conf.path) || isString(conf.path) || isPlainObject(conf.path))) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function validateStep({multiple, required}, conf) { | ||||||
|   conf = castArray(conf).filter(Boolean); |   conf = castArray(conf).filter(Boolean); | ||||||
|   if (required) { |   if (required) { | ||||||
|     return Boolean(conf) && conf.length >= 1 && (multiple ? validateMultipleConfig : validateSingleConfig)(conf); |     return conf.length >= 1 && (multiple ? validateMultipleStep : validateSingleStep)(conf); | ||||||
|   } |   } | ||||||
|   return conf.length === 0 || (multiple ? validateMultipleConfig : validateSingleConfig)(conf); |   return conf.length === 0 || (multiple ? validateMultipleStep : validateSingleStep)(conf); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| module.exports = {validateConfig}; | function loadPlugin({cwd}, path, pluginsPath) { | ||||||
|  |   const basePath = pluginsPath[path] | ||||||
|  |     ? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path])) | ||||||
|  |     : __dirname; | ||||||
|  |   return isFunction(path) ? path : require(resolveFrom.silent(basePath, path) || resolveFrom(cwd, path)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseConfig(plugin) { | ||||||
|  |   let path; | ||||||
|  |   let config; | ||||||
|  |   if (isArray(plugin)) { | ||||||
|  |     [path, config] = plugin; | ||||||
|  |   } else if (isPlainObject(plugin) && !isNil(plugin.path)) { | ||||||
|  |     ({path, ...config} = plugin); | ||||||
|  |   } else { | ||||||
|  |     path = plugin; | ||||||
|  |   } | ||||||
|  |   return [path, config || {}]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = {validatePlugin, validateStep, loadPlugin, parseConfig}; | ||||||
|  | |||||||
| @ -19,11 +19,11 @@ | |||||||
|     "Pierre Vanduynslager (https://twitter.com/@pvdlg_)" |     "Pierre Vanduynslager (https://twitter.com/@pvdlg_)" | ||||||
|   ], |   ], | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@semantic-release/commit-analyzer": "^6.0.0", |     "@semantic-release/commit-analyzer": "^6.1.0", | ||||||
|     "@semantic-release/error": "^2.2.0", |     "@semantic-release/error": "^2.2.0", | ||||||
|     "@semantic-release/github": "^5.0.0", |     "@semantic-release/github": "^5.1.0", | ||||||
|     "@semantic-release/npm": "^5.0.1", |     "@semantic-release/npm": "^5.0.5", | ||||||
|     "@semantic-release/release-notes-generator": "^7.0.0", |     "@semantic-release/release-notes-generator": "^7.1.0", | ||||||
|     "aggregate-error": "^1.0.0", |     "aggregate-error": "^1.0.0", | ||||||
|     "cosmiconfig": "^5.0.1", |     "cosmiconfig": "^5.0.1", | ||||||
|     "debug": "^4.0.0", |     "debug": "^4.0.0", | ||||||
|  | |||||||
| @ -33,6 +33,9 @@ test.serial('Pass options to semantic-release API', async t => { | |||||||
|     'https://github/com/owner/repo.git', |     'https://github/com/owner/repo.git', | ||||||
|     '-t', |     '-t', | ||||||
|     `v\${version}`, |     `v\${version}`, | ||||||
|  |     '-p', | ||||||
|  |     'plugin1', | ||||||
|  |     'plugin2', | ||||||
|     '-e', |     '-e', | ||||||
|     'config1', |     'config1', | ||||||
|     'config2', |     'config2', | ||||||
| @ -68,6 +71,7 @@ test.serial('Pass options to semantic-release API', async t => { | |||||||
|   t.is(run.args[0][0].branch, 'master'); |   t.is(run.args[0][0].branch, 'master'); | ||||||
|   t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git'); |   t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git'); | ||||||
|   t.is(run.args[0][0].tagFormat, `v\${version}`); |   t.is(run.args[0][0].tagFormat, `v\${version}`); | ||||||
|  |   t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']); | ||||||
|   t.deepEqual(run.args[0][0].extends, ['config1', 'config2']); |   t.deepEqual(run.args[0][0].extends, ['config1', 'config2']); | ||||||
|   t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']); |   t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']); | ||||||
|   t.is(run.args[0][0].analyzeCommits, 'analyze'); |   t.is(run.args[0][0].analyzeCommits, 'analyze'); | ||||||
| @ -94,6 +98,9 @@ test.serial('Pass options to semantic-release API with alias arguments', async t | |||||||
|     'https://github/com/owner/repo.git', |     'https://github/com/owner/repo.git', | ||||||
|     '--tag-format', |     '--tag-format', | ||||||
|     `v\${version}`, |     `v\${version}`, | ||||||
|  |     '--plugins', | ||||||
|  |     'plugin1', | ||||||
|  |     'plugin2', | ||||||
|     '--extends', |     '--extends', | ||||||
|     'config1', |     'config1', | ||||||
|     'config2', |     'config2', | ||||||
| @ -106,6 +113,7 @@ test.serial('Pass options to semantic-release API with alias arguments', async t | |||||||
|   t.is(run.args[0][0].branch, 'master'); |   t.is(run.args[0][0].branch, 'master'); | ||||||
|   t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git'); |   t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git'); | ||||||
|   t.is(run.args[0][0].tagFormat, `v\${version}`); |   t.is(run.args[0][0].tagFormat, `v\${version}`); | ||||||
|  |   t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']); | ||||||
|   t.deepEqual(run.args[0][0].extends, ['config1', 'config2']); |   t.deepEqual(run.args[0][0].extends, ['config1', 'config2']); | ||||||
|   t.is(run.args[0][0].dryRun, true); |   t.is(run.args[0][0].dryRun, true); | ||||||
| 
 | 
 | ||||||
| @ -151,6 +159,8 @@ test.serial('Do not set properties in option for which arg is not in command lin | |||||||
|   t.false('debug' in run.args[0][0]); |   t.false('debug' in run.args[0][0]); | ||||||
|   t.false('r' in run.args[0][0]); |   t.false('r' in run.args[0][0]); | ||||||
|   t.false('t' in run.args[0][0]); |   t.false('t' in run.args[0][0]); | ||||||
|  |   t.false('p' in run.args[0][0]); | ||||||
|  |   t.false('e' in run.args[0][0]); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test.serial('Set "noCi" options to "true" with "--no-ci"', async t => { | test.serial('Set "noCi" options to "true" with "--no-ci"', async t => { | ||||||
|  | |||||||
| @ -8,6 +8,13 @@ import {stub} from 'sinon'; | |||||||
| import yaml from 'js-yaml'; | import yaml from 'js-yaml'; | ||||||
| import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils'; | import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils'; | ||||||
| 
 | 
 | ||||||
|  | const DEFAULT_PLUGINS = [ | ||||||
|  |   '@semantic-release/commit-analyzer', | ||||||
|  |   '@semantic-release/release-notes-generator', | ||||||
|  |   '@semantic-release/npm', | ||||||
|  |   '@semantic-release/github', | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
| test.beforeEach(t => { | test.beforeEach(t => { | ||||||
|   t.context.plugins = stub().returns({}); |   t.context.plugins = stub().returns({}); | ||||||
|   t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins}); |   t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins}); | ||||||
| @ -69,6 +76,7 @@ test('Read options from package.json', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json in repository root
 |   // Create package.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: options}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: options}); | ||||||
| @ -89,6 +97,7 @@ test('Read options from .releaserc.yml', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json in repository root
 |   // Create package.json in repository root
 | ||||||
|   await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options)); |   await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options)); | ||||||
| @ -109,6 +118,7 @@ test('Read options from .releaserc.json', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json in repository root
 |   // Create package.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, '.releaserc.json'), options); |   await outputJson(path.resolve(cwd, '.releaserc.json'), options); | ||||||
| @ -129,6 +139,7 @@ test('Read options from .releaserc.js', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json in repository root
 |   // Create package.json in repository root
 | ||||||
|   await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`); |   await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`); | ||||||
| @ -149,6 +160,7 @@ test('Read options from release.config.js', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json in repository root
 |   // Create package.json in repository root
 | ||||||
|   await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`); |   await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`); | ||||||
| @ -176,6 +188,7 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async | |||||||
|     branch: 'branch_cli', |     branch: 'branch_cli', | ||||||
|     repositoryUrl: 'http://cli-url.com/owner/package', |     repositoryUrl: 'http://cli-url.com/owner/package', | ||||||
|     tagFormat: `cli\${version}`, |     tagFormat: `cli\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'}; |   const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'}; | ||||||
|   // Create package.json in repository root
 |   // Create package.json in repository root
 | ||||||
| @ -199,6 +212,7 @@ test('Read configuration from file path in "extends"', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json and shareable.json in repository root
 |   // Create package.json and shareable.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); | ||||||
| @ -226,6 +240,7 @@ test('Read configuration from module path in "extends"', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json and shareable.json in repository root
 |   // Create package.json and shareable.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); | ||||||
| @ -259,6 +274,7 @@ test('Read configuration from an array of paths in "extends"', async t => { | |||||||
|     analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'}, |     analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'}, | ||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json and shareable.json in repository root
 |   // Create package.json and shareable.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); | ||||||
| @ -296,6 +312,7 @@ test('Prioritize configuration from config file over "extends"', async t => { | |||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json and shareable.json in repository root
 |   // Create package.json and shareable.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); | ||||||
| @ -341,6 +358,7 @@ test('Prioritize configuration from cli/API options over "extends"', async t => | |||||||
|     publish: [{path: 'publishShareable', param: 'publishShareable_param2'}], |     publish: [{path: 'publishShareable', param: 'publishShareable_param2'}], | ||||||
|     branch: 'test_branch2', |     branch: 'test_branch2', | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json, shareable1.json and shareable2.json in repository root
 |   // Create package.json, shareable1.json and shareable2.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); | ||||||
| @ -366,11 +384,13 @@ test('Allow to unset properties defined in shareable config with "null"', async | |||||||
|     analyzeCommits: null, |     analyzeCommits: null, | ||||||
|     branch: 'test_branch', |     branch: 'test_branch', | ||||||
|     repositoryUrl: 'https://host.null/owner/module.git', |     repositoryUrl: 'https://host.null/owner/module.git', | ||||||
|  |     plugins: null, | ||||||
|   }; |   }; | ||||||
|   const options1 = { |   const options1 = { | ||||||
|     generateNotes: 'generateNotes', |     generateNotes: 'generateNotes', | ||||||
|     analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'}, |     analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'}, | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: ['test-plugin'], | ||||||
|   }; |   }; | ||||||
|   // Create package.json and shareable.json in repository root
 |   // Create package.json and shareable.json in repository root
 | ||||||
|   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); |   await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions}); | ||||||
| @ -378,19 +398,25 @@ test('Allow to unset properties defined in shareable config with "null"', async | |||||||
| 
 | 
 | ||||||
|   const {options} = await t.context.getConfig({cwd}); |   const {options} = await t.context.getConfig({cwd}); | ||||||
| 
 | 
 | ||||||
|   // Verify the options contains the plugin config from shareable.json
 |   // Verify the options contains the plugin config from shareable.json and the default `plugins`
 | ||||||
|   t.deepEqual(options, {...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits'])}); |   t.deepEqual(options, { | ||||||
|   // Verify the plugins module is called with the plugin options from shareable.json
 |     ...omit(options1, ['analyzeCommits']), | ||||||
|  |     ...omit(pkgOptions, ['extends', 'analyzeCommits']), | ||||||
|  |     plugins: DEFAULT_PLUGINS, | ||||||
|  |   }); | ||||||
|  |   // Verify the plugins module is called with the plugin options from shareable.json and the default `plugins`
 | ||||||
|   t.deepEqual(t.context.plugins.args[0][0], { |   t.deepEqual(t.context.plugins.args[0][0], { | ||||||
|     options: { |     options: { | ||||||
|       ...omit(options1, 'analyzeCommits'), |       ...omit(options1, 'analyzeCommits'), | ||||||
|       ...omit(pkgOptions, ['extends', 'analyzeCommits']), |       ...omit(pkgOptions, ['extends', 'analyzeCommits']), | ||||||
|  |       plugins: DEFAULT_PLUGINS, | ||||||
|     }, |     }, | ||||||
|     cwd, |     cwd, | ||||||
|   }); |   }); | ||||||
|   t.deepEqual(t.context.plugins.args[0][1], { |   t.deepEqual(t.context.plugins.args[0][1], { | ||||||
|     generateNotes: './shareable.json', |     generateNotes: './shareable.json', | ||||||
|     analyzeCommits: './shareable.json', |     analyzeCommits: './shareable.json', | ||||||
|  |     'test-plugin': './shareable.json', | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| @ -407,6 +433,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a | |||||||
|     generateNotes: 'generateNotes', |     generateNotes: 'generateNotes', | ||||||
|     analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'}, |     analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'}, | ||||||
|     tagFormat: `v\${version}`, |     tagFormat: `v\${version}`, | ||||||
|  |     plugins: false, | ||||||
|   }; |   }; | ||||||
|   // Create package.json and release.config.js in repository root
 |   // Create package.json and release.config.js in repository root
 | ||||||
|   await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`); |   await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`); | ||||||
|  | |||||||
| @ -64,6 +64,7 @@ test('Plugins are called with expected values', async t => { | |||||||
|   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`}; |   const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`}; | ||||||
|   const options = { |   const options = { | ||||||
|     ...config, |     ...config, | ||||||
|  |     plugins: false, | ||||||
|     verifyConditions: [verifyConditions1, verifyConditions2], |     verifyConditions: [verifyConditions1, verifyConditions2], | ||||||
|     analyzeCommits, |     analyzeCommits, | ||||||
|     verifyRelease, |     verifyRelease, | ||||||
| @ -359,6 +360,7 @@ test('Log all "verifyConditions" errors', async t => { | |||||||
|   const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`}; |   const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`}; | ||||||
|   const options = { |   const options = { | ||||||
|     ...config, |     ...config, | ||||||
|  |     plugins: false, | ||||||
|     verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)], |     verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)], | ||||||
|     fail, |     fail, | ||||||
|   }; |   }; | ||||||
|  | |||||||
| @ -152,7 +152,7 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin | |||||||
|   t.regex(error.details, /2/); |   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" (with object definition) and input', async t => { | ||||||
|   const pluginFunction = stub().resolves(); |   const pluginFunction = stub().resolves(); | ||||||
|   const pluginConf = {path: pluginFunction, conf: 'confValue'}; |   const pluginConf = {path: pluginFunction, conf: 'confValue'}; | ||||||
|   const options = {global: 'globalValue'}; |   const options = {global: 'globalValue'}; | ||||||
| @ -167,6 +167,21 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi | |||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test('Plugin is called with "pluginConfig" (with array definition) and input', async t => { | ||||||
|  |   const pluginFunction = stub().resolves(); | ||||||
|  |   const pluginConf = [pluginFunction, {conf: 'confValue'}]; | ||||||
|  |   const options = {global: 'globalValue'}; | ||||||
|  |   const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); | ||||||
|  |   await plugin({param: 'param'}); | ||||||
|  | 
 | ||||||
|  |   t.true( | ||||||
|  |     pluginFunction.calledWithMatch( | ||||||
|  |       {conf: 'confValue', global: 'globalValue'}, | ||||||
|  |       {param: 'param', logger: t.context.logger} | ||||||
|  |     ) | ||||||
|  |   ); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test('Prevent plugins to modify "pluginConfig"', async t => { | test('Prevent plugins to modify "pluginConfig"', async t => { | ||||||
|   const pluginFunction = stub().callsFake(pluginConfig => { |   const pluginFunction = stub().callsFake(pluginConfig => { | ||||||
|     pluginConfig.conf.subConf = 'otherConf'; |     pluginConfig.conf.subConf = 'otherConf'; | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ test('Export default plugins', t => { | |||||||
|   t.is(typeof plugins.fail, 'function'); |   t.is(typeof plugins.fail, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Export plugins based on config', t => { | test('Export plugins based on steps config', t => { | ||||||
|   const plugins = getPlugins( |   const plugins = getPlugins( | ||||||
|     { |     { | ||||||
|       cwd, |       cwd, | ||||||
| @ -55,6 +55,88 @@ test('Export plugins based on config', t => { | |||||||
|   t.is(typeof plugins.fail, 'function'); |   t.is(typeof plugins.fail, 'function'); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test('Export plugins based on "plugins" config (array)', async t => { | ||||||
|  |   const plugin1 = {verifyConditions: stub(), publish: stub()}; | ||||||
|  |   const plugin2 = {verifyConditions: stub(), verifyRelease: stub()}; | ||||||
|  |   const plugins = getPlugins( | ||||||
|  |     {cwd, logger: t.context.logger, options: {plugins: [plugin1, plugin2], verifyRelease: () => {}}}, | ||||||
|  |     {} | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   await plugins.verifyConditions({}); | ||||||
|  |   t.true(plugin1.verifyConditions.calledOnce); | ||||||
|  |   t.true(plugin2.verifyConditions.calledOnce); | ||||||
|  | 
 | ||||||
|  |   await plugins.publish({}); | ||||||
|  |   t.true(plugin1.publish.calledOnce); | ||||||
|  | 
 | ||||||
|  |   await plugins.verifyRelease({}); | ||||||
|  |   t.true(plugin2.verifyRelease.notCalled); | ||||||
|  | 
 | ||||||
|  |   // Verify the module returns a function for each plugin
 | ||||||
|  |   t.is(typeof plugins.verifyConditions, 'function'); | ||||||
|  |   t.is(typeof plugins.analyzeCommits, 'function'); | ||||||
|  |   t.is(typeof plugins.verifyRelease, 'function'); | ||||||
|  |   t.is(typeof plugins.generateNotes, 'function'); | ||||||
|  |   t.is(typeof plugins.prepare, 'function'); | ||||||
|  |   t.is(typeof plugins.publish, 'function'); | ||||||
|  |   t.is(typeof plugins.success, 'function'); | ||||||
|  |   t.is(typeof plugins.fail, 'function'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Export plugins based on "plugins" config (single definition)', async t => { | ||||||
|  |   const plugin1 = {verifyConditions: stub(), publish: stub()}; | ||||||
|  |   const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {}); | ||||||
|  | 
 | ||||||
|  |   await plugins.verifyConditions({}); | ||||||
|  |   t.true(plugin1.verifyConditions.calledOnce); | ||||||
|  | 
 | ||||||
|  |   await plugins.publish({}); | ||||||
|  |   t.true(plugin1.publish.calledOnce); | ||||||
|  | 
 | ||||||
|  |   // Verify the module returns a function for each plugin
 | ||||||
|  |   t.is(typeof plugins.verifyConditions, 'function'); | ||||||
|  |   t.is(typeof plugins.analyzeCommits, 'function'); | ||||||
|  |   t.is(typeof plugins.verifyRelease, 'function'); | ||||||
|  |   t.is(typeof plugins.generateNotes, 'function'); | ||||||
|  |   t.is(typeof plugins.prepare, 'function'); | ||||||
|  |   t.is(typeof plugins.publish, 'function'); | ||||||
|  |   t.is(typeof plugins.success, 'function'); | ||||||
|  |   t.is(typeof plugins.fail, 'function'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Merge global options, "plugins" options and sptep options', async t => { | ||||||
|  |   const plugin1 = [{verifyConditions: stub(), publish: stub()}, {pluginOpt1: 'plugin1'}]; | ||||||
|  |   const plugin2 = [{verifyConditions: stub()}, {pluginOpt2: 'plugin2'}]; | ||||||
|  |   const plugin3 = [stub(), {pluginOpt3: 'plugin3'}]; | ||||||
|  |   const plugins = getPlugins( | ||||||
|  |     { | ||||||
|  |       cwd, | ||||||
|  |       logger: t.context.logger, | ||||||
|  |       options: {globalOpt: 'global', plugins: [plugin1, plugin2], verifyRelease: [plugin3]}, | ||||||
|  |     }, | ||||||
|  |     {} | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   await plugins.verifyConditions({}); | ||||||
|  |   t.deepEqual(plugin1[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'}); | ||||||
|  |   t.deepEqual(plugin2[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt2: 'plugin2'}); | ||||||
|  | 
 | ||||||
|  |   await plugins.publish({}); | ||||||
|  |   t.deepEqual(plugin1[0].publish.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'}); | ||||||
|  | 
 | ||||||
|  |   await plugins.verifyRelease({}); | ||||||
|  |   t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Unknown steps of plugins configured in "plugins" are ignored', t => { | ||||||
|  |   const plugin1 = {verifyConditions: () => {}, unknown: () => {}}; | ||||||
|  |   const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {}); | ||||||
|  | 
 | ||||||
|  |   t.is(typeof plugins.verifyConditions, 'function'); | ||||||
|  |   t.is(plugins.unknown, undefined); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test('Export plugins loaded from the dependency of a shareable config module', async t => { | test('Export plugins loaded from the dependency of a shareable config module', async t => { | ||||||
|   const cwd = tempy.directory(); |   const cwd = tempy.directory(); | ||||||
|   await copy( |   await copy( | ||||||
| @ -121,11 +203,23 @@ test('Export plugins loaded from the dependency of a shareable config file', asy | |||||||
| 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 => { | ||||||
|   const analyzeCommits = {}; |   const analyzeCommits = {}; | ||||||
|   const generateNotes = {}; |   const generateNotes = {}; | ||||||
|  |   const publish = {}; | ||||||
|   const success = () => {}; |   const success = () => {}; | ||||||
|   const fail = [() => {}]; |   const fail = [() => {}]; | ||||||
| 
 | 
 | ||||||
|   const plugins = getPlugins( |   const plugins = getPlugins( | ||||||
|     {cwd, logger: t.context.logger, options: {analyzeCommits, generateNotes, success, fail}}, |     { | ||||||
|  |       cwd, | ||||||
|  |       logger: t.context.logger, | ||||||
|  |       options: { | ||||||
|  |         plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'], | ||||||
|  |         analyzeCommits, | ||||||
|  |         generateNotes, | ||||||
|  |         publish, | ||||||
|  |         success, | ||||||
|  |         fail, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|     {} |     {} | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| @ -159,14 +253,20 @@ test('Merge global options with plugin options', async t => { | |||||||
|   t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'}); |   t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'}); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Throw an error if plugins configuration are invalid', t => { | test('Throw an error for each invalid plugin configuration', t => { | ||||||
|   const errors = [ |   const errors = [ | ||||||
|     ...t.throws(() => |     ...t.throws(() => | ||||||
|       getPlugins( |       getPlugins( | ||||||
|         { |         { | ||||||
|           cwd, |           cwd, | ||||||
|           logger: t.context.logger, |           logger: t.context.logger, | ||||||
|           options: {verifyConditions: {}, analyzeCommits: [], verifyRelease: [{}], generateNotes: [{path: null}]}, |           options: { | ||||||
|  |             plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'], | ||||||
|  |             verifyConditions: 1, | ||||||
|  |             analyzeCommits: [], | ||||||
|  |             verifyRelease: [{}], | ||||||
|  |             generateNotes: [{path: null}], | ||||||
|  |           }, | ||||||
|         }, |         }, | ||||||
|         {} |         {} | ||||||
|       ) |       ) | ||||||
| @ -182,3 +282,38 @@ test('Throw an error if plugins configuration are invalid', t => { | |||||||
|   t.is(errors[3].name, 'SemanticReleaseError'); |   t.is(errors[3].name, 'SemanticReleaseError'); | ||||||
|   t.is(errors[3].code, 'EPLUGINCONF'); |   t.is(errors[3].code, 'EPLUGINCONF'); | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', t => { | ||||||
|  |   const errors = [ | ||||||
|  |     ...t.throws(() => | ||||||
|  |       getPlugins( | ||||||
|  |         { | ||||||
|  |           cwd, | ||||||
|  |           logger: t.context.logger, | ||||||
|  |           options: {plugins: ['./test/fixtures/multi-plugin', './test/fixtures/plugin-noop', () => {}]}, | ||||||
|  |         }, | ||||||
|  |         {} | ||||||
|  |       ) | ||||||
|  |     ), | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|  |   t.is(errors[0].code, 'EPLUGINSCONF'); | ||||||
|  |   t.is(errors[1].name, 'SemanticReleaseError'); | ||||||
|  |   t.is(errors[1].code, 'EPLUGINSCONF'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', t => { | ||||||
|  |   const errors = [ | ||||||
|  |     ...t.throws(() => | ||||||
|  |       getPlugins({cwd, logger: t.context.logger, options: {plugins: [1, {path: 1}, [() => {}, {}, {}]]}}, {}) | ||||||
|  |     ), | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   t.is(errors[0].name, 'SemanticReleaseError'); | ||||||
|  |   t.is(errors[0].code, 'EPLUGINSCONF'); | ||||||
|  |   t.is(errors[1].name, 'SemanticReleaseError'); | ||||||
|  |   t.is(errors[1].code, 'EPLUGINSCONF'); | ||||||
|  |   t.is(errors[2].name, 'SemanticReleaseError'); | ||||||
|  |   t.is(errors[2].code, 'EPLUGINSCONF'); | ||||||
|  | }); | ||||||
|  | |||||||
| @ -1,58 +1,306 @@ | |||||||
| import test from 'ava'; | import test from 'ava'; | ||||||
| import {validateConfig} from '../../lib/plugins/utils'; | import {validatePlugin, validateStep, loadPlugin, parseConfig} from '../../lib/plugins/utils'; | ||||||
| 
 | 
 | ||||||
| test('Validate multiple/optional plugin configuration', t => { | test('validatePlugin', t => { | ||||||
|  |   const path = 'plugin-module'; | ||||||
|  |   const options = {option1: 'value1', option2: 'value2'}; | ||||||
|  | 
 | ||||||
|  |   t.true(validatePlugin(path), 'String definition'); | ||||||
|  |   t.true(validatePlugin({publish: () => {}}), 'Object definition'); | ||||||
|  |   t.true(validatePlugin([path]), 'Array definition'); | ||||||
|  |   t.true(validatePlugin([path, options]), 'Array definition with options'); | ||||||
|  |   t.true(validatePlugin([{publish: () => {}}, options]), 'Array definition with options and path as object'); | ||||||
|  |   t.true(validatePlugin({path}), 'Object with path definition'); | ||||||
|  |   t.true(validatePlugin({path, ...options}), 'Object with path definition with options'); | ||||||
|  |   t.true( | ||||||
|  |     validatePlugin({path: {publish: () => {}}, ...options}), | ||||||
|  |     'Object with path  definition with options and path as object' | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   t.false(validatePlugin(1), 'String definition, wrong path'); | ||||||
|  |   t.false(validatePlugin([]), 'Array definition, missing path'); | ||||||
|  |   t.false(validatePlugin([path, options, {}]), 'Array definition, additional parameter'); | ||||||
|  |   t.false(validatePlugin([1]), 'Array definition, wrong path'); | ||||||
|  |   t.false(validatePlugin([path, 1]), 'Array definition, wrong options'); | ||||||
|  |   t.false(validatePlugin({path: 1}), 'Object definition, wrong path'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('validateStep: multiple/optional plugin configuration', t => { | ||||||
|   const type = {multiple: true, required: false}; |   const type = {multiple: true, required: false}; | ||||||
|   t.false(validateConfig(type, {})); |  | ||||||
|   t.false(validateConfig(type, {path: null})); |  | ||||||
| 
 | 
 | ||||||
|   t.true(validateConfig(type, {path: 'plugin-path.js'})); |   // Empty config
 | ||||||
|   t.true(validateConfig(type)); |   t.true(validateStep(type)); | ||||||
|   t.true(validateConfig(type, 'plugin-path.js')); |   t.true(validateStep(type, [])); | ||||||
|   t.true(validateConfig(type, ['plugin-path.js'])); | 
 | ||||||
|   t.true(validateConfig(type, () => {})); |   // Single value definition
 | ||||||
|   t.true(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |   t.true(validateStep(type, 'plugin-path.js')); | ||||||
|  |   t.true(validateStep(type, () => {})); | ||||||
|  |   t.true(validateStep(type, ['plugin-path.js'])); | ||||||
|  |   t.true(validateStep(type, [() => {}])); | ||||||
|  |   t.false(validateStep(type, {})); | ||||||
|  |   t.false(validateStep(type, [{}])); | ||||||
|  | 
 | ||||||
|  |   // Array type definition
 | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js']])); | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]])); | ||||||
|  |   t.true(validateStep(type, [[() => {}, {options: 'value'}]])); | ||||||
|  |   t.false(validateStep(type, [['plugin-path.js', 1]])); | ||||||
|  | 
 | ||||||
|  |   // Object type definition
 | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js'})); | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'})); | ||||||
|  |   t.true(validateStep(type, {path: () => {}, options: 'value'})); | ||||||
|  |   t.false(validateStep(type, {path: null})); | ||||||
|  | 
 | ||||||
|  |   // Considered as an Array of 2 definitions and not as one Array definition in case of a muliple plugin type
 | ||||||
|  |   t.false(validateStep(type, [() => {}, {options: 'value'}])); | ||||||
|  |   t.false(validateStep(type, ['plugin-path.js', {options: 'value'}])); | ||||||
|  | 
 | ||||||
|  |   // Multiple definitions
 | ||||||
|  |   t.true( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', 1], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: null}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Validate multiple/required plugin configuration', t => { | test('validateStep: multiple/required plugin configuration', t => { | ||||||
|   const type = {multiple: true, required: true}; |   const type = {multiple: true, required: true}; | ||||||
|   t.false(validateConfig(type, {})); |  | ||||||
|   t.false(validateConfig(type, {path: null})); |  | ||||||
|   t.false(validateConfig(type)); |  | ||||||
| 
 | 
 | ||||||
|   t.true(validateConfig(type, {path: 'plugin-path.js'})); |   // Empty config
 | ||||||
|   t.true(validateConfig(type, 'plugin-path.js')); |   t.false(validateStep(type)); | ||||||
|   t.true(validateConfig(type, ['plugin-path.js'])); |   t.false(validateStep(type, [])); | ||||||
|   t.true(validateConfig(type, () => {})); | 
 | ||||||
|   t.true(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |   // Single value definition
 | ||||||
|  |   t.true(validateStep(type, 'plugin-path.js')); | ||||||
|  |   t.true(validateStep(type, () => {})); | ||||||
|  |   t.true(validateStep(type, ['plugin-path.js'])); | ||||||
|  |   t.true(validateStep(type, [() => {}])); | ||||||
|  |   t.false(validateStep(type, {})); | ||||||
|  |   t.false(validateStep(type, [{}])); | ||||||
|  | 
 | ||||||
|  |   // Array type definition
 | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js']])); | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]])); | ||||||
|  |   t.true(validateStep(type, [[() => {}, {options: 'value'}]])); | ||||||
|  |   t.false(validateStep(type, [['plugin-path.js', 1]])); | ||||||
|  | 
 | ||||||
|  |   // Object type definition
 | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js'})); | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'})); | ||||||
|  |   t.true(validateStep(type, {path: () => {}, options: 'value'})); | ||||||
|  |   t.false(validateStep(type, {path: null})); | ||||||
|  | 
 | ||||||
|  |   // Considered as an Array of 2 definitions and not as one Array definition in the case of a muliple plugin type
 | ||||||
|  |   t.false(validateStep(type, [() => {}, {options: 'value'}])); | ||||||
|  |   t.false(validateStep(type, ['plugin-path.js', {options: 'value'}])); | ||||||
|  | 
 | ||||||
|  |   // Multiple definitions
 | ||||||
|  |   t.true( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', 1], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: null}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Validate single/required plugin configuration', t => { | test('validateStep: single/required plugin configuration', t => { | ||||||
|   const type = {multiple: false, required: true}; |   const type = {multiple: false, required: true}; | ||||||
| 
 | 
 | ||||||
|   t.false(validateConfig(type, {})); |   // Empty config
 | ||||||
|   t.false(validateConfig(type, {path: null})); |   t.false(validateStep(type)); | ||||||
|   t.false(validateConfig(type, [])); |   t.false(validateStep(type, [])); | ||||||
|   t.false(validateConfig(type)); |  | ||||||
|   t.false(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |  | ||||||
| 
 | 
 | ||||||
|   t.true(validateConfig(type, {path: 'plugin-path.js'})); |   // Single value definition
 | ||||||
|   t.true(validateConfig(type, 'plugin-path.js')); |   t.true(validateStep(type, 'plugin-path.js')); | ||||||
|   t.true(validateConfig(type, ['plugin-path.js'])); |   t.true(validateStep(type, () => {})); | ||||||
|   t.true(validateConfig(type, () => {})); |   t.true(validateStep(type, ['plugin-path.js'])); | ||||||
|  |   t.true(validateStep(type, [() => {}])); | ||||||
|  |   t.false(validateStep(type, {})); | ||||||
|  |   t.false(validateStep(type, [{}])); | ||||||
|  | 
 | ||||||
|  |   // Array type definition
 | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js']])); | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]])); | ||||||
|  |   t.true(validateStep(type, [[() => {}, {options: 'value'}]])); | ||||||
|  |   t.false(validateStep(type, [['plugin-path.js', 1]])); | ||||||
|  | 
 | ||||||
|  |   // Object type definition
 | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js'})); | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'})); | ||||||
|  |   t.true(validateStep(type, {path: () => {}, options: 'value'})); | ||||||
|  |   t.false(validateStep(type, {path: null})); | ||||||
|  | 
 | ||||||
|  |   // Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type
 | ||||||
|  |   t.true(validateStep(type, [() => {}, {options: 'value'}])); | ||||||
|  |   t.true(validateStep(type, ['plugin-path.js', {options: 'value'}])); | ||||||
|  | 
 | ||||||
|  |   // Multiple definitions
 | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('Validate single/optional plugin configuration', t => { | test('validateStep: single/optional plugin configuration', t => { | ||||||
|   const type = {multiple: false, required: false}; |   const type = {multiple: false, required: false}; | ||||||
| 
 | 
 | ||||||
|   t.false(validateConfig(type, {})); |   // Empty config
 | ||||||
|   t.false(validateConfig(type, {path: null})); |   t.true(validateStep(type)); | ||||||
|   t.false(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}])); |   t.true(validateStep(type, [])); | ||||||
| 
 | 
 | ||||||
|   t.true(validateConfig(type)); |   // Single value definition
 | ||||||
|   t.true(validateConfig(type, [])); |   t.true(validateStep(type, 'plugin-path.js')); | ||||||
|   t.true(validateConfig(type, {path: 'plugin-path.js'})); |   t.true(validateStep(type, () => {})); | ||||||
|   t.true(validateConfig(type, 'plugin-path.js')); |   t.true(validateStep(type, ['plugin-path.js'])); | ||||||
|   t.true(validateConfig(type, ['plugin-path.js'])); |   t.true(validateStep(type, [() => {}])); | ||||||
|   t.true(validateConfig(type, () => {})); |   t.false(validateStep(type, {})); | ||||||
|  |   t.false(validateStep(type, [{}])); | ||||||
|  | 
 | ||||||
|  |   // Array type definition
 | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js']])); | ||||||
|  |   t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]])); | ||||||
|  |   t.true(validateStep(type, [[() => {}, {options: 'value'}]])); | ||||||
|  |   t.false(validateStep(type, [['plugin-path.js', 1]])); | ||||||
|  | 
 | ||||||
|  |   // Object type definition
 | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js'})); | ||||||
|  |   t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'})); | ||||||
|  |   t.true(validateStep(type, {path: () => {}, options: 'value'})); | ||||||
|  |   t.false(validateStep(type, {path: null})); | ||||||
|  | 
 | ||||||
|  |   // Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type
 | ||||||
|  |   t.true(validateStep(type, [() => {}, {options: 'value'}])); | ||||||
|  |   t.true(validateStep(type, ['plugin-path.js', {options: 'value'}])); | ||||||
|  | 
 | ||||||
|  |   // Multiple definitions
 | ||||||
|  |   t.false( | ||||||
|  |     validateStep(type, [ | ||||||
|  |       'plugin-path.js', | ||||||
|  |       () => {}, | ||||||
|  |       ['plugin-path.js'], | ||||||
|  |       ['plugin-path.js', {options: 'value'}], | ||||||
|  |       [() => {}, {options: 'value'}], | ||||||
|  |       {path: 'plugin-path.js'}, | ||||||
|  |       {path: 'plugin-path.js', options: 'value'}, | ||||||
|  |       {path: () => {}, options: 'value'}, | ||||||
|  |     ]) | ||||||
|  |   ); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('loadPlugin', t => { | ||||||
|  |   const cwd = process.cwd(); | ||||||
|  |   const func = () => {}; | ||||||
|  | 
 | ||||||
|  |   t.is(require('../fixtures/plugin-noop'), loadPlugin({cwd: './test/fixtures'}, './plugin-noop', {}), 'From cwd'); | ||||||
|  |   t.is( | ||||||
|  |     require('../fixtures/plugin-noop'), | ||||||
|  |     loadPlugin({cwd}, './plugin-noop', {'./plugin-noop': './test/fixtures'}), | ||||||
|  |     'From a shareable config context' | ||||||
|  |   ); | ||||||
|  |   t.is(func, loadPlugin({cwd}, func, {}), 'Defined as a function'); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test('parseConfig', t => { | ||||||
|  |   const path = 'plugin-module'; | ||||||
|  |   const options = {option1: 'value1', option2: 'value2'}; | ||||||
|  | 
 | ||||||
|  |   t.deepEqual(parseConfig(path), [path, {}], 'String definition'); | ||||||
|  |   t.deepEqual(parseConfig({path}), [path, {}], 'Object definition'); | ||||||
|  |   t.deepEqual(parseConfig({path, ...options}), [path, options], 'Object definition with options'); | ||||||
|  |   t.deepEqual(parseConfig([path]), [path, {}], 'Array definition'); | ||||||
|  |   t.deepEqual(parseConfig([path, options]), [path, options], 'Array definition with options'); | ||||||
| }); | }); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user