Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd127ba98 | ||
|
|
c9bd5de075 | ||
|
|
51f6e5fd60 | ||
|
|
c6e95b0885 | ||
|
|
7fbd2721c4 | ||
|
|
fcb64e6fef | ||
|
|
d72cfc2546 | ||
|
|
49f5e704ba | ||
|
|
9b2f6bfed2 | ||
|
|
9360caf253 | ||
|
|
2f8d71644d | ||
|
|
5fd990acef |
+17
-17
@@ -10,7 +10,7 @@ As a contributor, here are the guidelines we would like you to follow:
|
||||
- [Coding rules](#coding-rules)
|
||||
- [Working with the code](#working-with-the-code)
|
||||
|
||||
We also recommend to read [How to Contribute to Open Source](https://opensource.guide/how-to-contribute).
|
||||
We also recommend that you read [How to Contribute to Open Source](https://opensource.guide/how-to-contribute).
|
||||
|
||||
## Code of conduct
|
||||
|
||||
@@ -20,33 +20,33 @@ Help us keep **semantic-release** open and inclusive. Please read and follow our
|
||||
|
||||
### Improve documentation
|
||||
|
||||
As a **semantic-release** user you are the perfect candidate to help us improve our documentation: typo corrections, clarifications, more examples, new [recipes](docs/recipes/README.md) etc. Take a look at the [documentation issues that need help](https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+user%3Asemantic-release+archived%3Afalse+label%3A%22help+wanted%22+label%3Adocs+).
|
||||
As a **semantic-release** user, you are the perfect candidate to help us improve our documentation: typo corrections, clarifications, more examples, new [recipes](docs/recipes/README.md), etc. Take a look at the [documentation issues that need help](https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+user%3Asemantic-release+archived%3Afalse+label%3A%22help+wanted%22+label%3Adocs+).
|
||||
|
||||
Please follow the [Documentation guideline](#documentation).
|
||||
Please follow the [Documentation guidelines](#documentation).
|
||||
|
||||
### Give feedback on issues
|
||||
|
||||
Some issues are created without information requested in the [Bug report guideline](#bug-report). Help making them easier to resolve by adding any relevant informations.
|
||||
Some issues are created without information requested in the [Bug report guideline](#bug-report). Help make them easier to resolve by adding any relevant information.
|
||||
|
||||
Issues with the [design label](https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Asemantic-release+archived%3Afalse+label%3Adesign) are meant to discuss the implementation of new features. Participating in the discussion is a good opportunity to get involved and influence the future direction of **semantic-release**.
|
||||
|
||||
### Fix bugs and implement features
|
||||
|
||||
Confirmed bug and ready to implement features are marked with the [help wanted label](https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+user%3Asemantic-release+archived%3Afalse+label%3A%22help+wanted%22). Post a comment on an issue to indicate you would like to work on it and to request help from the [@semantic-release/maintainers](https://github.com/orgs/semantic-release/teams/contributors) and the community.
|
||||
Confirmed bugs and ready-to-implement features are marked with the [help wanted label](https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+user%3Asemantic-release+archived%3Afalse+label%3A%22help+wanted%22). Post a comment on an issue to indicate you would like to work on it and to request help from the [@semantic-release/maintainers](https://github.com/orgs/semantic-release/teams/contributors) and the community.
|
||||
|
||||
## Using the issue tracker
|
||||
|
||||
The issue tracker is the channel for [bug reports](#bug-report), [features requests](#feature-request) and [submitting pull requests](#submitting-a-pull-request) only. Please use the [Support](docs/support/README.md) and [Get help](README.md#get-help) sections for support, troubleshooting and questions.
|
||||
|
||||
Before opening an Issue or a Pull Request, please use the [GitHub issue search](https://github.com/issues?utf8=%E2%9C%93&q=user%3Asemantic-release) to make the bug or feature request hasn't been already reported or fixed.
|
||||
Before opening an issue or a Pull Request, please use the [GitHub issue search](https://github.com/issues?utf8=%E2%9C%93&q=user%3Asemantic-release) to make sure the bug or feature request hasn't been already reported or fixed.
|
||||
|
||||
### Bug report
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report and fill the information requested in the [Bug report template](https://github.com/semantic-release/semantic-release/issues/new?template=bug-report.md).
|
||||
A good bug report shouldn't leave others needing to chase you for more information. Please try to be as detailed as possible in your report and fill the information requested in the [Bug report template](https://github.com/semantic-release/semantic-release/issues/new?template=bug-report.md).
|
||||
|
||||
### Feature request
|
||||
|
||||
Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible and fill the information requested in the [Feature request template](https://github.com/semantic-release/semantic-release/issues/new?template=feature-request.md).
|
||||
Feature requests are welcome, but take a moment to find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible and fill the information requested in the [Feature request template](https://github.com/semantic-release/semantic-release/issues/new?template=feature-request.md).
|
||||
|
||||
### New plugin request
|
||||
|
||||
@@ -54,9 +54,9 @@ Feature requests are welcome. But take a moment to find out whether your idea fi
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
Good pull requests whether patches, improvements or new features are a fantastic help. They should remain focused in scope and avoid containing unrelated commits.
|
||||
Good pull requests, whether patches, improvements, or new features, are a fantastic help. They should remain focused in scope and avoid containing unrelated commits.
|
||||
|
||||
**Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project.
|
||||
**Please ask first** before embarking on any significant pull requests (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project.
|
||||
|
||||
If you never created a pull request before, welcome 🎉 😄. [Here is a great tutorial](https://opensource.guide/how-to-contribute/#opening-a-pull-request) on how to send one :)
|
||||
|
||||
@@ -82,14 +82,14 @@ $ git push origin <topic-branch-name>
|
||||
6. [Open a Pull Request](https://help.github.com/articles/creating-a-pull-request/#creating-the-pull-request) with a clear title and description.
|
||||
|
||||
**Tips**:
|
||||
- For ambitious tasks, open a Pull Request as soon as possible with the `[WIP]` prefix i nthe title, in order to get feedback and help from the community.
|
||||
- [Allow semantic-release maintainers to make changes to you Pull Request branch](https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork) this way we can rebase it and make some minor changes if necessary. All changes we make will be done in new commit and we'll ask for your approval before merging them.
|
||||
- For ambitious tasks, open a Pull Request as soon as possible with the `[WIP]` prefix in the title, in order to get feedback and help from the community.
|
||||
- [Allow semantic-release maintainers to make changes to you Pull Request branch](https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This way, we can rebase it and make some minor changes if necessary. All changes we make will be done in new commit and we'll ask for your approval before merging them.
|
||||
|
||||
## Coding rules
|
||||
|
||||
### Source code
|
||||
|
||||
To ensure consistency and quality throughout the source code, all code modification must have:
|
||||
To ensure consistency and quality throughout the source code, all code modifications must have:
|
||||
- No [linting](#lint) errors
|
||||
- A [test](#tests) for every possible cases introduced by your code change
|
||||
- **100%** test coverage
|
||||
@@ -99,7 +99,7 @@ To ensure consistency and quality throughout the source code, all code modificat
|
||||
|
||||
### Documentation
|
||||
|
||||
To ensure consistency and quality all documentation modification must:
|
||||
To ensure consistency and quality, all documentation modifications must:
|
||||
- Refer to brand in [bold](https://help.github.com/articles/basic-writing-and-formatting-syntax/#styling-text) with proper capitalization, i.e. **GitHub**, **semantic-release**, **npm**
|
||||
- Prefer [tables](https://help.github.com/articles/organizing-information-with-tables) over [lists](https://help.github.com/articles/basic-writing-and-formatting-syntax/#lists) when listing key values, i.e. List of options with their description
|
||||
- Use [links](https://help.github.com/articles/basic-writing-and-formatting-syntax/#links) when, the first you are referring to:
|
||||
@@ -229,11 +229,11 @@ Before pushing your code changes make sure there is no linting errors with `npm
|
||||
|
||||
### Tests
|
||||
|
||||
Running the integration test require to install [Docker](https://docs.docker.com/engine/installation) on your machine.
|
||||
Running the integration test requires you to install [Docker](https://docs.docker.com/engine/installation) on your machine.
|
||||
|
||||
All the [semantic-release](https://github.com/semantic-release) repository use [AVA](https://github.com/avajs/ava) for writing and running tests.
|
||||
All the [semantic-release](https://github.com/semantic-release) repositories use [AVA](https://github.com/avajs/ava) for writing and running tests.
|
||||
|
||||
Before pushing your code changes make sure all **test pass** and the **coverage is 100%**:
|
||||
Before pushing your code changes make sure all **tests pass** and the **coverage is 100%**:
|
||||
|
||||
```bash
|
||||
$ npm run test
|
||||
|
||||
@@ -37,10 +37,11 @@ This removes the immediate connection between human emotions and version numbers
|
||||
- Fully automated release
|
||||
- Enforce [Semantic Versioning](https://semver.org) specification
|
||||
- New features and fixes are immediately available to users
|
||||
- Notify maintainers and users of new releases
|
||||
- Use formalized commit message convention to document changes in the codebase
|
||||
- Integrate with your [continuous integration workflow](docs/recipes/README.md#ci-configurations)
|
||||
- Avoid potential errors associated with manual releases
|
||||
- Support any [package managers and languages](docs/recipes/README.md#package-managers-and-languages) via [plugins](docs/usage/plugins.md)
|
||||
- Support any [package managers and languages](docs/recipes/README.md#package-managers-and-languages) via [plugins](docs/usage/plugins.md)
|
||||
- Simple and reusable configuration via [shareable configurations](docs/usage/shareable-configurations.md)
|
||||
|
||||
## 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
|
||||
|
||||
### Triggering a release
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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:
|
||||
- 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
|
||||
| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
|
||||
| Create Git tag | Create a Git tag corresponding the new release version |
|
||||
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
|
||||
| Notify | Notify of new releases or errors with the [success](docs/usage/plugins.md#success-plugin) and [fail](docs/usage/plugins.md#fail-plugin) plugins. |
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ const program = require('commander');
|
||||
const {pickBy, isUndefined} = require('lodash');
|
||||
|
||||
function list(values) {
|
||||
return values.split(',').map(value => value.trim());
|
||||
return values
|
||||
.split(',')
|
||||
.map(value => value.trim())
|
||||
.filter(value => value && value !== 'false');
|
||||
}
|
||||
|
||||
module.exports = async () => {
|
||||
@@ -26,6 +29,8 @@ module.exports = async () => {
|
||||
)
|
||||
.option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
|
||||
.option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list)
|
||||
.option('--success <paths>', 'Comma separated list of paths or packages name for the success plugin(s)', list)
|
||||
.option('--fail <paths>', 'Comma separated list of paths or packages name for the fail plugin(s)', list)
|
||||
.option(
|
||||
'--no-ci',
|
||||
'Skip Continuous Integration environment verifications, allowing to make releases from a local machine'
|
||||
@@ -48,7 +53,7 @@ module.exports = async () => {
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
const opts = program.opts();
|
||||
// Set the `noCi` options as commander.js sets the `ci` options instead (becasue args starts with `--no`)
|
||||
// Set the `noCi` options as commander.js sets the `ci` options instead (because args starts with `--no`)
|
||||
opts.noCi = opts.ci === false ? true : undefined;
|
||||
// Remove option with undefined values, as commander.js sets non defined options as `undefined`
|
||||
await require('.')(pickBy(opts, value => !isUndefined(value)));
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# semantic-release documentation
|
||||
|
||||
- [Usage](usage/README.md) - **semantic-release** installation and configuration
|
||||
- [Extending][extending/README.md]- Extending **semantic-release** with plugins and shareable configurations
|
||||
- [Extending](extending/README.md)- Extending **semantic-release** with plugins and shareable configurations
|
||||
- [Recipes](recipes/README.md) - Community written recipes for common **semantic-release** use-cases
|
||||
- [Developer Guide](developer-guide/README.md) - The essentials of writing a **semantic-release** plugin or shareable configurations
|
||||
- [Resources](resources.md) - Videos, articles and tutorials
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
- [@semantic-release/github](https://github.com/semantic-release/github)
|
||||
- [verifyConditions](https://github.com/semantic-release/github#verifyconditions): Verify the presence and the validity of the GitHub authentication and release configuration
|
||||
- [publish](https://github.com/semantic-release/github#publish): Publish a [GitHub release](https://help.github.com/articles/about-releases)
|
||||
- [success](https://github.com/semantic-release/github#success): Add a comment to GitHub issues and pull requests resolved in the release
|
||||
- [fail](https://github.com/semantic-release/github#fail): Open a GitHub issue when a release fails
|
||||
- [@semantic-release/npm](https://github.com/semantic-release/npm)
|
||||
- [verifyConditions](https://github.com/semantic-release/npm#verifyconditions): Verify the presence and the validity of the npm authentication and release configuration
|
||||
- [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry
|
||||
@@ -25,6 +27,8 @@
|
||||
- [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release
|
||||
- [verifyRelease](https://github.com/semantic-release/exec#verifyrelease): Execute a shell command to verifying a release that was determined before and is about to be published.
|
||||
- [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note
|
||||
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release.
|
||||
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release
|
||||
- [success](https://github.com/semantic-release/exec#success): Execute a shell command to notify of a new release
|
||||
- [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
|
||||
|
||||
## Community plugins
|
||||
|
||||
@@ -1 +1,65 @@
|
||||
# CircleCI 2.0 workflows
|
||||
# Using semantic-release with [CircleCI 2.0 workflows](https://circleci.com/docs/2.0/workflows)
|
||||
|
||||
## Environment variables
|
||||
|
||||
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)..
|
||||
|
||||
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).
|
||||
|
||||
## Multiple Node jobs configuration
|
||||
|
||||
### `.circleci/config.yml` configuration for multiple Node jobs
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
```yaml
|
||||
version: 2
|
||||
jobs:
|
||||
test_node_6:
|
||||
docker:
|
||||
- image: circleci/node:6
|
||||
steps:
|
||||
# Configure your test steps here (checkout, npm install, cache management, tests etc...)
|
||||
|
||||
test_node_8:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
# Configure your test steps here (checkout, npm install, cache management, tests etc...)
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
steps:
|
||||
- checkout
|
||||
- run: npm install
|
||||
# Run optional required steps before releasing
|
||||
# - run: npm run build-script
|
||||
- run: npx semantic-release
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
test_and_release:
|
||||
# Run the test jobs first, then the release only when all the test jobs are successful
|
||||
jobs:
|
||||
- test_node_6
|
||||
- test_node_8
|
||||
- release:
|
||||
requires:
|
||||
- test_node_6
|
||||
- test_node_8
|
||||
```
|
||||
|
||||
### `package.json` configuration for multiple Node jobs
|
||||
|
||||
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"semantic-release": "^12.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -8,11 +8,11 @@ The [Authentication](../usage/ci-configuration.md#authentication) environment va
|
||||
|
||||
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.
|
||||
|
||||
**Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--8).
|
||||
**Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
|
||||
|
||||
### `.gitlab-ci.yml` configuration for Node projects
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -25,12 +25,6 @@ stages:
|
||||
before_script:
|
||||
- npm install
|
||||
|
||||
node:4:
|
||||
image: node:4
|
||||
stage: test
|
||||
script:
|
||||
- npm test
|
||||
|
||||
node:6:
|
||||
image: node:6
|
||||
stage: test
|
||||
|
||||
@@ -1 +1,95 @@
|
||||
# Using semantic-release with [Travis CI build stages](https://docs.travis-ci.com/user/build-stages)
|
||||
|
||||
## Environment variables
|
||||
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
## Multiple Node jobs configuration
|
||||
|
||||
### `.travis.yml` configuration for multiple Node jobs
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
**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).
|
||||
|
||||
**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.
|
||||
|
||||
```yaml
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- 8
|
||||
- 6
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Define the release stage that runs semantic-release
|
||||
- stage: release
|
||||
node_js: lts/*
|
||||
# Advanced: optionally overwrite your default `script` step to skip the tests
|
||||
# script: skip
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script:
|
||||
- npx semantic-release
|
||||
```
|
||||
|
||||
### `package.json` configuration for multiple Node jobs
|
||||
|
||||
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"semantic-release": "^12.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Non-JavaScript projects configuration
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
### `.travis.yml` configuration for non-JavaScript projects
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
**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).
|
||||
|
||||
**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.
|
||||
|
||||
```yaml
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
- 1.7
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# Define the release stage that runs semantic-release
|
||||
- stage: release
|
||||
# Advanced: optionally overwrite your default `script` step to skip the tests
|
||||
# script:
|
||||
# - make
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script:
|
||||
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
|
||||
- nvm install lts/*
|
||||
- npx semantic-release
|
||||
```
|
||||
|
||||
+5
-22
@@ -10,7 +10,7 @@ Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with
|
||||
|
||||
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)).
|
||||
|
||||
**Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--8).
|
||||
**Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
|
||||
|
||||
### `.travis.yml` configuration for single Node job
|
||||
|
||||
@@ -25,10 +25,6 @@ language: node_js
|
||||
|
||||
node_js: 8
|
||||
|
||||
script:
|
||||
# Run tests
|
||||
- npm run test
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
@@ -56,9 +52,9 @@ For projects that require to be tested with multiple [Node versions](https://doc
|
||||
|
||||
### `.travis.yml` configuration for multiple Node jobs
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
@@ -70,15 +66,6 @@ language: node_js
|
||||
node_js:
|
||||
- 8
|
||||
- 6
|
||||
- 4
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
script:
|
||||
# Run tests
|
||||
- npm run test
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
@@ -110,7 +97,7 @@ This recipe cover the Travis specifics only. See [Non JavaScript projects recipe
|
||||
|
||||
### `.travis.yml` configuration for non-JavaScript projects
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -129,17 +116,13 @@ os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
script:
|
||||
# Run tests
|
||||
- go test -v ./...
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script:
|
||||
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
|
||||
- nvm install lts/*
|
||||
# Run semantic-release only on job, after all other are successful
|
||||
# Run semantic-release only on one job, after all other are successful
|
||||
- npx travis-deploy-once "npx semantic-release"
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ See [Artifactory - npm Registry](https://www.jfrog.com/confluence/display/RTF/Np
|
||||
|
||||
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.
|
||||
|
||||
### Can I exclude commits from the analysis?
|
||||
## Can I exclude commits from the analysis?
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ Default: `repository` property in `package.json` or [git origin url](https://git
|
||||
|
||||
CLI arguments: `-r`, `--repository-url`
|
||||
|
||||
The git repository URL
|
||||
The git repository URL.
|
||||
|
||||
Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)).
|
||||
|
||||
@@ -75,7 +75,7 @@ CLI arguments: `-t`, `--tag-format`
|
||||
|
||||
The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by **semantic-release** to identify releases. The tag name is generated with [Lodash template](https://lodash.com/docs#template) and will be compiled with the `version` variable.
|
||||
|
||||
**Note**: The `tagFormat` must contain the `version` variable and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
|
||||
**Note**: The `tagFormat` must contain the `version` variable exactly once and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
|
||||
|
||||
### dryRun
|
||||
|
||||
|
||||
@@ -34,6 +34,18 @@ Plugin responsible for publishing the release.
|
||||
|
||||
Default implementation: [npm](https://github.com/semantic-release/npm#publish) and [github](https://github.com/semantic-release/github#publish).
|
||||
|
||||
### success plugin
|
||||
|
||||
Plugin responsible for notifying of a new release.
|
||||
|
||||
Default implementation: [github](https://github.com/semantic-release/github#success).
|
||||
|
||||
### fail plugin
|
||||
|
||||
Plugin responsible for notifying of a failed release.
|
||||
|
||||
Default implementation: [github](https://github.com/semantic-release/github#fail).
|
||||
|
||||
## Configuration
|
||||
|
||||
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`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const {template} = require('lodash');
|
||||
const {template, isPlainObject, castArray} = require('lodash');
|
||||
const marked = require('marked');
|
||||
const TerminalRenderer = require('marked-terminal');
|
||||
const envCi = require('env-ci');
|
||||
@@ -9,13 +9,14 @@ const verify = require('./lib/verify');
|
||||
const getNextVersion = require('./lib/get-next-version');
|
||||
const getCommits = require('./lib/get-commits');
|
||||
const getLastRelease = require('./lib/get-last-release');
|
||||
const {extractErrors} = require('./lib/utils');
|
||||
const logger = require('./lib/logger');
|
||||
const {unshallow, gitHead: getGitHead, tag, push, deleteTag} = require('./lib/git');
|
||||
|
||||
async function run(opts) {
|
||||
marked.setOptions({renderer: new TerminalRenderer()});
|
||||
|
||||
async function run(options, plugins) {
|
||||
const {isCi, branch, isPr} = envCi();
|
||||
const config = await getConfig(opts, logger);
|
||||
const {plugins, options} = config;
|
||||
|
||||
if (!isCi && !options.dryRun && !options.noCi) {
|
||||
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
@@ -34,7 +35,7 @@ async function run(opts) {
|
||||
logger.log('Run automated release from branch %s', options.branch);
|
||||
|
||||
logger.log('Call plugin %s', 'verify-conditions');
|
||||
await plugins.verifyConditions({options, logger}, true);
|
||||
await plugins.verifyConditions({options, logger}, {settleAll: true});
|
||||
|
||||
// Unshallow the repo in order to get all the tags
|
||||
await unshallow();
|
||||
@@ -57,14 +58,13 @@ async function run(opts) {
|
||||
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})};
|
||||
|
||||
logger.log('Call plugin %s', 'verify-release');
|
||||
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, true);
|
||||
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, {settleAll: true});
|
||||
|
||||
const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
|
||||
|
||||
if (options.dryRun) {
|
||||
logger.log('Call plugin %s', 'generate-notes');
|
||||
const notes = await plugins.generateNotes(generateNotesParam);
|
||||
marked.setOptions({renderer: new TerminalRenderer()});
|
||||
logger.log('Release note for version %s:\n', nextRelease.version);
|
||||
process.stdout.write(`${marked(notes)}\n`);
|
||||
} else {
|
||||
@@ -77,45 +77,85 @@ async function run(opts) {
|
||||
await push(options.repositoryUrl, branch);
|
||||
|
||||
logger.log('Call plugin %s', 'publish');
|
||||
await plugins.publish({options, logger, lastRelease, commits, nextRelease}, false, async prevInput => {
|
||||
const newGitHead = await getGitHead();
|
||||
// If previous publish plugin has created a commit (gitHead changed)
|
||||
if (prevInput.nextRelease.gitHead !== newGitHead) {
|
||||
// Delete the previously created tag
|
||||
await deleteTag(options.repositoryUrl, nextRelease.gitTag);
|
||||
// Recreate the tag, referencing the new gitHead
|
||||
logger.log('Create tag %s', nextRelease.gitTag);
|
||||
await tag(nextRelease.gitTag);
|
||||
await push(options.repositoryUrl, branch);
|
||||
const releases = await plugins.publish(
|
||||
{options, logger, lastRelease, commits, nextRelease},
|
||||
{
|
||||
getNextInput: async lastResult => {
|
||||
const newGitHead = await getGitHead();
|
||||
// If previous publish plugin has created a commit (gitHead changed)
|
||||
if (lastResult.nextRelease.gitHead !== newGitHead) {
|
||||
// Delete the previously created tag
|
||||
await deleteTag(options.repositoryUrl, nextRelease.gitTag);
|
||||
// Recreate the tag, referencing the new gitHead
|
||||
logger.log('Create tag %s', nextRelease.gitTag);
|
||||
await tag(nextRelease.gitTag);
|
||||
await push(options.repositoryUrl, branch);
|
||||
|
||||
nextRelease.gitHead = newGitHead;
|
||||
// Regenerate the release notes
|
||||
logger.log('Call plugin %s', 'generateNotes');
|
||||
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
|
||||
nextRelease.gitHead = newGitHead;
|
||||
// Regenerate the release notes
|
||||
logger.log('Call plugin %s', 'generateNotes');
|
||||
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
|
||||
}
|
||||
// Call the next publish plugin with the updated `nextRelease`
|
||||
return {options, logger, lastRelease, commits, nextRelease};
|
||||
},
|
||||
// Add nextRelease and plugin properties to published release
|
||||
transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step}),
|
||||
}
|
||||
// Call the next publish plugin with the updated `nextRelease`
|
||||
return {options, logger, lastRelease, commits, nextRelease};
|
||||
});
|
||||
);
|
||||
|
||||
await plugins.success(
|
||||
{options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
|
||||
{settleAll: true}
|
||||
);
|
||||
|
||||
logger.log('Published release: %s', nextRelease.version);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function logErrors(err) {
|
||||
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
|
||||
for (const error of errors) {
|
||||
if (error.semanticRelease) {
|
||||
logger.log(`%s ${error.message}`, error.code);
|
||||
if (error.details) {
|
||||
process.stdout.write(`${marked(error.details)}\n`);
|
||||
}
|
||||
} else {
|
||||
logger.error('An error occurred while running semantic-release: %O', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function callFail(plugins, options, error) {
|
||||
const errors = extractErrors(error).filter(error => error.semanticRelease);
|
||||
if (errors.length > 0) {
|
||||
try {
|
||||
await plugins.fail({options, logger, errors}, {settleAll: true});
|
||||
} catch (err) {
|
||||
logErrors(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async opts => {
|
||||
const unhook = hookStd({silent: false}, hideSensitive);
|
||||
try {
|
||||
const result = await run(opts);
|
||||
unhook();
|
||||
return result;
|
||||
} catch (err) {
|
||||
const errors = err.name === 'AggregateError' ? Array.from(err).sort(error => !error.semanticRelease) : [err];
|
||||
for (const error of errors) {
|
||||
if (error.semanticRelease) {
|
||||
logger.log(`%s ${error.message}`, error.code);
|
||||
} else {
|
||||
logger.error('An error occurred while running semantic-release: %O', error);
|
||||
const config = await getConfig(opts, logger);
|
||||
const {plugins, options} = config;
|
||||
try {
|
||||
const result = await run(options, plugins);
|
||||
unhook();
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (!options.dryRun) {
|
||||
await callFail(plugins, options, err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
logErrors(err);
|
||||
unhook();
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
const url = require('url');
|
||||
const {inspect} = require('util');
|
||||
const {toLower, isString} = require('lodash');
|
||||
const pkg = require('../../package.json');
|
||||
const RELEASE_TYPE = require('./release-types');
|
||||
|
||||
const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
|
||||
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
|
||||
const linkify = file => `${homepage}/blob/caribou/${file}`;
|
||||
|
||||
module.exports = {
|
||||
ENOGITREPO: () => ({
|
||||
message: 'Not running from a git repository.',
|
||||
details: `The \`semantic-release\` command must be executed from a Git repository.
|
||||
|
||||
The current working directory is \`${process.cwd()}\`.
|
||||
|
||||
Please verify your CI configuration to make sure the \`semantic-release\` command is executed from the root of the cloned repository.`,
|
||||
}),
|
||||
ENOREPOURL: () => ({
|
||||
message: 'The `repositoryUrl` option is required.',
|
||||
details: `The [repositoryUrl option](${linkify(
|
||||
'docs/usage/configuration.md#repositoryurl'
|
||||
)}) cannot be determined from the semantic-release configuration, the \`package.json\` nor the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes).
|
||||
|
||||
Please make sure to add the \`repositoryUrl\` to the [semantic-release configuration] (${linkify(
|
||||
'docs/usage/configuration.md'
|
||||
)}).`,
|
||||
}),
|
||||
EGITNOPERMISSION: ({options}) => ({
|
||||
message: 'The push permission to the Git repository is required.',
|
||||
details: `**semantic-release** cannot push the version tag to the branch \`${
|
||||
options.branch
|
||||
}\` on remote Git repository.
|
||||
|
||||
Please refer to the [authentication configuration documentation](${linkify(
|
||||
'docs/usage/ci-configuration.md#authentication'
|
||||
)}) to configure the Git credentials on your CI environment.`,
|
||||
}),
|
||||
EINVALIDTAGFORMAT: ({tagFormat}) => ({
|
||||
message: 'Invalid `tagFormat` option.',
|
||||
details: `The [tagFormat](${linkify(
|
||||
'docs/usage/configuration.md#tagformat'
|
||||
)}) must compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
|
||||
|
||||
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
|
||||
}),
|
||||
ETAGNOVERSION: ({tagFormat}) => ({
|
||||
message: 'Invalid `tagFormat` option.',
|
||||
details: `The [tagFormat](${linkify(
|
||||
'docs/usage/configuration.md#tagformat'
|
||||
)}) option must contain the variable \`version\` exactly once.
|
||||
|
||||
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
|
||||
}),
|
||||
EPLUGINCONF: ({pluginType, pluginConf}) => ({
|
||||
message: `The \`${pluginType}\` plugin configuration is invalid.`,
|
||||
details: `The [${pluginType} plugin configuration](${linkify(
|
||||
`docs/usage/plugins.md#${toLower(pluginType)}-plugin`
|
||||
)}) if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a \`path\` property.
|
||||
|
||||
Your configuration for the \`${pluginType}\` plugin is \`${stringify(pluginConf)}\`.`,
|
||||
}),
|
||||
EPLUGIN: ({pluginName, pluginType}) => ({
|
||||
message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`,
|
||||
details: `A valid \`${pluginType}\` **semantic-release** plugin must be a function or an object with a function in the property \`${pluginType}\`.
|
||||
|
||||
The plugin \`${pluginName}\` doesn't have the property \`${pluginType}\` and cannot be used for the \`${pluginType}\` step.
|
||||
|
||||
Please refer to the \`${pluginName}\` and [semantic-release plugins configuration](${linkify(
|
||||
'docs/usage/plugins.md'
|
||||
)}) documentation for more details.`,
|
||||
}),
|
||||
EANALYZEOUTPUT: ({result, pluginName}) => ({
|
||||
message: 'The `analyzeCommits` plugin returned an invalid value. It must return a valid semver release type.',
|
||||
details: `The \`analyzeCommits\` plugin must return a valid [semver](https://semver.org) release type. The valid values are: ${RELEASE_TYPE.map(
|
||||
type => `\`${type}\``
|
||||
).join(', ')}.
|
||||
|
||||
The \`analyzeCommits\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
|
||||
|
||||
We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
|
||||
- The **semantic-release** version: \`${pkg.version}\`
|
||||
- The **semantic-release** logs from your CI job
|
||||
- The value returned by the plugin: \`${stringify(result)}\`
|
||||
- A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify(
|
||||
'docs/developer-guide/plugin.md'
|
||||
)})`,
|
||||
}),
|
||||
ERELEASENOTESOUTPUT: ({result, pluginName}) => ({
|
||||
message: 'The `generateNotes` plugin returned an invalid value. It must return a `String`.',
|
||||
details: `The \`generateNotes\` plugin must return a \`String\`.
|
||||
|
||||
The \`generateNotes\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
|
||||
|
||||
We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
|
||||
- The **semantic-release** version: \`${pkg.version}\`
|
||||
- The **semantic-release** logs from your CI job
|
||||
- The value returned by the plugin: \`${stringify(result)}\`
|
||||
- A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify(
|
||||
'docs/developer-guide/plugin.md'
|
||||
)})`,
|
||||
}),
|
||||
EPUBLISHOUTPUT: ({result, pluginName}) => ({
|
||||
message: 'A `publish` plugin returned an invalid value. It must return an `Object`.',
|
||||
details: `The \`publish\` plugins must return an \`Object\`.
|
||||
|
||||
The \`publish\` function of the \`${pluginName}\` returned \`${stringify(result)}\` instead.
|
||||
|
||||
We recommend to report the issue to the \`${pluginName}\` authors, providing the following informations:
|
||||
- The **semantic-release** version: \`${pkg.version}\`
|
||||
- The **semantic-release** logs from your CI job
|
||||
- The value returned by the plugin: \`${stringify(result)}\`
|
||||
- A link to the **semantic-release** plugin developer guide: [${linkify('docs/developer-guide/plugin.md')}](${linkify(
|
||||
'docs/developer-guide/plugin.md'
|
||||
)})`,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
|
||||
const RELEASE_TYPE = require('./release-types');
|
||||
|
||||
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
|
||||
|
||||
module.exports = {
|
||||
verifyConditions: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
config: {
|
||||
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
},
|
||||
},
|
||||
analyzeCommits: {
|
||||
default: '@semantic-release/commit-analyzer',
|
||||
config: {
|
||||
validator: conf => Boolean(conf) && validatePluginConfig(conf),
|
||||
},
|
||||
output: {
|
||||
validator: output => !output || RELEASE_TYPE.includes(output),
|
||||
error: 'EANALYZEOUTPUT',
|
||||
},
|
||||
},
|
||||
verifyRelease: {
|
||||
default: false,
|
||||
config: {
|
||||
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
},
|
||||
},
|
||||
generateNotes: {
|
||||
default: '@semantic-release/release-notes-generator',
|
||||
config: {
|
||||
validator: conf => !conf || validatePluginConfig(conf),
|
||||
},
|
||||
output: {
|
||||
validator: output => !output || isString(output),
|
||||
error: 'ERELEASENOTESOUTPUT',
|
||||
},
|
||||
},
|
||||
publish: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
config: {
|
||||
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
},
|
||||
output: {
|
||||
validator: output => !output || isPlainObject(output),
|
||||
error: 'EPUBLISHOUTPUT',
|
||||
},
|
||||
},
|
||||
success: {
|
||||
default: ['@semantic-release/github'],
|
||||
config: {
|
||||
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
},
|
||||
},
|
||||
fail: {
|
||||
default: ['@semantic-release/github'],
|
||||
config: {
|
||||
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
|
||||
+2
-2
@@ -4,7 +4,7 @@ const cosmiconfig = require('cosmiconfig');
|
||||
const resolveFrom = require('resolve-from');
|
||||
const debug = require('debug')('semantic-release:config');
|
||||
const {repoUrl} = require('./git');
|
||||
const PLUGINS_DEFINITION = require('./plugins/definitions');
|
||||
const PLUGINS_DEFINITIONS = require('./definitions/plugins');
|
||||
const plugins = require('./plugins');
|
||||
const getGitAuthUrl = require('./get-git-auth-url');
|
||||
|
||||
@@ -25,7 +25,7 @@ module.exports = async (opts, logger) => {
|
||||
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
|
||||
// so those plugin will be loaded relatively to the config file
|
||||
Object.keys(extendsOpts).reduce((pluginsPath, option) => {
|
||||
if (PLUGINS_DEFINITION[option]) {
|
||||
if (PLUGINS_DEFINITIONS[option]) {
|
||||
castArray(extendsOpts[option])
|
||||
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
|
||||
.map(plugin => (isString(plugin) ? plugin : plugin.path))
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const ERROR_DEFINITIONS = require('./definitions/errors');
|
||||
|
||||
module.exports = (code, ctx = {}) => {
|
||||
const {message, details} = ERROR_DEFINITIONS[code](ctx);
|
||||
return new SemanticReleaseError(message, code, details);
|
||||
};
|
||||
@@ -120,7 +120,6 @@ async function push(origin, branch) {
|
||||
*
|
||||
* @param {String} origin The remote repository URL.
|
||||
* @param {String} tagName The tag name to delete.
|
||||
* @throws {SemanticReleaseError} if the remote tag exists and references a commit that is not the local head commit.
|
||||
*/
|
||||
async function deleteTag(origin, tagName) {
|
||||
// Delete the local tag
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
const {isString, isFunction, isArray} = require('lodash');
|
||||
|
||||
const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
|
||||
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
|
||||
|
||||
module.exports = {
|
||||
verifyConditions: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
config: {
|
||||
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
message:
|
||||
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.',
|
||||
},
|
||||
},
|
||||
analyzeCommits: {
|
||||
default: '@semantic-release/commit-analyzer',
|
||||
config: {
|
||||
validator: conf => Boolean(conf) && validatePluginConfig(conf),
|
||||
message:
|
||||
'The "analyzeCommits" plugin is mandatory, and must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
|
||||
},
|
||||
output: {
|
||||
validator: output => !output || RELEASE_TYPE.includes(output),
|
||||
message: 'The "analyzeCommits" plugin output, if defined, must be a valid semver release type.',
|
||||
},
|
||||
},
|
||||
verifyRelease: {
|
||||
default: false,
|
||||
config: {
|
||||
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
message:
|
||||
'The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.',
|
||||
},
|
||||
},
|
||||
generateNotes: {
|
||||
default: '@semantic-release/release-notes-generator',
|
||||
config: {
|
||||
validator: conf => !conf || validatePluginConfig(conf),
|
||||
message:
|
||||
'The "generateNotes" plugin, if defined, must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
|
||||
},
|
||||
output: {
|
||||
validator: output => !output || isString(output),
|
||||
message: 'The "generateNotes" plugin output, if defined, must be a string.',
|
||||
},
|
||||
},
|
||||
publish: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
config: {
|
||||
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
message:
|
||||
'The "publish" plugin is mandatory, and must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.',
|
||||
},
|
||||
},
|
||||
};
|
||||
+14
-13
@@ -1,34 +1,35 @@
|
||||
const {isArray, isObject, omit} = require('lodash');
|
||||
const {isArray, isObject, omit, castArray, isUndefined} = require('lodash');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const PLUGINS_DEFINITION = require('./definitions');
|
||||
const getError = require('../get-error');
|
||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||
const pipeline = require('./pipeline');
|
||||
const normalize = require('./normalize');
|
||||
|
||||
module.exports = (options, pluginsPath, logger) => {
|
||||
const errors = [];
|
||||
const plugins = Object.keys(PLUGINS_DEFINITION).reduce((plugins, pluginType) => {
|
||||
const {config, output, default: def} = PLUGINS_DEFINITION[pluginType];
|
||||
const plugins = Object.keys(PLUGINS_DEFINITIONS).reduce((plugins, pluginType) => {
|
||||
const {config, default: def} = PLUGINS_DEFINITIONS[pluginType];
|
||||
let pluginConfs;
|
||||
if (options[pluginType]) {
|
||||
|
||||
if (isUndefined(options[pluginType])) {
|
||||
pluginConfs = def;
|
||||
} else {
|
||||
// If an object is passed and the path is missing, set the default one for single plugins
|
||||
if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) {
|
||||
options[pluginType].path = def;
|
||||
}
|
||||
if (config && !config.validator(options[pluginType])) {
|
||||
errors.push(new SemanticReleaseError(config.message, 'EPLUGINCONF'));
|
||||
errors.push(getError('EPLUGINCONF', {pluginType, pluginConf: options[pluginType]}));
|
||||
return plugins;
|
||||
}
|
||||
pluginConfs = options[pluginType];
|
||||
} else {
|
||||
pluginConfs = def;
|
||||
}
|
||||
|
||||
const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITION));
|
||||
const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS));
|
||||
|
||||
plugins[pluginType] = isArray(pluginConfs)
|
||||
? pipeline(pluginConfs.map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger, output)))
|
||||
: normalize(pluginType, pluginsPath, globalOpts, pluginConfs, logger, output);
|
||||
plugins[pluginType] = pipeline(
|
||||
castArray(pluginConfs).map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger))
|
||||
);
|
||||
|
||||
return plugins;
|
||||
}, {});
|
||||
|
||||
+26
-17
@@ -1,15 +1,18 @@
|
||||
const {dirname} = require('path');
|
||||
const {inspect} = require('util');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash');
|
||||
const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash');
|
||||
const resolveFrom = require('resolve-from');
|
||||
const getError = require('../get-error');
|
||||
const {extractErrors} = require('../utils');
|
||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||
|
||||
module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger, validator) => {
|
||||
module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
|
||||
if (!pluginOpts) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const {path, ...config} = isString(pluginOpts) || isFunction(pluginOpts) ? {path: pluginOpts} : pluginOpts;
|
||||
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
|
||||
|
||||
if (!isFunction(pluginOpts)) {
|
||||
if (pluginsPath[path]) {
|
||||
logger.log('Load plugin %s from %s in shareable config %s', pluginType, path, pluginsPath[path]);
|
||||
@@ -28,21 +31,27 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger, valid
|
||||
let func;
|
||||
if (isFunction(plugin)) {
|
||||
func = plugin.bind(null, cloneDeep({...globalOpts, ...config}));
|
||||
} else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
|
||||
} else if (isPlainObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
|
||||
func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config}));
|
||||
} else {
|
||||
throw new SemanticReleaseError(
|
||||
`The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.`,
|
||||
'EPLUGINCONF'
|
||||
);
|
||||
throw getError('EPLUGIN', {pluginType, pluginName});
|
||||
}
|
||||
|
||||
return async input => {
|
||||
const result = await func(cloneDeep(input));
|
||||
|
||||
if (validator && !validator.validator(result)) {
|
||||
throw new Error(`${validator.message} Received: ${inspect(result)}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return Object.defineProperty(
|
||||
async input => {
|
||||
const definition = PLUGINS_DEFINITIONS[pluginType];
|
||||
try {
|
||||
const result = await func(cloneDeep(input));
|
||||
if (definition && definition.output && !definition.output.validator(result)) {
|
||||
throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName});
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
'pluginName',
|
||||
{value: pluginName, writable: false, enumerable: true}
|
||||
);
|
||||
};
|
||||
|
||||
+38
-16
@@ -1,33 +1,55 @@
|
||||
const {identity} = require('lodash');
|
||||
const pReflect = require('p-reflect');
|
||||
const pReduce = require('p-reduce');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const {extractErrors} = require('../utils');
|
||||
|
||||
module.exports = steps => async (input, settleAll = false, getNextInput = identity) => {
|
||||
/**
|
||||
* A Function that execute a list of function sequencially. If at least one Function ins the pipeline throw an Error or rejects, the pipeline function rejects as well.
|
||||
*
|
||||
* @typedef {Function} Pipeline
|
||||
* @param {Any} input Argument to pass to the first step in the pipeline.
|
||||
* @param {Object} options Pipeline options.
|
||||
* @param {Boolean} [options.settleAll=false] If `true` all the steps in the pipeline are executed, even if one rejects, if `false` the execution stops after a steps rejects.
|
||||
* @param {Function} [options.getNextInput=identity] Function called after each step is executed, with the last and current step results; the returned value will be used as the argument of the next step.
|
||||
* @param {Function} [options.transform=identity] Function called after each step is executed, with the current step result and the step function; the returned value will be saved in the pipeline results.
|
||||
*
|
||||
* @return {Array<*>|*} An Array with the result of each step in the pipeline; if there is only 1 step in the pipeline, the result of this step is returned directly.
|
||||
*
|
||||
* @throws {AggregateError|Error} An AggregateError with the errors of each step in the pipeline that rejected; if there is only 1 step in the pipeline, the error of this step is thrown directly.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a Pipeline with a list of Functions.
|
||||
*
|
||||
* @param {Array<Function>} steps The list of Function to execute.
|
||||
* @return {Pipeline} A Function that execute the `steps` sequencially
|
||||
*/
|
||||
module.exports = steps => async (input, {settleAll = false, getNextInput = identity, transform = identity} = {}) => {
|
||||
const results = [];
|
||||
const errors = [];
|
||||
await pReduce(
|
||||
steps,
|
||||
async (prevResult, nextStep) => {
|
||||
async (lastResult, step) => {
|
||||
let result;
|
||||
|
||||
// Call the next step with the input computed at the end of the previous iteration and save intermediary result
|
||||
if (settleAll) {
|
||||
const {isFulfilled, value, reason} = await pReflect(nextStep(prevResult));
|
||||
result = isFulfilled ? value : reason;
|
||||
(isFulfilled ? results : errors).push(result);
|
||||
} else {
|
||||
result = await nextStep(prevResult);
|
||||
try {
|
||||
// Call the step with the input computed at the end of the previous iteration and save intermediary result
|
||||
result = await transform(await step(lastResult), step);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
if (settleAll) {
|
||||
errors.push(...extractErrors(err));
|
||||
result = err;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare input for next step, passing the result of the previous iteration and the current one
|
||||
return getNextInput(prevResult, result);
|
||||
// Prepare input for the next step, passing the result of the last iteration (or initial parameter for the first iteration) and the current one
|
||||
return getNextInput(lastResult, result);
|
||||
},
|
||||
input
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
throw errors.length === 1 ? errors[0] : new AggregateError(errors);
|
||||
}
|
||||
return results;
|
||||
return results.length <= 1 ? results[0] : results;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const {isFunction} = require('lodash');
|
||||
|
||||
function extractErrors(err) {
|
||||
return err && isFunction(err[Symbol.iterator]) ? [...err] : [err];
|
||||
}
|
||||
|
||||
module.exports = {extractErrors};
|
||||
+7
-22
@@ -1,44 +1,29 @@
|
||||
const {template} = require('lodash');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
|
||||
const getError = require('./get-error');
|
||||
|
||||
module.exports = async (options, branch, logger) => {
|
||||
const errors = [];
|
||||
|
||||
if (!await isGitRepo()) {
|
||||
logger.error('Semantic-release must run from a git repository.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.repositoryUrl) {
|
||||
errors.push(new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL'));
|
||||
errors.push(getError('ENOGITREPO'));
|
||||
} else if (!options.repositoryUrl) {
|
||||
errors.push(getError('ENOREPOURL'));
|
||||
} else if (!await verifyAuth(options.repositoryUrl, options.branch)) {
|
||||
errors.push(
|
||||
new SemanticReleaseError(
|
||||
`The git credentials doesn't allow to push on the branch ${options.branch}.`,
|
||||
'EGITNOPERMISSION'
|
||||
)
|
||||
);
|
||||
errors.push(getError('EGITNOPERMISSION', {options}));
|
||||
}
|
||||
|
||||
// Verify that compiling the `tagFormat` produce a valid Git tag
|
||||
if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) {
|
||||
errors.push(
|
||||
new SemanticReleaseError('The tagFormat template must compile to a valid Git tag format', 'EINVALIDTAGFORMAT')
|
||||
);
|
||||
errors.push(getError('EINVALIDTAGFORMAT', {tagFormat: options.tagFormat}));
|
||||
}
|
||||
|
||||
// Verify the `tagFormat` contains the variable `version` by compiling the `tagFormat` template
|
||||
// with a space as the `version` value and verify the result contains the space.
|
||||
// The space is used as it's an invalid tag character, so it's guaranteed to no be present in the `tagFormat`.
|
||||
if ((template(options.tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) {
|
||||
errors.push(
|
||||
new SemanticReleaseError(
|
||||
`The tagFormat template must contain the variable "\${version}" exactly once`,
|
||||
'ETAGNOVERSION'
|
||||
)
|
||||
);
|
||||
errors.push(getError('ETAGNOVERSION', {tagFormat: options.tagFormat}));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
+3
-4
@@ -20,9 +20,9 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@semantic-release/commit-analyzer": "^5.0.0",
|
||||
"@semantic-release/error": "^2.1.0",
|
||||
"@semantic-release/github": "^4.0.2",
|
||||
"@semantic-release/npm": "^3.0.0",
|
||||
"@semantic-release/error": "^2.2.0",
|
||||
"@semantic-release/github": "^4.1.0",
|
||||
"@semantic-release/npm": "^3.1.0",
|
||||
"@semantic-release/release-notes-generator": "^6.0.0",
|
||||
"aggregate-error": "^1.0.0",
|
||||
"chalk": "^2.3.0",
|
||||
@@ -40,7 +40,6 @@
|
||||
"marked-terminal": "^2.0.0",
|
||||
"p-locate": "^2.0.0",
|
||||
"p-reduce": "^1.0.0",
|
||||
"p-reflect": "^1.0.0",
|
||||
"read-pkg-up": "^3.0.0",
|
||||
"resolve-from": "^4.0.0",
|
||||
"semver": "^5.4.1"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import test from 'ava';
|
||||
import plugins from '../../lib/definitions/plugins';
|
||||
import errors from '../../lib/definitions/errors';
|
||||
|
||||
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyConditions.config.validator({}));
|
||||
t.false(plugins.verifyConditions.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.verifyConditions.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyConditions.config.validator());
|
||||
t.true(plugins.verifyConditions.config.validator('plugin-path.js'));
|
||||
t.true(plugins.verifyConditions.config.validator(() => {}));
|
||||
t.true(plugins.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
|
||||
t.false(plugins.analyzeCommits.config.validator({}));
|
||||
t.false(plugins.analyzeCommits.config.validator({path: null}));
|
||||
t.false(plugins.analyzeCommits.config.validator([]));
|
||||
t.false(plugins.analyzeCommits.config.validator());
|
||||
|
||||
t.true(plugins.analyzeCommits.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.analyzeCommits.config.validator('plugin-path.js'));
|
||||
t.true(plugins.analyzeCommits.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyRelease.config.validator({}));
|
||||
t.false(plugins.verifyRelease.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyRelease.config.validator());
|
||||
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
|
||||
t.true(plugins.verifyRelease.config.validator(() => {}));
|
||||
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
|
||||
t.false(plugins.generateNotes.config.validator({}));
|
||||
t.false(plugins.generateNotes.config.validator({path: null}));
|
||||
t.false(plugins.generateNotes.config.validator([]));
|
||||
|
||||
t.true(plugins.generateNotes.config.validator());
|
||||
t.true(plugins.generateNotes.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.generateNotes.config.validator('plugin-path.js'));
|
||||
t.true(plugins.generateNotes.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.publish.config.validator({}));
|
||||
t.false(plugins.publish.config.validator({path: null}));
|
||||
t.false(plugins.publish.config.validator());
|
||||
|
||||
t.true(plugins.publish.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.publish.config.validator('plugin-path.js'));
|
||||
t.true(plugins.publish.config.validator(() => {}));
|
||||
t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.success.config.validator({}));
|
||||
t.false(plugins.success.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.success.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.success.config.validator());
|
||||
t.true(plugins.success.config.validator('plugin-path.js'));
|
||||
t.true(plugins.success.config.validator(() => {}));
|
||||
t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.fail.config.validator({}));
|
||||
t.false(plugins.fail.config.validator({path: null}));
|
||||
|
||||
t.true(plugins.fail.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.fail.config.validator());
|
||||
t.true(plugins.fail.config.validator('plugin-path.js'));
|
||||
t.true(plugins.fail.config.validator(() => {}));
|
||||
t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||
t.false(plugins.analyzeCommits.output.validator('invalid'));
|
||||
t.false(plugins.analyzeCommits.output.validator(1));
|
||||
t.false(plugins.analyzeCommits.output.validator({}));
|
||||
|
||||
t.true(plugins.analyzeCommits.output.validator());
|
||||
t.true(plugins.analyzeCommits.output.validator(null));
|
||||
t.true(plugins.analyzeCommits.output.validator('major'));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin output, if defined, must be a string', t => {
|
||||
t.false(plugins.generateNotes.output.validator(1));
|
||||
t.false(plugins.generateNotes.output.validator({}));
|
||||
|
||||
t.true(plugins.generateNotes.output.validator());
|
||||
t.true(plugins.generateNotes.output.validator(null));
|
||||
t.true(plugins.generateNotes.output.validator(''));
|
||||
t.true(plugins.generateNotes.output.validator('string'));
|
||||
});
|
||||
|
||||
test('The "publish" plugin output, if defined, must be an object', t => {
|
||||
t.false(plugins.publish.output.validator(1));
|
||||
t.false(plugins.publish.output.validator('string'));
|
||||
|
||||
t.true(plugins.publish.output.validator({}));
|
||||
t.true(plugins.publish.output.validator());
|
||||
t.true(plugins.publish.output.validator(null));
|
||||
t.true(plugins.publish.output.validator(''));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin output definition return an existing error code', t => {
|
||||
t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin output definition return an existing error code', t => {
|
||||
t.true(Object.keys(errors).includes(plugins.generateNotes.output.error));
|
||||
});
|
||||
|
||||
test('The "publish" plugin output definition return an existing error code', t => {
|
||||
t.true(Object.keys(errors).includes(plugins.publish.output.error));
|
||||
});
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
const AggregateError = require('aggregate-error');
|
||||
|
||||
module.exports = () => {
|
||||
throw new AggregateError([new Error('a'), new Error('b')]);
|
||||
};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
module.exports = (pluginConfig, options) => options;
|
||||
+236
-87
@@ -1,10 +1,10 @@
|
||||
import test from 'ava';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import tempy from 'tempy';
|
||||
import {spy, stub} from 'sinon';
|
||||
import clearModule from 'clear-module';
|
||||
import AggregateError from 'aggregate-error';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
import DEFINITIONS from '../lib/plugins/definitions';
|
||||
import DEFINITIONS from '../lib/definitions/plugins';
|
||||
import {
|
||||
gitHead as getGitHead,
|
||||
gitTagHead,
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
const envBackup = Object.assign({}, process.env);
|
||||
// Save the current working diretory
|
||||
const cwd = process.cwd();
|
||||
const pluginNoop = require.resolve('./fixtures/plugin-noop');
|
||||
|
||||
test.beforeEach(t => {
|
||||
clearModule('../lib/hide-sensitive');
|
||||
|
||||
// Delete environment variables that could have been set on the machine running the tests
|
||||
delete process.env.GIT_CREDENTIALS;
|
||||
delete process.env.GH_TOKEN;
|
||||
@@ -31,8 +31,8 @@ test.beforeEach(t => {
|
||||
delete process.env.GL_TOKEN;
|
||||
delete process.env.GITLAB_TOKEN;
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.error = stub();
|
||||
t.context.log = spy();
|
||||
t.context.error = spy();
|
||||
t.context.logger = {log: t.context.log, error: t.context.error};
|
||||
t.context.stdout = stub(process.stdout, 'write');
|
||||
t.context.stderr = stub(process.stderr, 'write');
|
||||
@@ -66,7 +66,9 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const release1 = {name: 'Release 1', url: 'https://release1.com'};
|
||||
const publish1 = stub().resolves(release1);
|
||||
const success = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
@@ -75,7 +77,8 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
publish: [publish1, pluginNoop],
|
||||
success,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -116,14 +119,27 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
t.deepEqual(generateNotes.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
|
||||
t.is(publish.callCount, 1);
|
||||
t.deepEqual(publish.args[0][0], config);
|
||||
t.deepEqual(publish.args[0][1].options, options);
|
||||
t.deepEqual(publish.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
|
||||
t.deepEqual(publish.args[0][1].commits[0].hash, commits[0].hash);
|
||||
t.deepEqual(publish.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(publish.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.is(publish1.callCount, 1);
|
||||
t.deepEqual(publish1.args[0][0], config);
|
||||
t.deepEqual(publish1.args[0][1].options, options);
|
||||
t.deepEqual(publish1.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(publish1.args[0][1].lastRelease, lastRelease);
|
||||
t.deepEqual(publish1.args[0][1].commits[0].hash, commits[0].hash);
|
||||
t.deepEqual(publish1.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
|
||||
t.is(success.callCount, 1);
|
||||
t.deepEqual(success.args[0][0], config);
|
||||
t.deepEqual(success.args[0][1].options, options);
|
||||
t.deepEqual(success.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(success.args[0][1].lastRelease, lastRelease);
|
||||
t.deepEqual(success.args[0][1].commits[0].hash, commits[0].hash);
|
||||
t.deepEqual(success.args[0][1].commits[0].message, commits[0].message);
|
||||
t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
t.deepEqual(success.args[0][1].releases, [
|
||||
{...release1, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
||||
{...nextRelease, ...{notes}, ...{pluginName: pluginNoop}},
|
||||
]);
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead);
|
||||
@@ -138,20 +154,16 @@ test.serial('Use custom tag format', async t => {
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions,
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits: stub().resolves(nextRelease.type),
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(notes),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -192,6 +204,8 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes,
|
||||
publish: [publish1, publish2],
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -204,19 +218,69 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
|
||||
t.is(generateNotes.callCount, 2);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
t.is(publish1.callCount, 1);
|
||||
t.deepEqual(publish1.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
|
||||
nextRelease.gitHead = await getGitHead();
|
||||
|
||||
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, {...nextRelease, ...{notes}});
|
||||
t.is(publish2.callCount, 1);
|
||||
t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
|
||||
t.deepEqual(publish2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the last gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash);
|
||||
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), commits[0].hash);
|
||||
});
|
||||
|
||||
test.serial('Call all "success" plugins even if one errors out', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
await gitCommits(['Second']);
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions1 = stub().resolves();
|
||||
const verifyConditions2 = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const release = {name: 'Release', url: 'https://release.com'};
|
||||
const publish = stub().resolves(release);
|
||||
const success1 = stub().rejects();
|
||||
const success2 = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||
analyzeCommits,
|
||||
generateNotes,
|
||||
publish,
|
||||
success: [success1, success2],
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.is(success1.callCount, 1);
|
||||
t.deepEqual(success1.args[0][0], config);
|
||||
t.deepEqual(success1.args[0][1].releases, [
|
||||
{...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
||||
]);
|
||||
|
||||
t.is(success2.callCount, 1);
|
||||
t.deepEqual(success2.args[0][1].releases, [
|
||||
{...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
|
||||
]);
|
||||
});
|
||||
|
||||
test.serial('Log all "verifyConditions" errors', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
@@ -226,10 +290,12 @@ test.serial('Log all "verifyConditions" errors', async t => {
|
||||
const error1 = new Error('error 1');
|
||||
const error2 = new SemanticReleaseError('error 2', 'ERR2');
|
||||
const error3 = new SemanticReleaseError('error 3', 'ERR3');
|
||||
const fail = stub().resolves();
|
||||
const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [stub().rejects(error1), stub().rejects(error2), stub().rejects(error3)],
|
||||
...config,
|
||||
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -246,6 +312,11 @@ test.serial('Log all "verifyConditions" errors', async t => {
|
||||
error1,
|
||||
]);
|
||||
t.true(t.context.error.calledAfter(t.context.log));
|
||||
t.is(fail.callCount, 1);
|
||||
t.deepEqual(fail.args[0][0], config);
|
||||
t.deepEqual(fail.args[0][1].options, options);
|
||||
t.deepEqual(fail.args[0][1].logger, t.context.logger);
|
||||
t.deepEqual(fail.args[0][1].errors, [error2, error3]);
|
||||
});
|
||||
|
||||
test.serial('Log all "verifyRelease" errors', async t => {
|
||||
@@ -260,12 +331,14 @@ test.serial('Log all "verifyRelease" errors', async t => {
|
||||
|
||||
const error1 = new SemanticReleaseError('error 1', 'ERR1');
|
||||
const error2 = new SemanticReleaseError('error 2', 'ERR2');
|
||||
const fail = stub().resolves();
|
||||
const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
...config,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits: stub().resolves('major'),
|
||||
verifyRelease: [stub().rejects(error1), stub().rejects(error2)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -277,9 +350,12 @@ test.serial('Log all "verifyRelease" errors', async t => {
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
|
||||
t.is(fail.callCount, 1);
|
||||
t.deepEqual(fail.args[0][0], config);
|
||||
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
|
||||
});
|
||||
|
||||
test.serial('Dry-run skips publish', async t => {
|
||||
test.serial('Dry-run skips publish and success', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
@@ -297,6 +373,7 @@ test.serial('Dry-run skips publish', async t => {
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const success = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: true,
|
||||
@@ -307,6 +384,7 @@ test.serial('Dry-run skips publish', async t => {
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -321,6 +399,41 @@ test.serial('Dry-run skips publish', async t => {
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 0);
|
||||
t.is(success.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Dry-run skips fail', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
// Create the tag corresponding to version 1.0.0
|
||||
await gitTagVersion('v1.0.0');
|
||||
// Add new commits to the master branch
|
||||
await gitCommits(['Second']);
|
||||
|
||||
const error1 = new SemanticReleaseError('error 1', 'ERR1');
|
||||
const error2 = new SemanticReleaseError('error 2', 'ERR2');
|
||||
const fail = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: true,
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [stub().rejects(error1), stub().rejects(error2)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = await t.throws(semanticRelease(options));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
|
||||
t.is(fail.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t => {
|
||||
@@ -341,6 +454,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const success = stub().resolves();
|
||||
|
||||
const options = {
|
||||
dryRun: false,
|
||||
@@ -351,6 +465,8 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -365,6 +481,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 0);
|
||||
t.is(success.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Allow local releases with "noCi" option', async t => {
|
||||
@@ -385,6 +502,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
const success = stub().resolves();
|
||||
|
||||
const options = {
|
||||
noCi: true,
|
||||
@@ -395,6 +513,8 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -413,6 +533,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
t.is(verifyRelease.callCount, 1);
|
||||
t.is(generateNotes.callCount, 1);
|
||||
t.is(publish.callCount, 1);
|
||||
t.is(success.callCount, 1);
|
||||
});
|
||||
|
||||
test.serial('Accept "undefined" value returned by the "generateNotes" plugins', async t => {
|
||||
@@ -427,7 +548,6 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
||||
|
||||
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
@@ -436,11 +556,13 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -463,18 +585,6 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
|
||||
t.falsy(publish.args[0][1].nextRelease.notes);
|
||||
});
|
||||
|
||||
test.serial('Returns falsy value if not running from a git repository', async t => {
|
||||
// Set the current working directory to a temp directory
|
||||
process.chdir(tempy.directory());
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'}));
|
||||
t.is(t.context.error.args[0][0], 'Semantic-release must run from a git repository.');
|
||||
});
|
||||
|
||||
test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
@@ -486,7 +596,7 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
|
||||
t.falsy(await semanticRelease({repositoryUrl}));
|
||||
t.is(
|
||||
t.context.log.args[6][0],
|
||||
t.context.log.args[8][0],
|
||||
"This run was triggered by a pull request and therefore a new version won't be published."
|
||||
);
|
||||
});
|
||||
@@ -494,21 +604,16 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
|
||||
test.serial('Returns falsy value if not running from the configured branch', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
const publish = stub().resolves();
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits: stub().resolves(),
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -529,7 +634,6 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
|
||||
// Add commits to the master branch
|
||||
await gitCommits(['First']);
|
||||
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
@@ -538,11 +642,13 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
verifyConditions: [stub().resolves()],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -572,22 +678,17 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
|
||||
'Test commit\n\n commit body\n[skip release]',
|
||||
'Test commit\n\n commit body\n[release skip]',
|
||||
]);
|
||||
|
||||
const verifyConditions1 = stub().resolves();
|
||||
const verifyConditions2 = stub().resolves();
|
||||
const analyzeCommits = stub().resolves();
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves();
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||
verifyConditions: [stub().resolves(), stub().resolves()],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -622,12 +723,60 @@ test.serial('Hide sensitive environment variable values from the logs', async t
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.regex(t.context.stdout.args[6][0], /Console: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stdout.args[7][0], /Log: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stdout.args[8][0], /Console: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stdout.args[9][0], /Log: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stderr.args[0][0], /Error: The token \[secure\] is invalid/);
|
||||
t.regex(t.context.stderr.args[1][0], /Invalid token \[secure\]/);
|
||||
});
|
||||
|
||||
test.serial('Log both plugins errors and errors thrown by "fail" plugin', async t => {
|
||||
process.env.MY_TOKEN = 'secret token';
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const pluginError = new SemanticReleaseError('Plugin error', 'ERR');
|
||||
const failError1 = new Error('Fail error 1');
|
||||
const failError2 = new Error('Fail error 2');
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: stub().rejects(pluginError),
|
||||
fail: [stub().rejects(failError1), stub().rejects(failError2)],
|
||||
};
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError1);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s Plugin error', 'ERR']);
|
||||
});
|
||||
|
||||
test.serial('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
||||
process.env.MY_TOKEN = 'secret token';
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const pluginError = new Error('Plugin error');
|
||||
const fail = stub().resolves();
|
||||
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: stub().rejects(pluginError),
|
||||
fail,
|
||||
};
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options));
|
||||
|
||||
t.true(fail.notCalled);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
|
||||
});
|
||||
|
||||
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
@@ -661,6 +810,8 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
repositoryUrl,
|
||||
verifyConditions: [verifyConditions],
|
||||
analyzeCommits,
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
@@ -671,7 +822,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
|
||||
// Verify error message
|
||||
t.regex(error.message, new RegExp(DEFINITIONS.analyzeCommits.output.message));
|
||||
t.regex(error.message, /Received: 'string'/);
|
||||
t.regex(error.details, /string/);
|
||||
});
|
||||
|
||||
test.serial('Get all commits including the ones not in the shallow clone', async t => {
|
||||
@@ -684,20 +835,18 @@ test.serial('Get all commits including the ones not in the shallow clone', async
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions,
|
||||
verifyConditions: stub().resolves(),
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
verifyRelease: stub().resolves(),
|
||||
generateNotes: stub().resolves(notes),
|
||||
publish: stub().resolves(),
|
||||
success: stub().resolves(),
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
|
||||
@@ -101,6 +101,7 @@ test.serial('Release patch, minor and major versions', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {success: false, fail: false},
|
||||
});
|
||||
// Create a npm-shrinkwrap.json file
|
||||
await execa('npm', ['shrinkwrap'], {env: testEnv});
|
||||
@@ -298,7 +299,7 @@ test.serial('Exit with 1 if a plugin is not found', async t => {
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: `git+https://github.com/${owner}/${packageName}`},
|
||||
release: {analyzeCommits: 'non-existing-path'},
|
||||
release: {analyzeCommits: 'non-existing-path', success: false, fail: false},
|
||||
});
|
||||
|
||||
const {code, stderr} = await t.throws(execa(cli, [], {env}));
|
||||
@@ -316,7 +317,7 @@ test.serial('Exit with 1 if a shareable config is not found', async t => {
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: `git+https://github.com/${owner}/${packageName}`},
|
||||
release: {extends: 'non-existing-path'},
|
||||
release: {extends: 'non-existing-path', success: false, fail: false},
|
||||
});
|
||||
|
||||
const {code, stderr} = await t.throws(execa(cli, [], {env}));
|
||||
@@ -336,7 +337,7 @@ test.serial('Exit with 1 if a shareable config reference a not found plugin', as
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: `git+https://github.com/${owner}/${packageName}`},
|
||||
release: {extends: './shareable.json'},
|
||||
release: {extends: './shareable.json', success: false, fail: false},
|
||||
});
|
||||
await writeJson('./shareable.json', shareable);
|
||||
|
||||
@@ -357,6 +358,7 @@ test.serial('Dry-run', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {success: false, fail: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -394,6 +396,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {success: false, fail: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -459,7 +462,17 @@ test.serial('Pass options via CLI arguments', async t => {
|
||||
t.log('$ semantic-release');
|
||||
const {stdout, code} = await execa(
|
||||
cli,
|
||||
['--verify-conditions', '@semantic-release/npm', '--publish', '@semantic-release/npm', '--debug'],
|
||||
[
|
||||
'--verify-conditions',
|
||||
'@semantic-release/npm',
|
||||
'--publish',
|
||||
'@semantic-release/npm',
|
||||
`--success`,
|
||||
false,
|
||||
`--fail`,
|
||||
false,
|
||||
'--debug',
|
||||
],
|
||||
{env}
|
||||
);
|
||||
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
|
||||
@@ -515,7 +528,7 @@ test.serial('Run via JS API', async t => {
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit']);
|
||||
t.log('$ Call semantic-release via API');
|
||||
await semanticRelease();
|
||||
await semanticRelease({fail: false, success: false});
|
||||
|
||||
// Verify package.json and has been updated
|
||||
t.is((await readJson('./package.json')).version, version);
|
||||
@@ -545,7 +558,7 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
release: {verifyConditions: pluginError},
|
||||
release: {verifyConditions: pluginError, fail: false, success: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -572,7 +585,7 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 1', async
|
||||
name: packageName,
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
release: {verifyConditions: pluginInheritedError},
|
||||
release: {verifyConditions: pluginInheritedError, fail: false, success: false},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import test from 'ava';
|
||||
import definitions from '../../lib/plugins/definitions';
|
||||
|
||||
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(definitions.verifyConditions.config.validator({}));
|
||||
t.false(definitions.verifyConditions.config.validator({path: null}));
|
||||
|
||||
t.true(definitions.verifyConditions.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.verifyConditions.config.validator());
|
||||
t.true(definitions.verifyConditions.config.validator('plugin-path.js'));
|
||||
t.true(definitions.verifyConditions.config.validator(() => {}));
|
||||
t.true(definitions.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
|
||||
t.false(definitions.analyzeCommits.config.validator({}));
|
||||
t.false(definitions.analyzeCommits.config.validator({path: null}));
|
||||
t.false(definitions.analyzeCommits.config.validator([]));
|
||||
t.false(definitions.analyzeCommits.config.validator());
|
||||
|
||||
t.true(definitions.analyzeCommits.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.analyzeCommits.config.validator('plugin-path.js'));
|
||||
t.true(definitions.analyzeCommits.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(definitions.verifyRelease.config.validator({}));
|
||||
t.false(definitions.verifyRelease.config.validator({path: null}));
|
||||
|
||||
t.true(definitions.verifyRelease.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.verifyRelease.config.validator());
|
||||
t.true(definitions.verifyRelease.config.validator('plugin-path.js'));
|
||||
t.true(definitions.verifyRelease.config.validator(() => {}));
|
||||
t.true(definitions.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
|
||||
t.false(definitions.generateNotes.config.validator({}));
|
||||
t.false(definitions.generateNotes.config.validator({path: null}));
|
||||
t.false(definitions.generateNotes.config.validator([]));
|
||||
|
||||
t.true(definitions.generateNotes.config.validator());
|
||||
t.true(definitions.generateNotes.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.generateNotes.config.validator('plugin-path.js'));
|
||||
t.true(definitions.generateNotes.config.validator(() => {}));
|
||||
});
|
||||
|
||||
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
|
||||
t.false(definitions.publish.config.validator({}));
|
||||
t.false(definitions.publish.config.validator({path: null}));
|
||||
t.false(definitions.publish.config.validator());
|
||||
|
||||
t.true(definitions.publish.config.validator({path: 'plugin-path.js'}));
|
||||
t.true(definitions.publish.config.validator('plugin-path.js'));
|
||||
t.true(definitions.publish.config.validator(() => {}));
|
||||
t.true(definitions.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||
t.false(definitions.analyzeCommits.output.validator('invalid'));
|
||||
t.false(definitions.analyzeCommits.output.validator(1));
|
||||
t.false(definitions.analyzeCommits.output.validator({}));
|
||||
|
||||
t.true(definitions.analyzeCommits.output.validator());
|
||||
t.true(definitions.analyzeCommits.output.validator(null));
|
||||
t.true(definitions.analyzeCommits.output.validator('major'));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin output, if defined, must be a string', t => {
|
||||
t.false(definitions.generateNotes.output.validator(1));
|
||||
t.false(definitions.generateNotes.output.validator({}));
|
||||
|
||||
t.true(definitions.generateNotes.output.validator());
|
||||
t.true(definitions.generateNotes.output.validator(null));
|
||||
t.true(definitions.generateNotes.output.validator(''));
|
||||
t.true(definitions.generateNotes.output.validator('string'));
|
||||
});
|
||||
@@ -12,6 +12,7 @@ test.beforeEach(t => {
|
||||
test('Normalize and load plugin from string', t => {
|
||||
const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/plugin-noop', t.context.logger);
|
||||
|
||||
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
|
||||
});
|
||||
@@ -19,6 +20,7 @@ test('Normalize and load plugin from string', t => {
|
||||
test('Normalize and load plugin from object', t => {
|
||||
const plugin = normalize('publish', {}, {}, {path: './test/fixtures/plugin-noop'}, t.context.logger);
|
||||
|
||||
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']);
|
||||
});
|
||||
@@ -32,6 +34,7 @@ test('Normalize and load plugin from a base file path', t => {
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
t.is(plugin.pluginName, './plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], [
|
||||
'Load plugin %s from %s in shareable config %s',
|
||||
@@ -41,9 +44,40 @@ test('Normalize and load plugin from a base file path', t => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from function', t => {
|
||||
const plugin = normalize('', {}, {}, () => {}, t.context.logger);
|
||||
test('Wrap plugin in a function that add the "pluginName" to the error"', async t => {
|
||||
const plugin = normalize(
|
||||
'verifyConditions',
|
||||
{'./plugin-error': './test/fixtures'},
|
||||
{},
|
||||
'./plugin-error',
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
t.is(error.pluginName, './plugin-error');
|
||||
});
|
||||
|
||||
test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async t => {
|
||||
const plugin = normalize(
|
||||
'verifyConditions',
|
||||
{'./plugin-errors': './test/fixtures'},
|
||||
{},
|
||||
'./plugin-errors',
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
const errors = [...(await t.throws(plugin()))];
|
||||
for (const error of errors) {
|
||||
t.is(error.pluginName, './plugin-errors');
|
||||
}
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from function', t => {
|
||||
const pluginFunction = () => {};
|
||||
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger);
|
||||
|
||||
t.is(plugin.pluginName, '[Function: pluginFunction]');
|
||||
t.is(typeof plugin, 'function');
|
||||
});
|
||||
|
||||
@@ -54,18 +88,42 @@ test('Normalize and load plugin that retuns multiple functions', t => {
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
|
||||
});
|
||||
|
||||
test('Wrap plugin in a function that validate the output of the plugin', async t => {
|
||||
const pluginFunction = stub().resolves(1);
|
||||
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger, {
|
||||
validator: output => output === 1,
|
||||
message: 'The output must be 1.',
|
||||
});
|
||||
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
|
||||
const analyzeCommits = stub().resolves(2);
|
||||
const plugin = normalize('analyzeCommits', {}, {}, analyzeCommits, t.context.logger);
|
||||
|
||||
await t.notThrows(plugin());
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
pluginFunction.resolves(2);
|
||||
const error = await t.throws(plugin(), Error);
|
||||
t.is(error.message, 'The output must be 1. Received: 2');
|
||||
t.is(error.code, 'EANALYZEOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
|
||||
const generateNotes = stub().resolves(2);
|
||||
const plugin = normalize('generateNotes', {}, {}, generateNotes, t.context.logger);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
t.is(error.code, 'ERELEASENOTESOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
|
||||
const plugin = normalize(
|
||||
'publish',
|
||||
{'./plugin-identity': './test/fixtures'},
|
||||
{},
|
||||
'./plugin-identity',
|
||||
t.context.logger
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin(2));
|
||||
|
||||
t.is(error.code, 'EPUBLISHOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
|
||||
@@ -127,12 +185,8 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async
|
||||
test('Throws an error if the plugin return an object without the expected plugin function', t => {
|
||||
const error = t.throws(() => normalize('inexistantPlugin', {}, {}, './test/fixtures/multi-plugin', t.context.logger));
|
||||
|
||||
t.is(error.code, 'EPLUGINCONF');
|
||||
t.is(error.code, 'EPLUGIN');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.is(
|
||||
error.message,
|
||||
'The inexistantPlugin plugin must be a function, or an object with a function in the property inexistantPlugin.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Throws an error if the plugin is not found', t => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from 'ava';
|
||||
import {stub} from 'sinon';
|
||||
import AggregateError from 'aggregate-error';
|
||||
import pipeline from '../../lib/plugins/pipeline';
|
||||
|
||||
test('Execute each function in series passing the same input', async t => {
|
||||
@@ -17,13 +18,32 @@ test('Execute each function in series passing the same input', async t => {
|
||||
t.true(step2.calledBefore(step3));
|
||||
});
|
||||
|
||||
test('Execute each function in series passing a transformed input', async t => {
|
||||
test('With one step, returns the step values rather than an Array ', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
|
||||
const result = await pipeline([step1])(0);
|
||||
|
||||
t.deepEqual(result, 1);
|
||||
t.true(step1.calledWith(0));
|
||||
});
|
||||
|
||||
test('With one step, throws the error rather than an AggregateError ', async t => {
|
||||
const error = new Error('test error 1');
|
||||
const step1 = stub().rejects(error);
|
||||
|
||||
const thrown = await t.throws(pipeline([step1])(0));
|
||||
|
||||
t.is(error, thrown);
|
||||
});
|
||||
|
||||
test('Execute each function in series passing a transformed input from "getNextInput"', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = (lastResult, result) => lastResult + result;
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => prevResult + result);
|
||||
const result = await pipeline([step1, step2, step3, step4])(0, {settleAll: false, getNextInput});
|
||||
|
||||
t.deepEqual(result, [1, 2, 3, 4]);
|
||||
t.true(step1.calledWith(0));
|
||||
@@ -35,22 +55,45 @@ test('Execute each function in series passing a transformed input', async t => {
|
||||
t.true(step3.calledBefore(step4));
|
||||
});
|
||||
|
||||
test('Execute each function in series passing the result of the previous one', async t => {
|
||||
test('Execute each function in series passing the "lastResult" and "result" to "getNextInput"', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = stub().returnsArg(0);
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(0, false, (prevResult, result) => result);
|
||||
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: false, getNextInput});
|
||||
|
||||
t.deepEqual(result, [1, 2, 3, 4]);
|
||||
t.true(step1.calledWith(0));
|
||||
t.true(step2.calledWith(1));
|
||||
t.true(step3.calledWith(2));
|
||||
t.true(step4.calledWith(3));
|
||||
t.true(step1.calledBefore(step2));
|
||||
t.true(step2.calledBefore(step3));
|
||||
t.true(step3.calledBefore(step4));
|
||||
t.deepEqual(getNextInput.args, [[5, 1], [5, 2], [5, 3], [5, 4]]);
|
||||
});
|
||||
|
||||
test('Execute each function in series calling "transform" to modify the results', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = stub().returnsArg(0);
|
||||
const transform = stub().callsFake(result => result + 1);
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(5, {getNextInput, transform});
|
||||
|
||||
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
|
||||
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
|
||||
});
|
||||
|
||||
test('Execute each function in series calling "transform" to modify the results with "settleAll"', async t => {
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().resolves(2);
|
||||
const step3 = stub().resolves(3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = stub().returnsArg(0);
|
||||
const transform = stub().callsFake(result => result + 1);
|
||||
|
||||
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: true, getNextInput, transform});
|
||||
|
||||
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
|
||||
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
|
||||
});
|
||||
|
||||
test('Stop execution and throw error is a step rejects', async t => {
|
||||
@@ -65,6 +108,22 @@ test('Stop execution and throw error is a step rejects', async t => {
|
||||
t.true(step3.notCalled);
|
||||
});
|
||||
|
||||
test('Throw all errors from the first step throwing an AggregateError', async t => {
|
||||
const error1 = new Error('test error 1');
|
||||
const error2 = new Error('test error 2');
|
||||
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().rejects(new AggregateError([error1, error2]));
|
||||
const step3 = stub().resolves(3);
|
||||
|
||||
const errors = await t.throws(pipeline([step1, step2, step3])(0));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.true(step1.calledWith(0));
|
||||
t.true(step2.calledWith(0));
|
||||
t.true(step3.notCalled);
|
||||
});
|
||||
|
||||
test('Execute all even if a Promise rejects', async t => {
|
||||
const error1 = new Error('test error 1');
|
||||
const error2 = new Error('test error 2');
|
||||
@@ -72,7 +131,7 @@ test('Execute all even if a Promise rejects', async t => {
|
||||
const step2 = stub().rejects(error1);
|
||||
const step3 = stub().rejects(error2);
|
||||
|
||||
const errors = await t.throws(pipeline([step1, step2, step3])(0, true));
|
||||
const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true}));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2]);
|
||||
t.true(step1.calledWith(0));
|
||||
@@ -80,17 +139,31 @@ test('Execute all even if a Promise rejects', async t => {
|
||||
t.true(step3.calledWith(0));
|
||||
});
|
||||
|
||||
test('Execute each function in series passing a transformed input even if a Promise rejects', async t => {
|
||||
test('Throw all errors from all steps throwing an AggregateError', async t => {
|
||||
const error1 = new Error('test error 1');
|
||||
const error2 = new Error('test error 2');
|
||||
const error3 = new Error('test error 3');
|
||||
const error4 = new Error('test error 4');
|
||||
const step1 = stub().rejects(new AggregateError([error1, error2]));
|
||||
const step2 = stub().rejects(new AggregateError([error3, error4]));
|
||||
|
||||
const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true}));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error1, error2, error3, error4]);
|
||||
t.true(step1.calledWith(0));
|
||||
t.true(step2.calledWith(0));
|
||||
});
|
||||
|
||||
test('Execute each function in series passing a transformed input even if a step rejects', async t => {
|
||||
const error2 = new Error('test error 2');
|
||||
const error3 = new Error('test error 3');
|
||||
const step1 = stub().resolves(1);
|
||||
const step2 = stub().rejects(error2);
|
||||
const step3 = stub().rejects(error3);
|
||||
const step4 = stub().resolves(4);
|
||||
const getNextInput = (prevResult, result) => prevResult + result;
|
||||
|
||||
const errors = await t.throws(
|
||||
pipeline([step1, step2, step3, step4])(0, true, (prevResult, result) => prevResult + result)
|
||||
);
|
||||
const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput}));
|
||||
|
||||
t.deepEqual(Array.from(errors), [error2, error3]);
|
||||
t.true(step1.calledWith(0));
|
||||
|
||||
@@ -28,6 +28,8 @@ test('Export default plugins', t => {
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Export plugins based on config', t => {
|
||||
@@ -48,6 +50,8 @@ test('Export plugins based on config', t => {
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test.serial('Export plugins loaded from the dependency of a shareable config module', async t => {
|
||||
@@ -76,6 +80,8 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test.serial('Export plugins loaded from the dependency of a shareable config file', async t => {
|
||||
@@ -101,6 +107,8 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Use default when only options are passed for a single plugin', t => {
|
||||
@@ -128,22 +136,10 @@ test('Merge global options with plugin options', async t => {
|
||||
});
|
||||
|
||||
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
|
||||
const errors = Array.from(
|
||||
t.throws(() => getPlugins({verifyConditions: {}, verifyRelease: {}}, {}, t.context.logger))
|
||||
);
|
||||
const errors = Array.from(t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[0].message,
|
||||
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
|
||||
);
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[1].message,
|
||||
'The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
|
||||
@@ -153,8 +149,4 @@ test('Throw an error if an array of plugin configuration is missing a path for p
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[0].message,
|
||||
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
|
||||
);
|
||||
});
|
||||
|
||||
+10
-13
@@ -29,29 +29,29 @@ test.afterEach.always(() => {
|
||||
process.chdir(cwd);
|
||||
});
|
||||
|
||||
test.serial('Return "false" if does not run on a git repository', async t => {
|
||||
const dir = tempy.directory();
|
||||
process.chdir(dir);
|
||||
|
||||
t.false(await verify({}, 'master', t.context.logger));
|
||||
});
|
||||
|
||||
test.serial('Throw a AggregateError', async t => {
|
||||
await gitRepo();
|
||||
|
||||
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, 'The repositoryUrl option is required');
|
||||
t.is(errors[0].code, 'ENOREPOURL');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].message, 'The tagFormat template must compile to a valid Git tag format');
|
||||
t.is(errors[1].code, 'EINVALIDTAGFORMAT');
|
||||
t.is(errors[2].name, 'SemanticReleaseError');
|
||||
t.is(errors[2].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[2].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if does not run on a git repository', async t => {
|
||||
const dir = tempy.directory();
|
||||
process.chdir(dir);
|
||||
|
||||
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'ENOGITREPO');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: `?\${version}`};
|
||||
@@ -59,7 +59,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', asyn
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, 'The tagFormat template must compile to a valid Git tag format');
|
||||
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
|
||||
});
|
||||
|
||||
@@ -70,7 +69,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains t
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[0].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
@@ -81,7 +79,6 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[0].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user