semantic-release 13.2.0 → 13.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,10 +37,11 @@ This removes the immediate connection between human emotions and version numbers
37
37
  - Fully automated release
38
38
  - Enforce [Semantic Versioning](https://semver.org) specification
39
39
  - New features and fixes are immediately available to users
40
+ - Notify maintainers and users of new releases
40
41
  - Use formalized commit message convention to document changes in the codebase
41
42
  - Integrate with your [continuous integration workflow](docs/recipes/README.md#ci-configurations)
42
43
  - Avoid potential errors associated with manual releases
43
- - Support any [package managers and languages](docs/recipes/README.md#package-managers-and-languages) via [plugins](docs/usage/plugins.md)
44
+ - Support any [package managers and languages](docs/recipes/README.md#package-managers-and-languages) via [plugins](docs/usage/plugins.md)
44
45
  - Simple and reusable configuration via [shareable configurations](docs/usage/shareable-configurations.md)
45
46
 
46
47
  ## How does it work?
@@ -67,7 +68,7 @@ Here is an example of the release type that will be done based on a commit messa
67
68
 
68
69
  ### Triggering a release
69
70
 
70
- When pushing new commits to the release branch (i.e. `master`) with `git push` or by merging a pull request or merging from another branch, a CI build is triggered and runs the `semantic-release` command to make a release if there is relevant codebase changes since the last release.
71
+ When pushing new commits to the release branch (i.e. `master`) with `git push` or by merging a pull request or merging from another branch, a CI build is triggered and runs the `semantic-release` command to make a release if there are relevant codebase changes since the last release.
71
72
 
72
73
  By default a release will be done for each push to the release branch that contains relevant code changes. If you need more control over the timing of releases you have a couple of options:
73
74
  - Publish releases on a distribution channel (for example npm’s [dist-tags](https://docs.npmjs.com/cli/dist-tag)). This way you can keep control over what your users end up using by default, and you can decide when to make an automatically released version available to the stable channel, and promote it.
@@ -86,6 +87,7 @@ After running the tests the command `semantic-release` will execute the followin
86
87
  | Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
87
88
  | Create Git tag | Create a Git tag corresponding the new release version |
88
89
  | Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
90
+ | 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. |
89
91
 
90
92
  ## Documentation
91
93
 
package/cli.js CHANGED
@@ -1,59 +1,64 @@
1
- const program = require('commander');
2
1
  const {pickBy, isUndefined} = require('lodash');
3
2
 
4
- function list(values) {
5
- return values.split(',').map(value => value.trim());
6
- }
3
+ const stringList = {
4
+ type: 'string',
5
+ array: true,
6
+ coerce: values =>
7
+ values.length === 1 && values[0].trim() === 'false'
8
+ ? []
9
+ : values.reduce((values, value) => values.concat(value.split(',').map(value => value.trim())), []),
10
+ };
7
11
 
8
12
  module.exports = async () => {
9
- program
10
- .name('semantic-release')
11
- .description('Run automated package publishing')
12
- .option('-b, --branch <branch>', 'Branch to release from')
13
- .option('-r, --repository-url <repositoryUrl>', 'Git repository URL')
14
- .option('-t, --tag-format <tagFormat>', `Git tag format`)
15
- .option('-e, --extends <paths>', 'Comma separated list of shareable config paths or packages name', list)
16
- .option(
17
- '--verify-conditions <paths>',
18
- 'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
19
- list
20
- )
21
- .option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin')
22
- .option(
23
- '--verify-release <paths>',
24
- 'Comma separated list of paths or packages name for the verifyRelease plugin(s)',
25
- list
26
- )
27
- .option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
28
- .option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list)
29
- .option(
30
- '--no-ci',
31
- 'Skip Continuous Integration environment verifications, allowing to make releases from a local machine'
32
- )
33
- .option('--debug', 'Output debugging information')
34
- .option(
35
- '-d, --dry-run',
36
- 'Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes'
37
- )
38
- .parse(process.argv);
39
-
40
- if (program.debug) {
41
- // Debug must be enabled before other requires in order to work
42
- require('debug').enable('semantic-release:*');
43
- }
13
+ const cli = require('yargs')
14
+ .command('$0', 'Run automated package publishing', yargs => {
15
+ yargs.demandCommand(0, 0).usage(`Run automated package publishing
16
+ Usage:
17
+ semantic-release [options] [plugins]`);
18
+ })
19
+ .option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
20
+ .option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
21
+ .option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
22
+ .option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
23
+ .option('ci', {describe: 'Toggle CI verifications', default: true, type: 'boolean', group: 'Options'})
24
+ .option('verify-conditions', {...stringList, group: 'Plugins'})
25
+ .option('analyze-commits', {type: 'string', group: 'Plugins'})
26
+ .option('verify-release', {...stringList, group: 'Plugins'})
27
+ .option('generate-notes', {type: 'string', group: 'Plugins'})
28
+ .option('publish', {...stringList, group: 'Plugins'})
29
+ .option('success', {...stringList, group: 'Plugins'})
30
+ .option('fail', {...stringList, group: 'Plugins'})
31
+ .option('debug', {describe: 'Output debugging information', default: false, type: 'boolean', group: 'Options'})
32
+ .option('d', {alias: 'dry-run', describe: 'Skip publishing', default: false, type: 'boolean', group: 'Options'})
33
+ .option('h', {alias: 'help', group: 'Options'})
34
+ .option('v', {alias: 'version', group: 'Options'})
35
+ .strict(false)
36
+ .exitProcess(false);
44
37
 
45
38
  try {
46
- if (program.args.length > 0) {
47
- program.outputHelp();
48
- process.exitCode = 1;
49
- } else {
50
- const opts = program.opts();
51
- // Set the `noCi` options as commander.js sets the `ci` options instead (becasue args starts with `--no`)
52
- opts.noCi = opts.ci === false ? true : undefined;
53
- // Remove option with undefined values, as commander.js sets non defined options as `undefined`
54
- await require('.')(pickBy(opts, value => !isUndefined(value)));
39
+ const {help, version, ...opts} = cli.argv;
40
+ if (Boolean(help) || Boolean(version)) {
41
+ process.exitCode = 0;
42
+ return;
43
+ }
44
+
45
+ // Set the `noCi` options as yargs sets the `ci` options instead (because arg starts with `--no`)
46
+ if (opts.ci === false) {
47
+ opts.noCi = true;
55
48
  }
49
+
50
+ if (opts.debug) {
51
+ // Debug must be enabled before other requires in order to work
52
+ require('debug').enable('semantic-release:*');
53
+ }
54
+
55
+ // Remove option with undefined values, as yargs sets non defined options as `undefined`
56
+ await require('.')(pickBy(opts, value => !isUndefined(value)));
57
+ process.exitCode = 0;
56
58
  } catch (err) {
59
+ if (err.name !== 'YError') {
60
+ console.error(err);
61
+ }
57
62
  process.exitCode = 1;
58
63
  }
59
64
  };
package/docs/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # semantic-release documentation
2
2
 
3
3
  - [Usage](usage/README.md) - **semantic-release** installation and configuration
4
- - [Extending][extending/README.md]- Extending **semantic-release** with plugins and shareable configurations
4
+ - [Extending](extending/README.md)- Extending **semantic-release** with plugins and shareable configurations
5
5
  - [Recipes](recipes/README.md) - Community written recipes for common **semantic-release** use-cases
6
6
  - [Developer Guide](developer-guide/README.md) - The essentials of writing a **semantic-release** plugin or shareable configurations
7
7
  - [Resources](resources.md) - Videos, articles and tutorials
@@ -5,6 +5,8 @@
5
5
  - [@semantic-release/github](https://github.com/semantic-release/github)
6
6
  - [verifyConditions](https://github.com/semantic-release/github#verifyconditions): Verify the presence and the validity of the GitHub authentication and release configuration
7
7
  - [publish](https://github.com/semantic-release/github#publish): Publish a [GitHub release](https://help.github.com/articles/about-releases)
8
+ - [success](https://github.com/semantic-release/github#success): Add a comment to GitHub issues and pull requests resolved in the release
9
+ - [fail](https://github.com/semantic-release/github#fail): Open a GitHub issue when a release fails
8
10
  - [@semantic-release/npm](https://github.com/semantic-release/npm)
9
11
  - [verifyConditions](https://github.com/semantic-release/npm#verifyconditions): Verify the presence and the validity of the npm authentication and release configuration
10
12
  - [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry
@@ -25,6 +27,8 @@
25
27
  - [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release
26
28
  - [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.
27
29
  - [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note
28
- - [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release.
30
+ - [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release
31
+ - [success](https://github.com/semantic-release/exec#success): Execute a shell command to notify of a new release
32
+ - [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
29
33
 
30
34
  ## Community plugins
@@ -1 +1,65 @@
1
- # CircleCI 2.0 workflows
1
+ # Using semantic-release with [CircleCI 2.0 workflows](https://circleci.com/docs/2.0/workflows)
2
+
3
+ ## Environment variables
4
+
5
+ The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [CircleCi Project Settings](https://circleci.com/docs/2.0/env-vars/#adding-environment-variables-in-the-app)..
6
+
7
+ Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
8
+
9
+ ## Multiple Node jobs configuration
10
+
11
+ ### `.circleci/config.yml` configuration for multiple Node jobs
12
+
13
+ This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [CircleCI documentation](https://circleci.com/docs/2.0) for additional configuration options.
14
+
15
+ This example create the workflows `test_node_4`, `test_node_6`, `test_node_8` and `release`. The release workflows will [run `semantic-release` only after the all the `test_node_*` are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
16
+
17
+ ```yaml
18
+ version: 2
19
+ jobs:
20
+ test_node_6:
21
+ docker:
22
+ - image: circleci/node:6
23
+ steps:
24
+ # Configure your test steps here (checkout, npm install, cache management, tests etc...)
25
+
26
+ test_node_8:
27
+ docker:
28
+ - image: circleci/node:8
29
+ steps:
30
+ # Configure your test steps here (checkout, npm install, cache management, tests etc...)
31
+
32
+ release:
33
+ docker:
34
+ - image: circleci/node:8
35
+ steps:
36
+ - checkout
37
+ - run: npm install
38
+ # Run optional required steps before releasing
39
+ # - run: npm run build-script
40
+ - run: npx semantic-release
41
+
42
+ workflows:
43
+ version: 2
44
+ test_and_release:
45
+ # Run the test jobs first, then the release only when all the test jobs are successful
46
+ jobs:
47
+ - test_node_6
48
+ - test_node_8
49
+ - release:
50
+ requires:
51
+ - test_node_6
52
+ - test_node_8
53
+ ```
54
+
55
+ ### `package.json` configuration for multiple Node jobs
56
+
57
+ A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
58
+
59
+ ```json
60
+ {
61
+ "devDependencies": {
62
+ "semantic-release": "^12.0.0"
63
+ }
64
+ }
65
+ ```
@@ -8,11 +8,11 @@ The [Authentication](../usage/ci-configuration.md#authentication) environment va
8
8
 
9
9
  GitLab CI supports [Pipelines](https://docs.gitlab.com/ee/ci/pipelines.html) allowing to test on multiple Node versions and publishing a release only when all test pass.
10
10
 
11
- **Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--8).
11
+ **Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
12
12
 
13
13
  ### `.gitlab-ci.yml` configuration for Node projects
14
14
 
15
- This example is a minimal configuration for **semantic-release** with a build running Node 4, 6 and 8 on Linux. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
15
+ This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
16
16
 
17
17
  **Note**: The`semantic-release` execution command varies depending if you are using a [local](../usage/installation.md#local-installation) or [global](../usage/installation.md#global-installation) **semantic-release** installation.
18
18
 
@@ -25,12 +25,6 @@ stages:
25
25
  before_script:
26
26
  - npm install
27
27
 
28
- node:4:
29
- image: node:4
30
- stage: test
31
- script:
32
- - npm test
33
-
34
28
  node:6:
35
29
  image: node:6
36
30
  stage: test
@@ -1 +1,95 @@
1
1
  # Using semantic-release with [Travis CI build stages](https://docs.travis-ci.com/user/build-stages)
2
+
3
+ ## Environment variables
4
+
5
+ The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [Travis Repository Settings](https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-Settings) or with the [travis env set CLI](https://github.com/travis-ci/travis.rb#env).
6
+
7
+ Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
8
+
9
+ ## Multiple Node jobs configuration
10
+
11
+ ### `.travis.yml` configuration for multiple Node jobs
12
+
13
+ This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
14
+
15
+ This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
16
+
17
+ It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
18
+
19
+ **Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
20
+
21
+ **Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
22
+
23
+ ```yaml
24
+ language: node_js
25
+
26
+ node_js:
27
+ - 8
28
+ - 6
29
+
30
+ jobs:
31
+ include:
32
+ # Define the release stage that runs semantic-release
33
+ - stage: release
34
+ node_js: lts/*
35
+ # Advanced: optionally overwrite your default `script` step to skip the tests
36
+ # script: skip
37
+ deploy:
38
+ provider: script
39
+ skip_cleanup: true
40
+ script:
41
+ - npx semantic-release
42
+ ```
43
+
44
+ ### `package.json` configuration for multiple Node jobs
45
+
46
+ A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
47
+
48
+ ```json
49
+ {
50
+ "devDependencies": {
51
+ "semantic-release": "^12.0.0"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Non-JavaScript projects configuration
57
+
58
+ For projects that require to be tested with one or multiple version of a Non-JavaScript [language](https://docs.travis-ci.com/user/languages), optionally on multiple [Operating Systems](https://docs.travis-ci.com/user/multi-os).
59
+
60
+ This recipe cover the Travis specifics only. See [Non JavaScript projects recipe](../support/FAQ.md#can-i-use-semantic-release-to-publish-non-javascript-packages) for more information on the **semantic-release** configuration.
61
+
62
+ ### `.travis.yml` configuration for non-JavaScript projects
63
+
64
+ This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go). See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
65
+
66
+ This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
67
+
68
+ It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
69
+
70
+ **Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
71
+
72
+ **Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
73
+
74
+ ```yaml
75
+ language: go
76
+
77
+ go:
78
+ - 1.6
79
+ - 1.7
80
+
81
+ jobs:
82
+ include:
83
+ # Define the release stage that runs semantic-release
84
+ - stage: release
85
+ # Advanced: optionally overwrite your default `script` step to skip the tests
86
+ # script:
87
+ # - make
88
+ deploy:
89
+ provider: script
90
+ skip_cleanup: true
91
+ script:
92
+ # Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
93
+ - nvm install lts/*
94
+ - npx semantic-release
95
+ ```
@@ -10,7 +10,7 @@ Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with
10
10
 
11
11
  For projects that require to be tested only with a single [Node version](https://docs.travis-ci.com/user/getting-started/#Selecting-a-different-programming-language) on [one Operating System](https://docs.travis-ci.com/user/getting-started/#Selecting-infrastructure-(optional)).
12
12
 
13
- **Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--8).
13
+ **Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
14
14
 
15
15
  ### `.travis.yml` configuration for single Node job
16
16
 
@@ -25,10 +25,6 @@ language: node_js
25
25
 
26
26
  node_js: 8
27
27
 
28
- script:
29
- # Run tests
30
- - npm run test
31
-
32
28
  deploy:
33
29
  provider: script
34
30
  skip_cleanup: true
@@ -56,9 +52,9 @@ For projects that require to be tested with multiple [Node versions](https://doc
56
52
 
57
53
  ### `.travis.yml` configuration for multiple Node jobs
58
54
 
59
- This example is a minimal configuration for **semantic-release** with a build running Node 4, 6 and 8 on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
55
+ This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
60
56
 
61
- This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to command [Run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
57
+ This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to [Run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
62
58
 
63
59
  It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
64
60
 
@@ -70,15 +66,6 @@ language: node_js
70
66
  node_js:
71
67
  - 8
72
68
  - 6
73
- - 4
74
-
75
- os:
76
- - linux
77
- - osx
78
-
79
- script:
80
- # Run tests
81
- - npm run test
82
69
 
83
70
  deploy:
84
71
  provider: script
@@ -110,7 +97,7 @@ This recipe cover the Travis specifics only. See [Non JavaScript projects recipe
110
97
 
111
98
  ### `.travis.yml` configuration for non-JavaScript projects
112
99
 
113
- This example is a minimal configuration for semantic-release with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go) on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
100
+ This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go) on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
114
101
 
115
102
  This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to [run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
116
103
 
@@ -129,17 +116,13 @@ os:
129
116
  - linux
130
117
  - osx
131
118
 
132
- script:
133
- # Run tests
134
- - go test -v ./...
135
-
136
119
  deploy:
137
120
  provider: script
138
121
  skip_cleanup: true
139
122
  script:
140
123
  # Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
141
124
  - nvm install lts/*
142
- # Run semantic-release only on job, after all other are successful
125
+ # Run semantic-release only on one job, after all other are successful
143
126
  - npx travis-deploy-once "npx semantic-release"
144
127
  ```
145
128
 
@@ -131,7 +131,7 @@ See [Artifactory - npm Registry](https://www.jfrog.com/confluence/display/RTF/Np
131
131
 
132
132
  You can trigger a release by pushing to your Git repository. You deliberately cannot trigger a *specific* version release, because this is the whole point of semantic-release.
133
133
 
134
- ### Can I exclude commits from the analysis?
134
+ ## Can I exclude commits from the analysis?
135
135
 
136
136
  Yes, every commits that contains `[skip release]` or `[release skip]` in their message will be excluded from the commit analysis and won't participate in the release type determination.
137
137
 
@@ -59,7 +59,7 @@ Default: `repository` property in `package.json` or [git origin url](https://git
59
59
 
60
60
  CLI arguments: `-r`, `--repository-url`
61
61
 
62
- The git repository URL
62
+ The git repository URL.
63
63
 
64
64
  Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)).
65
65
 
@@ -75,7 +75,7 @@ CLI arguments: `-t`, `--tag-format`
75
75
 
76
76
  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.
77
77
 
78
- **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).
78
+ **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).
79
79
 
80
80
  ### dryRun
81
81
 
@@ -166,3 +166,27 @@ CLI argument: `--publish`
166
166
  Define the list of [publish plugins](plugins.md#publish-plugin). Plugins will run in series, in the order defined in the `Array`.
167
167
 
168
168
  See [Plugins configuration](plugins.md#configuration) for more details.
169
+
170
+ ### success
171
+
172
+ Type: `Array`, `String`, `Object`
173
+
174
+ Default: `[]`
175
+
176
+ CLI argument: `--success`
177
+
178
+ Define the list of [success plugins](plugins.md#success-plugin). Plugins will run in series, in the order defined in the `Array`.
179
+
180
+ See [Plugins configuration](plugins.md#configuration) for more details.
181
+
182
+ ### fail
183
+
184
+ Type: `Array`, `String`, `Object`
185
+
186
+ Default: `[]`
187
+
188
+ CLI argument: `--fail`
189
+
190
+ Define the list of [fail plugins](plugins.md#fail-plugin). Plugins will run in series, in the order defined in the `Array`.
191
+
192
+ See [Plugins configuration](plugins.md#configuration) for more details.
@@ -34,6 +34,18 @@ Plugin responsible for publishing the release.
34
34
 
35
35
  Default implementation: [npm](https://github.com/semantic-release/npm#publish) and [github](https://github.com/semantic-release/github#publish).
36
36
 
37
+ ### success plugin
38
+
39
+ Plugin responsible for notifying of a new release.
40
+
41
+ Default implementation: [github](https://github.com/semantic-release/github#success).
42
+
43
+ ### fail plugin
44
+
45
+ Plugin responsible for notifying of a failed release.
46
+
47
+ Default implementation: [github](https://github.com/semantic-release/github#fail).
48
+
37
49
  ## Configuration
38
50
 
39
51
  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`.
package/index.js CHANGED
@@ -1,21 +1,23 @@
1
- const {template} = require('lodash');
1
+ const {template, isPlainObject, castArray} = require('lodash');
2
2
  const marked = require('marked');
3
3
  const TerminalRenderer = require('marked-terminal');
4
4
  const envCi = require('env-ci');
5
5
  const hookStd = require('hook-std');
6
+ const pkg = require('./package.json');
6
7
  const hideSensitive = require('./lib/hide-sensitive');
7
8
  const getConfig = require('./lib/get-config');
8
9
  const verify = require('./lib/verify');
9
10
  const getNextVersion = require('./lib/get-next-version');
10
11
  const getCommits = require('./lib/get-commits');
11
12
  const getLastRelease = require('./lib/get-last-release');
13
+ const {extractErrors} = require('./lib/utils');
12
14
  const logger = require('./lib/logger');
13
15
  const {unshallow, gitHead: getGitHead, tag, push, deleteTag} = require('./lib/git');
14
16
 
15
- async function run(opts) {
17
+ marked.setOptions({renderer: new TerminalRenderer()});
18
+
19
+ async function run(options, plugins) {
16
20
  const {isCi, branch, isPr} = envCi();
17
- const config = await getConfig(opts, logger);
18
- const {plugins, options} = config;
19
21
 
20
22
  if (!isCi && !options.dryRun && !options.noCi) {
21
23
  logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
@@ -34,7 +36,7 @@ async function run(opts) {
34
36
  logger.log('Run automated release from branch %s', options.branch);
35
37
 
36
38
  logger.log('Call plugin %s', 'verify-conditions');
37
- await plugins.verifyConditions({options, logger}, true);
39
+ await plugins.verifyConditions({options, logger}, {settleAll: true});
38
40
 
39
41
  // Unshallow the repo in order to get all the tags
40
42
  await unshallow();
@@ -57,14 +59,13 @@ async function run(opts) {
57
59
  const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})};
58
60
 
59
61
  logger.log('Call plugin %s', 'verify-release');
60
- await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, true);
62
+ await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, {settleAll: true});
61
63
 
62
64
  const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
63
65
 
64
66
  if (options.dryRun) {
65
67
  logger.log('Call plugin %s', 'generate-notes');
66
68
  const notes = await plugins.generateNotes(generateNotesParam);
67
- marked.setOptions({renderer: new TerminalRenderer()});
68
69
  logger.log('Release note for version %s:\n', nextRelease.version);
69
70
  process.stdout.write(`${marked(notes)}\n`);
70
71
  } else {
@@ -77,45 +78,86 @@ async function run(opts) {
77
78
  await push(options.repositoryUrl, branch);
78
79
 
79
80
  logger.log('Call plugin %s', 'publish');
80
- await plugins.publish({options, logger, lastRelease, commits, nextRelease}, false, async prevInput => {
81
- const newGitHead = await getGitHead();
82
- // If previous publish plugin has created a commit (gitHead changed)
83
- if (prevInput.nextRelease.gitHead !== newGitHead) {
84
- // Delete the previously created tag
85
- await deleteTag(options.repositoryUrl, nextRelease.gitTag);
86
- // Recreate the tag, referencing the new gitHead
87
- logger.log('Create tag %s', nextRelease.gitTag);
88
- await tag(nextRelease.gitTag);
89
- await push(options.repositoryUrl, branch);
90
-
91
- nextRelease.gitHead = newGitHead;
92
- // Regenerate the release notes
93
- logger.log('Call plugin %s', 'generateNotes');
94
- nextRelease.notes = await plugins.generateNotes(generateNotesParam);
81
+ const releases = await plugins.publish(
82
+ {options, logger, lastRelease, commits, nextRelease},
83
+ {
84
+ getNextInput: async lastResult => {
85
+ const newGitHead = await getGitHead();
86
+ // If previous publish plugin has created a commit (gitHead changed)
87
+ if (lastResult.nextRelease.gitHead !== newGitHead) {
88
+ // Delete the previously created tag
89
+ await deleteTag(options.repositoryUrl, nextRelease.gitTag);
90
+ // Recreate the tag, referencing the new gitHead
91
+ logger.log('Create tag %s', nextRelease.gitTag);
92
+ await tag(nextRelease.gitTag);
93
+ await push(options.repositoryUrl, branch);
94
+
95
+ nextRelease.gitHead = newGitHead;
96
+ // Regenerate the release notes
97
+ logger.log('Call plugin %s', 'generateNotes');
98
+ nextRelease.notes = await plugins.generateNotes(generateNotesParam);
99
+ }
100
+ // Call the next publish plugin with the updated `nextRelease`
101
+ return {options, logger, lastRelease, commits, nextRelease};
102
+ },
103
+ // Add nextRelease and plugin properties to published release
104
+ transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step}),
95
105
  }
96
- // Call the next publish plugin with the updated `nextRelease`
97
- return {options, logger, lastRelease, commits, nextRelease};
98
- });
106
+ );
107
+
108
+ await plugins.success(
109
+ {options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
110
+ {settleAll: true}
111
+ );
112
+
99
113
  logger.log('Published release: %s', nextRelease.version);
100
114
  }
101
115
  return true;
102
116
  }
103
117
 
118
+ function logErrors(err) {
119
+ const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
120
+ for (const error of errors) {
121
+ if (error.semanticRelease) {
122
+ logger.log(`%s ${error.message}`, error.code);
123
+ if (error.details) {
124
+ process.stdout.write(`${marked(error.details)}\n`);
125
+ }
126
+ } else {
127
+ logger.error('An error occurred while running semantic-release: %O', error);
128
+ }
129
+ }
130
+ }
131
+
132
+ async function callFail(plugins, options, error) {
133
+ const errors = extractErrors(error).filter(error => error.semanticRelease);
134
+ if (errors.length > 0) {
135
+ try {
136
+ await plugins.fail({options, logger, errors}, {settleAll: true});
137
+ } catch (err) {
138
+ logErrors(err);
139
+ }
140
+ }
141
+ }
142
+
104
143
  module.exports = async opts => {
144
+ logger.log(`Running %s version %s`, pkg.name, pkg.version);
105
145
  const unhook = hookStd({silent: false}, hideSensitive);
106
146
  try {
107
- const result = await run(opts);
108
- unhook();
109
- return result;
110
- } catch (err) {
111
- const errors = err.name === 'AggregateError' ? Array.from(err).sort(error => !error.semanticRelease) : [err];
112
- for (const error of errors) {
113
- if (error.semanticRelease) {
114
- logger.log(`%s ${error.message}`, error.code);
115
- } else {
116
- logger.error('An error occurred while running semantic-release: %O', error);
147
+ const config = await getConfig(opts, logger);
148
+ const {plugins, options} = config;
149
+ try {
150
+ const result = await run(options, plugins);
151
+ unhook();
152
+ return result;
153
+ } catch (err) {
154
+ if (!options.dryRun) {
155
+ await callFail(plugins, options, err);
117
156
  }
157
+ throw err;
118
158
  }
159
+ } catch (err) {
160
+ logErrors(err);
119
161
  unhook();
120
162
  throw err;
121
163
  }
@@ -0,0 +1,118 @@
1
+ const url = require('url');
2
+ const {inspect} = require('util');
3
+ const {toLower, isString} = require('lodash');
4
+ const pkg = require('../../package.json');
5
+ const RELEASE_TYPE = require('./release-types');
6
+
7
+ const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
8
+ const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
9
+ const linkify = file => `${homepage}/blob/caribou/${file}`;
10
+
11
+ module.exports = {
12
+ ENOGITREPO: () => ({
13
+ message: 'Not running from a git repository.',
14
+ details: `The \`semantic-release\` command must be executed from a Git repository.
15
+
16
+ The current working directory is \`${process.cwd()}\`.
17
+
18
+ Please verify your CI configuration to make sure the \`semantic-release\` command is executed from the root of the cloned repository.`,
19
+ }),
20
+ ENOREPOURL: () => ({
21
+ message: 'The `repositoryUrl` option is required.',
22
+ details: `The [repositoryUrl option](${linkify(
23
+ 'docs/usage/configuration.md#repositoryurl'
24
+ )}) 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).
25
+
26
+ Please make sure to add the \`repositoryUrl\` to the [semantic-release configuration] (${linkify(
27
+ 'docs/usage/configuration.md'
28
+ )}).`,
29
+ }),
30
+ EGITNOPERMISSION: ({options}) => ({
31
+ message: 'The push permission to the Git repository is required.',
32
+ details: `**semantic-release** cannot push the version tag to the branch \`${
33
+ options.branch
34
+ }\` on remote Git repository.
35
+
36
+ Please refer to the [authentication configuration documentation](${linkify(
37
+ 'docs/usage/ci-configuration.md#authentication'
38
+ )}) to configure the Git credentials on your CI environment.`,
39
+ }),
40
+ EINVALIDTAGFORMAT: ({tagFormat}) => ({
41
+ message: 'Invalid `tagFormat` option.',
42
+ details: `The [tagFormat](${linkify(
43
+ 'docs/usage/configuration.md#tagformat'
44
+ )}) must compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
45
+
46
+ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
47
+ }),
48
+ ETAGNOVERSION: ({tagFormat}) => ({
49
+ message: 'Invalid `tagFormat` option.',
50
+ details: `The [tagFormat](${linkify(
51
+ 'docs/usage/configuration.md#tagformat'
52
+ )}) option must contain the variable \`version\` exactly once.
53
+
54
+ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
55
+ }),
56
+ EPLUGINCONF: ({pluginType, pluginConf}) => ({
57
+ message: `The \`${pluginType}\` plugin configuration is invalid.`,
58
+ details: `The [${pluginType} plugin configuration](${linkify(
59
+ `docs/usage/plugins.md#${toLower(pluginType)}-plugin`
60
+ )}) 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.
61
+
62
+ Your configuration for the \`${pluginType}\` plugin is \`${stringify(pluginConf)}\`.`,
63
+ }),
64
+ EPLUGIN: ({pluginName, pluginType}) => ({
65
+ message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`,
66
+ details: `A valid \`${pluginType}\` **semantic-release** plugin must be a function or an object with a function in the property \`${pluginType}\`.
67
+
68
+ The plugin \`${pluginName}\` doesn't have the property \`${pluginType}\` and cannot be used for the \`${pluginType}\` step.
69
+
70
+ Please refer to the \`${pluginName}\` and [semantic-release plugins configuration](${linkify(
71
+ 'docs/usage/plugins.md'
72
+ )}) documentation for more details.`,
73
+ }),
74
+ EANALYZEOUTPUT: ({result, pluginName}) => ({
75
+ message: 'The `analyzeCommits` plugin returned an invalid value. It must return a valid semver release type.',
76
+ details: `The \`analyzeCommits\` plugin must return a valid [semver](https://semver.org) release type. The valid values are: ${RELEASE_TYPE.map(
77
+ type => `\`${type}\``
78
+ ).join(', ')}.
79
+
80
+ The \`analyzeCommits\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
81
+
82
+ We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
83
+ - The **semantic-release** version: \`${pkg.version}\`
84
+ - The **semantic-release** logs from your CI job
85
+ - The value returned by the plugin: \`${stringify(result)}\`
86
+ - A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify(
87
+ 'docs/developer-guide/plugin.md'
88
+ )})`,
89
+ }),
90
+ ERELEASENOTESOUTPUT: ({result, pluginName}) => ({
91
+ message: 'The `generateNotes` plugin returned an invalid value. It must return a `String`.',
92
+ details: `The \`generateNotes\` plugin must return a \`String\`.
93
+
94
+ The \`generateNotes\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
95
+
96
+ We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
97
+ - The **semantic-release** version: \`${pkg.version}\`
98
+ - The **semantic-release** logs from your CI job
99
+ - The value returned by the plugin: \`${stringify(result)}\`
100
+ - A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify(
101
+ 'docs/developer-guide/plugin.md'
102
+ )})`,
103
+ }),
104
+ EPUBLISHOUTPUT: ({result, pluginName}) => ({
105
+ message: 'A `publish` plugin returned an invalid value. It must return an `Object`.',
106
+ details: `The \`publish\` plugins must return an \`Object\`.
107
+
108
+ The \`publish\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
109
+
110
+ We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
111
+ - The **semantic-release** version: \`${pkg.version}\`
112
+ - The **semantic-release** logs from your CI job
113
+ - The value returned by the plugin: \`${stringify(result)}\`
114
+ - A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify(
115
+ 'docs/developer-guide/plugin.md'
116
+ )})`,
117
+ }),
118
+ };
@@ -0,0 +1,61 @@
1
+ const {isString, isFunction, isArray, isPlainObject} = require('lodash');
2
+ const RELEASE_TYPE = require('./release-types');
3
+
4
+ const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
5
+
6
+ module.exports = {
7
+ verifyConditions: {
8
+ default: ['@semantic-release/npm', '@semantic-release/github'],
9
+ config: {
10
+ validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
11
+ },
12
+ },
13
+ analyzeCommits: {
14
+ default: '@semantic-release/commit-analyzer',
15
+ config: {
16
+ validator: conf => Boolean(conf) && validatePluginConfig(conf),
17
+ },
18
+ output: {
19
+ validator: output => !output || RELEASE_TYPE.includes(output),
20
+ error: 'EANALYZEOUTPUT',
21
+ },
22
+ },
23
+ verifyRelease: {
24
+ default: false,
25
+ config: {
26
+ validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
27
+ },
28
+ },
29
+ generateNotes: {
30
+ default: '@semantic-release/release-notes-generator',
31
+ config: {
32
+ validator: conf => !conf || validatePluginConfig(conf),
33
+ },
34
+ output: {
35
+ validator: output => !output || isString(output),
36
+ error: 'ERELEASENOTESOUTPUT',
37
+ },
38
+ },
39
+ publish: {
40
+ default: ['@semantic-release/npm', '@semantic-release/github'],
41
+ config: {
42
+ validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
43
+ },
44
+ output: {
45
+ validator: output => !output || isPlainObject(output),
46
+ error: 'EPUBLISHOUTPUT',
47
+ },
48
+ },
49
+ success: {
50
+ default: false,
51
+ config: {
52
+ validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
53
+ },
54
+ },
55
+ fail: {
56
+ default: false,
57
+ config: {
58
+ validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
59
+ },
60
+ },
61
+ };
@@ -0,0 +1 @@
1
+ module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
package/lib/get-config.js CHANGED
@@ -4,7 +4,7 @@ const cosmiconfig = require('cosmiconfig');
4
4
  const resolveFrom = require('resolve-from');
5
5
  const debug = require('debug')('semantic-release:config');
6
6
  const {repoUrl} = require('./git');
7
- const PLUGINS_DEFINITION = require('./plugins/definitions');
7
+ const PLUGINS_DEFINITIONS = require('./definitions/plugins');
8
8
  const plugins = require('./plugins');
9
9
  const getGitAuthUrl = require('./get-git-auth-url');
10
10
 
@@ -25,7 +25,7 @@ module.exports = async (opts, logger) => {
25
25
  // For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
26
26
  // so those plugin will be loaded relatively to the config file
27
27
  Object.keys(extendsOpts).reduce((pluginsPath, option) => {
28
- if (PLUGINS_DEFINITION[option]) {
28
+ if (PLUGINS_DEFINITIONS[option]) {
29
29
  castArray(extendsOpts[option])
30
30
  .filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
31
31
  .map(plugin => (isString(plugin) ? plugin : plugin.path))
@@ -42,17 +42,17 @@ module.exports = async (opts, logger) => {
42
42
  };
43
43
  }
44
44
 
45
- const repositoryUrl = (await pkgRepoUrl()) || (await repoUrl());
46
-
47
45
  // Set default options values if not defined yet
48
46
  options = {
49
47
  branch: 'master',
50
- repositoryUrl: repositoryUrl ? getGitAuthUrl(repositoryUrl) : repositoryUrl,
48
+ repositoryUrl: (await pkgRepoUrl()) || (await repoUrl()),
51
49
  tagFormat: `v\${version}`,
52
50
  // Remove `null` and `undefined` options so they can be replaced with default ones
53
51
  ...pickBy(options, option => !isUndefined(option) && !isNull(option)),
54
52
  };
55
53
 
54
+ options.repositoryUrl = options.repositoryUrl ? getGitAuthUrl(options.repositoryUrl) : options.repositoryUrl;
55
+
56
56
  debug('options values: %O', options);
57
57
 
58
58
  return {options, plugins: await plugins(options, pluginsPath, logger)};
@@ -0,0 +1,7 @@
1
+ const SemanticReleaseError = require('@semantic-release/error');
2
+ const ERROR_DEFINITIONS = require('./definitions/errors');
3
+
4
+ module.exports = (code, ctx = {}) => {
5
+ const {message, details} = ERROR_DEFINITIONS[code](ctx);
6
+ return new SemanticReleaseError(message, code, details);
7
+ };
package/lib/git.js CHANGED
@@ -120,7 +120,6 @@ async function push(origin, branch) {
120
120
  *
121
121
  * @param {String} origin The remote repository URL.
122
122
  * @param {String} tagName The tag name to delete.
123
- * @throws {SemanticReleaseError} if the remote tag exists and references a commit that is not the local head commit.
124
123
  */
125
124
  async function deleteTag(origin, tagName) {
126
125
  // Delete the local tag
@@ -1,34 +1,35 @@
1
- const {isArray, isObject, omit} = require('lodash');
1
+ const {isArray, isObject, omit, castArray, isUndefined} = require('lodash');
2
2
  const AggregateError = require('aggregate-error');
3
- const SemanticReleaseError = require('@semantic-release/error');
4
- const PLUGINS_DEFINITION = require('./definitions');
3
+ const getError = require('../get-error');
4
+ const PLUGINS_DEFINITIONS = require('../definitions/plugins');
5
5
  const pipeline = require('./pipeline');
6
6
  const normalize = require('./normalize');
7
7
 
8
8
  module.exports = (options, pluginsPath, logger) => {
9
9
  const errors = [];
10
- const plugins = Object.keys(PLUGINS_DEFINITION).reduce((plugins, pluginType) => {
11
- const {config, output, default: def} = PLUGINS_DEFINITION[pluginType];
10
+ const plugins = Object.keys(PLUGINS_DEFINITIONS).reduce((plugins, pluginType) => {
11
+ const {config, default: def} = PLUGINS_DEFINITIONS[pluginType];
12
12
  let pluginConfs;
13
- if (options[pluginType]) {
13
+
14
+ if (isUndefined(options[pluginType])) {
15
+ pluginConfs = def;
16
+ } else {
14
17
  // If an object is passed and the path is missing, set the default one for single plugins
15
18
  if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) {
16
19
  options[pluginType].path = def;
17
20
  }
18
21
  if (config && !config.validator(options[pluginType])) {
19
- errors.push(new SemanticReleaseError(config.message, 'EPLUGINCONF'));
22
+ errors.push(getError('EPLUGINCONF', {pluginType, pluginConf: options[pluginType]}));
20
23
  return plugins;
21
24
  }
22
25
  pluginConfs = options[pluginType];
23
- } else {
24
- pluginConfs = def;
25
26
  }
26
27
 
27
- const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITION));
28
+ const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS));
28
29
 
29
- plugins[pluginType] = isArray(pluginConfs)
30
- ? pipeline(pluginConfs.map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger, output)))
31
- : normalize(pluginType, pluginsPath, globalOpts, pluginConfs, logger, output);
30
+ plugins[pluginType] = pipeline(
31
+ castArray(pluginConfs).map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger))
32
+ );
32
33
 
33
34
  return plugins;
34
35
  }, {});
@@ -1,15 +1,18 @@
1
1
  const {dirname} = require('path');
2
- const {inspect} = require('util');
3
- const SemanticReleaseError = require('@semantic-release/error');
4
- const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash');
2
+ const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash');
5
3
  const resolveFrom = require('resolve-from');
4
+ const getError = require('../get-error');
5
+ const {extractErrors} = require('../utils');
6
+ const PLUGINS_DEFINITIONS = require('../definitions/plugins');
6
7
 
7
- module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger, validator) => {
8
+ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
8
9
  if (!pluginOpts) {
9
10
  return noop;
10
11
  }
11
12
 
12
13
  const {path, ...config} = isString(pluginOpts) || isFunction(pluginOpts) ? {path: pluginOpts} : pluginOpts;
14
+ const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
15
+
13
16
  if (!isFunction(pluginOpts)) {
14
17
  if (pluginsPath[path]) {
15
18
  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
28
31
  let func;
29
32
  if (isFunction(plugin)) {
30
33
  func = plugin.bind(null, cloneDeep({...globalOpts, ...config}));
31
- } else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
34
+ } else if (isPlainObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
32
35
  func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config}));
33
36
  } else {
34
- throw new SemanticReleaseError(
35
- `The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.`,
36
- 'EPLUGINCONF'
37
- );
37
+ throw getError('EPLUGIN', {pluginType, pluginName});
38
38
  }
39
39
 
40
- return async input => {
41
- const result = await func(cloneDeep(input));
42
-
43
- if (validator && !validator.validator(result)) {
44
- throw new Error(`${validator.message} Received: ${inspect(result)}`);
45
- }
46
- return result;
47
- };
40
+ return Object.defineProperty(
41
+ async input => {
42
+ const definition = PLUGINS_DEFINITIONS[pluginType];
43
+ try {
44
+ const result = await func(cloneDeep(input));
45
+ if (definition && definition.output && !definition.output.validator(result)) {
46
+ throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName});
47
+ }
48
+ return result;
49
+ } catch (err) {
50
+ extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
51
+ throw err;
52
+ }
53
+ },
54
+ 'pluginName',
55
+ {value: pluginName, writable: false, enumerable: true}
56
+ );
48
57
  };
@@ -1,33 +1,55 @@
1
1
  const {identity} = require('lodash');
2
- const pReflect = require('p-reflect');
3
2
  const pReduce = require('p-reduce');
4
3
  const AggregateError = require('aggregate-error');
4
+ const {extractErrors} = require('../utils');
5
5
 
6
- module.exports = steps => async (input, settleAll = false, getNextInput = identity) => {
6
+ /**
7
+ * 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.
8
+ *
9
+ * @typedef {Function} Pipeline
10
+ * @param {Any} input Argument to pass to the first step in the pipeline.
11
+ * @param {Object} options Pipeline options.
12
+ * @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.
13
+ * @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.
14
+ * @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.
15
+ *
16
+ * @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.
17
+ *
18
+ * @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.
19
+ */
20
+
21
+ /**
22
+ * Create a Pipeline with a list of Functions.
23
+ *
24
+ * @param {Array<Function>} steps The list of Function to execute.
25
+ * @return {Pipeline} A Function that execute the `steps` sequencially
26
+ */
27
+ module.exports = steps => async (input, {settleAll = false, getNextInput = identity, transform = identity} = {}) => {
7
28
  const results = [];
8
29
  const errors = [];
9
30
  await pReduce(
10
31
  steps,
11
- async (prevResult, nextStep) => {
32
+ async (lastResult, step) => {
12
33
  let result;
13
-
14
- // Call the next step with the input computed at the end of the previous iteration and save intermediary result
15
- if (settleAll) {
16
- const {isFulfilled, value, reason} = await pReflect(nextStep(prevResult));
17
- result = isFulfilled ? value : reason;
18
- (isFulfilled ? results : errors).push(result);
19
- } else {
20
- result = await nextStep(prevResult);
34
+ try {
35
+ // Call the step with the input computed at the end of the previous iteration and save intermediary result
36
+ result = await transform(await step(lastResult), step);
21
37
  results.push(result);
38
+ } catch (err) {
39
+ if (settleAll) {
40
+ errors.push(...extractErrors(err));
41
+ result = err;
42
+ } else {
43
+ throw err;
44
+ }
22
45
  }
23
-
24
- // Prepare input for next step, passing the result of the previous iteration and the current one
25
- return getNextInput(prevResult, result);
46
+ // Prepare input for the next step, passing the result of the last iteration (or initial parameter for the first iteration) and the current one
47
+ return getNextInput(lastResult, result);
26
48
  },
27
49
  input
28
50
  );
29
51
  if (errors.length > 0) {
30
- throw new AggregateError(errors);
52
+ throw errors.length === 1 ? errors[0] : new AggregateError(errors);
31
53
  }
32
- return results;
54
+ return results.length <= 1 ? results[0] : results;
33
55
  };
package/lib/utils.js ADDED
@@ -0,0 +1,7 @@
1
+ const {isFunction} = require('lodash');
2
+
3
+ function extractErrors(err) {
4
+ return err && isFunction(err[Symbol.iterator]) ? [...err] : [err];
5
+ }
6
+
7
+ module.exports = {extractErrors};
package/lib/verify.js CHANGED
@@ -1,44 +1,29 @@
1
1
  const {template} = require('lodash');
2
- const SemanticReleaseError = require('@semantic-release/error');
3
2
  const AggregateError = require('aggregate-error');
4
3
  const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
4
+ const getError = require('./get-error');
5
5
 
6
6
  module.exports = async (options, branch, logger) => {
7
7
  const errors = [];
8
8
 
9
9
  if (!await isGitRepo()) {
10
- logger.error('Semantic-release must run from a git repository.');
11
- return false;
12
- }
13
-
14
- if (!options.repositoryUrl) {
15
- errors.push(new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL'));
10
+ errors.push(getError('ENOGITREPO'));
11
+ } else if (!options.repositoryUrl) {
12
+ errors.push(getError('ENOREPOURL'));
16
13
  } else if (!await verifyAuth(options.repositoryUrl, options.branch)) {
17
- errors.push(
18
- new SemanticReleaseError(
19
- `The git credentials doesn't allow to push on the branch ${options.branch}.`,
20
- 'EGITNOPERMISSION'
21
- )
22
- );
14
+ errors.push(getError('EGITNOPERMISSION', {options}));
23
15
  }
24
16
 
25
17
  // Verify that compiling the `tagFormat` produce a valid Git tag
26
18
  if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) {
27
- errors.push(
28
- new SemanticReleaseError('The tagFormat template must compile to a valid Git tag format', 'EINVALIDTAGFORMAT')
29
- );
19
+ errors.push(getError('EINVALIDTAGFORMAT', {tagFormat: options.tagFormat}));
30
20
  }
31
21
 
32
22
  // Verify the `tagFormat` contains the variable `version` by compiling the `tagFormat` template
33
23
  // with a space as the `version` value and verify the result contains the space.
34
24
  // The space is used as it's an invalid tag character, so it's guaranteed to no be present in the `tagFormat`.
35
25
  if ((template(options.tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) {
36
- errors.push(
37
- new SemanticReleaseError(
38
- `The tagFormat template must contain the variable "\${version}" exactly once`,
39
- 'ETAGNOVERSION'
40
- )
41
- );
26
+ errors.push(getError('ETAGNOVERSION', {tagFormat: options.tagFormat}));
42
27
  }
43
28
 
44
29
  if (errors.length > 0) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "semantic-release",
3
3
  "description": "Automated semver compliant package publishing",
4
- "version": "13.2.0",
4
+ "version": "13.4.1",
5
5
  "author": "Stephan Bönnemann <stephan@boennemann.me> (http://boennemann.me)",
6
6
  "bin": {
7
7
  "semantic-release": "bin/semantic-release.js"
@@ -20,9 +20,9 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "@semantic-release/commit-analyzer": "^5.0.0",
23
- "@semantic-release/error": "^2.1.0",
24
- "@semantic-release/github": "^4.0.2",
25
- "@semantic-release/npm": "^3.0.0",
23
+ "@semantic-release/error": "^2.2.0",
24
+ "@semantic-release/github": "^4.1.0",
25
+ "@semantic-release/npm": "^3.1.0",
26
26
  "@semantic-release/release-notes-generator": "^6.0.0",
27
27
  "aggregate-error": "^1.0.0",
28
28
  "chalk": "^2.3.0",
@@ -40,10 +40,10 @@
40
40
  "marked-terminal": "^2.0.0",
41
41
  "p-locate": "^2.0.0",
42
42
  "p-reduce": "^1.0.0",
43
- "p-reflect": "^1.0.0",
44
43
  "read-pkg-up": "^3.0.0",
45
44
  "resolve-from": "^4.0.0",
46
- "semver": "^5.4.1"
45
+ "semver": "^5.4.1",
46
+ "yargs": "^11.0.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "ava": "^0.25.0",
@@ -1,55 +0,0 @@
1
- const {isString, isFunction, isArray} = require('lodash');
2
-
3
- const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
4
- const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
5
-
6
- module.exports = {
7
- verifyConditions: {
8
- default: ['@semantic-release/npm', '@semantic-release/github'],
9
- config: {
10
- validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
11
- message:
12
- '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.',
13
- },
14
- },
15
- analyzeCommits: {
16
- default: '@semantic-release/commit-analyzer',
17
- config: {
18
- validator: conf => Boolean(conf) && validatePluginConfig(conf),
19
- message:
20
- '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.',
21
- },
22
- output: {
23
- validator: output => !output || RELEASE_TYPE.includes(output),
24
- message: 'The "analyzeCommits" plugin output, if defined, must be a valid semver release type.',
25
- },
26
- },
27
- verifyRelease: {
28
- default: false,
29
- config: {
30
- validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
31
- message:
32
- '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.',
33
- },
34
- },
35
- generateNotes: {
36
- default: '@semantic-release/release-notes-generator',
37
- config: {
38
- validator: conf => !conf || validatePluginConfig(conf),
39
- message:
40
- '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.',
41
- },
42
- output: {
43
- validator: output => !output || isString(output),
44
- message: 'The "generateNotes" plugin output, if defined, must be a string.',
45
- },
46
- },
47
- publish: {
48
- default: ['@semantic-release/npm', '@semantic-release/github'],
49
- config: {
50
- validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
51
- message:
52
- '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.',
53
- },
54
- },
55
- };