Compare commits

...
52 Commits
Author SHA1 Message Date
Pierre Vanduynslager 7c48afad47 fix: prevent git CLI to prompt user/password on CI 2018-03-16 14:33:18 -04:00
Honza JavorekandPierre Vanduynslager c84ac15020 fix(package): Remove commander.js dependency (#704) 2018-03-16 14:18:49 -04:00
Pierre VanduynslagerandGregor Martynus 5f1d530e2a feat: allow to use shorthand for repositoryUrl
- `owner/repo` => `https://github.com/owner/repo.git`
- `gitlab:owner/repo` => `https://gitlab.com/owner/repo.git`
- `bitbucket:owner/repo` => `https://bitbucket.com/owner/repo.git`
2018-03-15 22:40:09 +00:00
greenkeeper[bot]andPierre Vanduynslager f13ec6a615 fix(package): update execa to version 0.10.0 2018-03-15 08:18:04 -04:00
Pierre VanduynslagerandGregor Martynus b0b4fc82de fix: convert git+https URL in package.json to https 2018-03-13 00:54:32 +00:00
Pierre VanduynslagerandGregor Martynus 6f74dcbd4a fix: use correct debug namespace 2018-03-13 00:54:32 +00:00
Gregor MartynusandPierre Vanduynslager 1129d47e1d docs(configuration): environment variable DEBUG must be set to semantic-release:*, not semantic-release 2018-03-12 16:39:46 -04:00
Pierre VanduynslagerandGregor Martynus e4618a2fbc fix: exclude prereleases from version retrived by getLastRelease 2018-03-08 17:25:40 +00:00
Pierre VanduynslagerandGregor Martynus e5a73d8628 fix: remove unecessary console.log 2018-03-07 23:01:08 +00:00
robertandPierre Vanduynslager 50f3c6e140 docs(README): correct pluralization 2018-03-07 09:39:20 -05:00
greenkeeper[bot]andPierre Vanduynslager 78f0937f24 chore(package): update proxyquire to version 2.0.0 2018-03-03 16:42:54 -05:00
William HosfordandGregor Martynus a7c187f31b docs: fix grammar and typos in README, CONTRIBUTING, installation guide, and plugin guide 2018-02-25 17:43:51 +00:00
Pierre Vanduynslager c2beb643fa feat: add the prepare plugin hook
BREAKING CHANGE: Committing or creating files in the `publish` plugin hook is not supported anymore and now must be done in the `prepare` hook

Plugins with a `publish` hook that makes a commit or create a file that can be committed must use the `prepare` hook.
2018-02-19 00:28:50 -05:00
Pierre Vanduynslager 20246c02b1 fix: exclude empty env var value from replacement 2018-02-19 00:01:06 -05:00
Pierre Vanduynslager 857d4180e9 fix: allow boolean option to be set in config file 2018-02-16 00:51:58 -05:00
Pierre Vanduynslager 4d0490122c style: lint 2018-02-15 23:19:57 -05:00
Pierre Vanduynslager aa724e80f7 chore(package): simplify xo configuration 2018-02-15 23:19:57 -05:00
greenkeeper[bot]andPierre Vanduynslager ce15b75edf chore(package): update xo to version 0.20.0 2018-02-15 23:19:57 -05:00
Pierre VanduynslagerandGregor Martynus 1966f0e3e2 fix: verify branch first 2018-02-15 18:35:37 +00:00
Pierre Vanduynslager 305f4ee8eb fix: do not transform repositoryUrl if it allow to push
Even the user set Git credentials via environment variable, use the configured URL (with authentication) if it works.
This allow users to push tags and commits via ssh while still using the GitHub/GitLab API.
2018-02-14 10:45:44 -05:00
Pierre Vanduynslager 9788fcad4e feat: use @semantic-release/github as default for success and fail hooks
BREAKING CHANGE: `success` and `fail` hooks are now enabled by default

In order to disable the `@semantic-release/github` plugin for the `success` and `fail` hook, the corresponding options have to be set to `false` in the **semantic-release** configuration:

```json
{
  "release": {
    "success": false,
    "fail": false
  }
}
```

Users who do not use the `@semantic-release/github` plugin, should disable it in the `success` and `fail` by setting the corresponding options to `false` or to alternative plugin providing `success` and `fail` hooks.
2018-02-13 16:33:37 -05:00
Pierre Vanduynslager 04f3061bed fix: remove the github plugin from default success and fail hooks 2018-02-13 16:06:30 -05:00
Pierre Vanduynslager 1b3c51d624 docs: add missing success and fail options 2018-02-13 16:06:30 -05:00
Pierre Vanduynslager 8a2ef48308 test: revert to mockserver:latest docker container 2018-02-12 21:40:47 -05:00
Pierre Vanduynslager ce1e74f611 fix: set repository authentication when repositoryUrl is set as an option 2018-02-12 16:17:09 -05:00
Pierre Vanduynslager b6837a20a8 test: use older version of mockserver
Due to https://github.com/jamesdbloom/mockserver/issues/435
2018-02-12 16:17:09 -05:00
Pierre VanduynslagerandGregor Martynus 97cb354fea feat: improve CLI
- Replace `commander.js` with `yargs`
- Add CLI unit tests
- Add a `--version` option
- Improve `--help` output
- Remove `commander.js` related workaround
- Allow to set list option with arg repetition or space separated list
- Maintain the list options defined as comma separated list
2018-02-12 11:01:09 -08:00
Pierre VanduynslagerandGregor Martynus f92677b092 fix: log current version of semantic-release 2018-02-12 11:01:09 -08:00
Pierre Vanduynslager 9dd127ba98 fix: fix EPLUGINCONF error details 2018-02-12 00:36:06 -05:00
Pierre Vanduynslager c9bd5de075 docs: simplify GitLAb-ci config
- remove Node 4
2018-02-11 22:17:12 -05:00
Pierre Vanduynslager 51f6e5fd60 docs: simplify travis config
- remove OSs
- remove Node 4
2018-02-11 22:17:12 -05:00
Pierre Vanduynslager c6e95b0885 docs: add CircleCI workflows recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager 7fbd2721c4 docs: add Travis Build Stages recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager fcb64e6fef docs: remove unnecessary script override in travis recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager d72cfc2546 docs: fix typo in travis recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager 49f5e704ba feat: add success and fail notification plugins
- Allow `publish` plugins to return an `Object` with information related to the releases
- Add the `success` plugin hook, called when all `publish` are successful, receiving a list of release
- Add the `fail` plugin hook, called when an error happens at any point, receiving a list of errors
- Add detailed message for each error
2018-02-11 19:53:41 -05:00
Pierre Vanduynslager 9b2f6bfed2 feat: allow plugins to throw an iterable list of errors 2018-02-11 19:53:41 -05:00
Pierre Vanduynslager 9360caf253 docs: fix minor typo in FAQ 2018-02-10 23:38:12 -05:00
mchao409andGregor Martynus 2f8d71644d docs: make some grammatical, spelling, typo fixes. 2018-02-09 21:26:21 -08:00
Pierre Vanduynslager 5fd990acef docs: fix typo in extending link 2018-02-09 18:43:56 -05:00
Pierre Vanduynslager 687435b9a2 feat: add debug logs for git commands 2018-02-08 16:43:17 -05:00
Pierre Vanduynslager 7e785fa757 fix: debug log all options 2018-02-08 16:13:04 -05:00
Pierre Vanduynslager 053c9edf9b test: add test to parse GitLab/Bitbucket URLs with groups 2018-02-07 21:03:36 -05:00
Pierre Vanduynslager 2edd9da598 fix(package): update git-url-parse to version 8.1.0 2018-02-07 21:03:36 -05:00
Pierre Vanduynslager cbf57851fb fix: always transform git+https url to https 2018-02-02 15:33:36 -05:00
Pierre VanduynslagerandGitHub af8c830335 fix(package): update @semantic-release/github to version 4.0.2 2018-02-02 13:55:21 -05:00
Christian MusaandPierre Vanduynslager d8208237f2 fix: use long option to delete tag in git push 2018-01-31 16:22:04 -05:00
Pierre Vanduynslager 39536fa34e feat: add tagFormat option to customize Git tag name 2018-01-29 00:55:32 -05:00
Pierre Vanduynslager faabffb208 fix: log all core verification errors 2018-01-29 00:55:32 -05:00
Anthony NgandGregor Martynus 2f75dff3fc docs: fix image path (#625) 2018-01-28 14:01:12 -08:00
Stephan BönnemannandGregor Martynus dc416b2ce9 docs: explain npx alternatives for Yarn 2018-01-27 19:04:12 -08:00
Stephan BönnemannandGregor Martynus e3c9bb8bd4 docs: recommend to execute semantic-release via npx 2018-01-27 19:04:12 -08:00
48 changed files with 2188 additions and 782 deletions
+25 -25
View File
@@ -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,11 +54,11 @@ 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 :)
If you have 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 :)
Here is a summary of the steps to follow:
@@ -82,16 +82,16 @@ $ 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 your 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
- A [test](#tests) for every possible case introduced by your code change
- **100%** test coverage
- [Valid commit message(s)](#commit-message-guidelines)
- Documentation for new features
@@ -99,10 +99,10 @@ 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:
- Use [links](https://help.github.com/articles/basic-writing-and-formatting-syntax/#links) when you are referring to:
- a **semantic-release** concept described somewhere else in the documentation, i.e. How to [contribute](CONTRIBUTING.md)
- a third-party product/brand/service, i.e. Integrate with [GitHub](https://github.com)
- an external concept or feature, i.e. Create a [GitHub release](https://help.github.com/articles/creating-releases)
@@ -125,7 +125,7 @@ If possible, make [atomic commits](https://en.wikipedia.org/wiki/Atomic_commit),
- a functional change should be contained in exactly one commit
- a commit should not create an inconsistent state (such as test errors, linting errors, partial fix, feature with documentation etc...)
A complex feature can be broken down into multiple commits as long as each one keep a consistent state and consist of a self-contained change.
A complex feature can be broken down into multiple commits as long as each one maintains a consistent state and consists of a self-contained change.
#### Commit message format
@@ -219,21 +219,21 @@ $ npm install
### Lint
All the [semantic-release](https://github.com/semantic-release) repository use [XO](https://github.com/sindresorhus/xo) for linting and [Prettier](https://prettier.io) for formatting. Prettier formatting will be automatically verified and fixed by XO.
All the [semantic-release](https://github.com/semantic-release) repositories use [XO](https://github.com/sindresorhus/xo) for linting and [Prettier](https://prettier.io) for formatting. Prettier formatting will be automatically verified and fixed by XO.
Before pushing your code changes make sure there is no linting errors with `npm run lint`.
Before pushing your code changes make sure there are no linting errors with `npm run lint`.
**Tips**:
- Most linting errors can be automatically fixed with `npm run lint --fix`.
- Install the [XO plugin](https://github.com/sindresorhus/xo#editor-plugins) for your editor to see linting error directly in your editor and automatically fix them on save.
- Install the [XO plugin](https://github.com/sindresorhus/xo#editor-plugins) for your editor to see linting errors directly in your editor and automatically fix them on save.
### 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
@@ -246,6 +246,6 @@ $ npm run test
### Commits
All the [semantic-release](https://github.com/semantic-release) repository use [Commitizen](https://github.com/commitizen/cz-cli) to help you create [valid commit messages](#commit-message-guidelines).
All the [semantic-release](https://github.com/semantic-release) repositories use [Commitizen](https://github.com/commitizen/cz-cli) to help you create [valid commit messages](#commit-message-guidelines).
After staging your changes with `git add`, run `npm run cm` to start the interactive commit message CLI.
+11 -8
View File
@@ -37,21 +37,22 @@ 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?
### Commit message format
**semantic-release** uses the commit messages to determine the type of changes in the codebase. Following formalized conventions for commit messages, **semantic-release** automatically determines the next [semantic version](https://semver.org) number, generates a changelog and publish the release.
**semantic-release** uses the commit messages to determine the type of changes in the codebase. Following formalized conventions for commit messages, **semantic-release** automatically determines the next [semantic version](https://semver.org) number, generates a changelog and publishes the release.
By default **semantic-release** uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). The commit message format can be changed with the [`preset` or `config` options](docs/usage/configuration.md#options) of the [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer#options) and [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator#options) plugins.
Tools such as [commitizen](https://github.com/commitizen/cz-cli), [commitlint](https://github.com/marionebl/commitlint) or [semantic-git-commit-cli](https://github.com/JPeer264/node-semantic-git-commit-cli) can be used to help contributor and enforce valid commits message.
Tools such as [commitizen](https://github.com/commitizen/cz-cli), [commitlint](https://github.com/marionebl/commitlint) or [semantic-git-commit-cli](https://github.com/JPeer264/node-semantic-git-commit-cli) can be used to help contributors and enforce valid commit messages.
Here is an example of the release type that will be done based on a commit messages:
@@ -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 npms [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.
@@ -81,11 +82,13 @@ After running the tests the command `semantic-release` will execute the followin
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Verify Conditions | Verify all the conditions to proceed with the release with the [verify conditions plugins](docs/usage/plugins.md#verifyconditions-plugin). |
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
| Analyze commits | Determine the type of release to do with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
| Analyze commits | Determine the type of release with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
| Create Git tag | Create a Git tag corresponding the new release version |
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
| Create Git tag | Create a Git tag corresponding to the new release version |
| Prepare | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin). |
| 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
+55 -47
View File
@@ -1,58 +1,66 @@
const program = require('commander');
const {pickBy, isUndefined} = require('lodash');
function list(values) {
return values.split(',').map(value => value.trim());
}
const stringList = {
type: 'string',
array: true,
coerce: values =>
values.length === 1 && values[0].trim() === 'false'
? []
: values.reduce((values, value) => values.concat(value.split(',').map(value => value.trim())), []),
};
module.exports = async () => {
program
.name('semantic-release')
.description('Run automated package publishing')
.option('-b, --branch <branch>', 'Branch to release from')
.option('-r, --repository-url <repositoryUrl>', 'Git repository URL')
.option('-e, --extends <paths>', 'Comma separated list of shareable config paths or packages name', list)
.option(
'--verify-conditions <paths>',
'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
list
)
.option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin')
.option(
'--verify-release <paths>',
'Comma separated list of paths or packages name for the verifyRelease plugin(s)',
list
)
.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(
'--no-ci',
'Skip Continuous Integration environment verifications, allowing to make releases from a local machine'
)
.option('--debug', 'Output debugging information')
.option(
'-d, --dry-run',
'Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes'
)
.parse(process.argv);
if (program.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
const cli = require('yargs')
.command('$0', 'Run automated package publishing', yargs => {
yargs.demandCommand(0, 0).usage(`Run automated package publishing
Usage:
semantic-release [options] [plugins]`);
})
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', default: undefined, type: 'boolean', group: 'Options'})
.option('verify-conditions', {...stringList, group: 'Plugins'})
.option('analyze-commits', {type: 'string', group: 'Plugins'})
.option('verify-release', {...stringList, group: 'Plugins'})
.option('generate-notes', {type: 'string', group: 'Plugins'})
.option('prepare', {...stringList, group: 'Plugins'})
.option('publish', {...stringList, group: 'Plugins'})
.option('success', {...stringList, group: 'Plugins'})
.option('fail', {...stringList, group: 'Plugins'})
.option('debug', {describe: 'Output debugging information', default: undefined, type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: undefined, type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', default: undefined, group: 'Options'})
.option('v', {alias: 'version', default: undefined, group: 'Options'})
.strict(false)
.exitProcess(false);
try {
if (program.args.length > 0) {
program.outputHelp();
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`)
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)));
// Remove option with undefined values, as yargs sets non defined options as `undefined`
const {help, version, ...opts} = pickBy(cli.argv, value => !isUndefined(value));
if (Boolean(help) || Boolean(version)) {
process.exitCode = 0;
return;
}
// Set the `noCi` options as yargs sets the `ci` options instead (because arg starts with `--no`)
if (opts.ci === false) {
opts.noCi = true;
}
if (opts.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
await require('.')(opts);
process.exitCode = 0;
} catch (err) {
if (err.name !== 'YError') {
console.error(err);
}
process.exitCode = 1;
}
};
+1 -1
View File
@@ -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
+9 -3
View File
@@ -5,8 +5,11 @@
- [@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
- [prepare](https://github.com/semantic-release/npm#prepare): Update the package.json version and create the npm package tarball
- [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry
## Official plugins
@@ -16,15 +19,18 @@
- [publish](https://github.com/semantic-release/gitlab#publish): Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
- [@semantic-release/git](https://github.com/semantic-release/git)
- [verifyConditions](https://github.com/semantic-release/git#verifyconditions): Verify the presence and the validity of the Git authentication and release configuration
- [publish](https://github.com/semantic-release/git#publish): Push a release commit and tag, including configurable files
- [prepare](https://github.com/semantic-release/git#prepare): Push a release commit and tag, including configurable files
- [@semantic-release/changelog](https://github.com/semantic-release/changelog)
- [verifyConditions](https://github.com/semantic-release/changelog#verifyconditions): Verify the presence and the validity of the configuration
- [publish](https://github.com/semantic-release/changelog#publish): Create or update the changelog file in the local project repository
- [prepare](https://github.com/semantic-release/changelog#prepare): Create or update the changelog file in the local project repository
- [@semantic-release/exec](https://github.com/semantic-release/exec)
- [verifyConditions](https://github.com/semantic-release/exec#verifyconditions): Execute a shell command to verify if the release should happen
- [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release
- [verifyRelease](https://github.com/semantic-release/exec#verifyrelease): Execute a shell command to verifying a release that was determined before and is about to be published.
- [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release.
- [prepare](https://github.com/semantic-release/exec#prepare): Execute a shell command to prepare the release
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release
- [success](https://github.com/semantic-release/exec#success): Execute a shell command to notify of a new release
- [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
## Community plugins
+65 -1
View File
@@ -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"
}
}
```
+3 -17
View File
@@ -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
@@ -47,12 +41,7 @@ publish:
image: node:8
stage: release
script:
# Only for a local semantic-release installation
- npm run semantic-release
# Only for a global semantic-release installation
- npm install -g semantic-release
- semantic-release
- npx semantic-release
```
### `package.json` configuration
@@ -63,9 +52,6 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
{
"devDependencies": {
"semantic-release": "^12.0.0"
},
"scripts": {
"semantic-release": "semantic-release"
}
}
```
+94
View File
@@ -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
```
+8 -44
View File
@@ -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
@@ -20,25 +20,16 @@ It's recommended to run the `semantic-release` command in the [Travis `deploy` s
**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).
**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.
```yaml
language: node_js
node_js: 8
script:
# Run tests
- npm run test
deploy:
provider: script
skip_cleanup: true
script:
# Only for a local semantic-release installation
- npm run semantic-release
# Only for a global semantic-release installation
- npm install -g semantic-release && semantic-release
- npx semantic-release
```
### `package.json` configuration for single Node job
@@ -49,9 +40,6 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
{
"devDependencies": {
"semantic-release": "^11.0.0"
},
"scripts": {
"semantic-release": "semantic-release"
}
}
```
@@ -64,40 +52,26 @@ 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.
**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).
**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.
```yaml
language: node_js
node_js:
- 8
- 6
- 4
os:
- linux
- osx
script:
# Run tests
- npm run test
deploy:
provider: script
skip_cleanup: true
script:
# Only for a local semantic-release installation
- npm run travis-deploy-once "npm run semantic-release"
# Only for a global semantic-release installation
- npm install -g travis-deploy-once semantic-release && travis-deploy-once "semantic-release"
- npx travis-deploy-once "npx semantic-release"
```
**Note**: See the `travis-deploy-once` [`pro`](https://github.com/semantic-release/travis-deploy-once#-p---pro) and [`travis-url`](https://github.com/semantic-release/travis-deploy-once#-u---travis-url) options for using with [Travis Pro](https://docs.travis-ci.com/user/travis-pro) and [Travis Enterprise](https://enterprise.travis-ci.com).
@@ -111,10 +85,6 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
"devDependencies": {
"semantic-release": "^12.0.0",
"travis-deploy-once": "^4.0.0"
},
"scripts": {
"semantic-release": "semantic-release",
"travis-deploy-once": "travis-deploy-once"
}
}
```
@@ -127,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).
@@ -146,20 +116,14 @@ 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/*
# Install travis-deploy-once and semantic-release
- npm install -g travis-deploy-once semantic-release
# Run semantic-release only on job, after all other are successful
- travis-deploy-once "semantic-release"
# Run semantic-release only on one job, after all other are successful
- npx travis-deploy-once "npx semantic-release"
```
**Note**: See the `travis-deploy-once` [`pro`](https://github.com/semantic-release/travis-deploy-once#-p---pro) and [`travis-url`](https://github.com/semantic-release/travis-deploy-once#-u---travis-url) options for using with [Travis Pro](https://docs.travis-ci.com/user/travis-pro) and [Travis Enterprise](https://enterprise.travis-ci.com).
+17 -3
View File
@@ -35,11 +35,19 @@ Alternatively you can use a [global](../usage/installation.md#global-installatio
If your CI environment provides [nvm](https://github.com/creationix/nvm) you can switch to Node 8 before installing and running the `semantic-release` command:
```bash
$ nvm install 8 && yarn install -g semantic-release && semantic-release
$ nvm install 8 && yarn global add semantic-release && semantic-release
```
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
As `semantic-release` is recommended to be executed with [`npx`](https://www.npmjs.com/package/npx) an alternative is required for usage with Yarn. Even though it is possible to install npx with Yarn, it's not recommended. Yarn and npx would be using different cache locations.
For [local installation](../usage/installation.md#local-installation) replace
`npx semantic-release` with `yarn run semantic-release`.
For [global installation](../usage/installation.md#global-installation) replace
`npx semantic-release` with `yarn global add semantic-release && semantic-release`.
## Can I use semantic-release to publish non-JavaScript packages?
Yes, **semantic-release** is a Node CLI application but it can be used to publish any type of packages.
@@ -64,7 +72,7 @@ See the [CI configuration recipes](../usage/ci-configuration.md) for more detail
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#options) option. You will also have to set the required [authentication](../usage/ci-configuration.md#authentication) via environment variables on your local machine, for example:
```bash
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npm run semantic-release --no-ci
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npx semantic-release --no-ci
```
However this is not the recommended approach, as running unit and integration tests on an independent machine before publishing software is a crucial part of the release workflow.
@@ -123,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.
@@ -156,3 +164,9 @@ In addition the [verify conditions step](../../README.md#release-steps) verifies
**semantic-release** is written using the latest [ECMAScript 2017](https://www.ecma-international.org/publications/standards/Ecma-262.htm) features, without transpilation which **requires Node version 8.3 or higher**.
See [Node version requirement](../support/node-version.md#node-version-requirement) for more details and solutions.
# What is npx?
[`npx`](https://www.npmjs.com/package/npx) short for "npm exec" is a CLI to find and execute npm binaries within the local `node_modules` folder or in the $PATH. If a binary can't be located npx will download the required package and execute it from its cache location.
The tool is bundled with [npm](https://www.npmjs.com/package/npm) >= 5.2, or can be installed via `npm install -g npx`.
For more details and motivation read the [introductory blog post](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) by [@zkat](https://github.com/zkat).
+4 -21
View File
@@ -18,35 +18,18 @@ See [CI configuration](../usage/ci-configuration.md) and [CI configuration recip
### Use `npx`
[`npx`](https://github.com/zkat/npx) is a CLI to execute npm binaries. It is bundled with [npm](https://www.npmjs.com/package/npm) >= 5.4, or can be installed via `npm install -g npx`.
`npx` can be used to download the latest [Node 8 package published on npm](https://www.npmjs.com/package/node) and use it to execute the `semantic-release` command.
If you are using a [local](../usage/installation.md#local-installation) **semantic-release** installation:
`npx` is included with npm >= 5.2 and can be used to download the latest [Node 8 package published on npm](https://www.npmjs.com/package/node). Use it to execute the `semantic-release` command.
```bash
$ npm install -g npx && npx -p node@8 -c "npm run semantic-release"
$ npx -p node@8 -c "npx semantic-release"
```
If you are using a [global](../usage/installation.md#global-installation) **semantic-release** installation:
```bash
# For global semantic-release install
$ npm install -g semantic-release npx && npx -p node@8 -c "semantic-release"
```
**Note:**: See [What is npx](./FAQ.md#what-is-npx) for more details.
### Use `nvm`
If your CI environment provides [nvm](https://github.com/creationix/nvm) you can use it to switch to Node 8 before running the `semantic-release` command.
If you are using a [local](../usage/installation.md#local-installation) **semantic-release** installation:
```bash
$ nvm install 8 && npm run semantic-release
```
If you are using a [global](../usage/installation.md#global-installation) **semantic-release** installation:
```bash
$ nvm install 8 && npm install -g semantic-release && semantic-release
$ nvm install 8 && npx semantic-release
```
+1 -1
View File
@@ -44,6 +44,6 @@ cd your-module
semantic-release-cli setup
```
![dialogue](media/semantic-release-cli.png)
![dialogue](../../media/semantic-release-cli.png)
See the [semantic-release-cli](https://github.com/semantic-release/cli#what-it-does) documentation for more details.
+49 -3
View File
@@ -59,11 +59,21 @@ 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)).
**Note**: If the [Github plugin](https://github.com/semantic-release/github) is used the URL must be a valid Github URL that include the `owner`, the `repository` name and the `host`. **The Github shorthand URL is not supported.**
### tagFormat
Type: `String`
Default: `v${version}`
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 exactly once and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
### dryRun
@@ -93,7 +103,7 @@ Default: `false`
CLI argument: `--debug`
Output debugging information. It can also be enabled by set the `DEBUG` environment variable to `semantic-release`.
Output debugging information. It can also be enabled by set the `DEBUG` environment variable to `semantic-release:*`.
### verifyConditions
@@ -143,6 +153,18 @@ Define the [generate notes plugin](plugins.md#generatenotes-plugin).
See [Plugins configuration](plugins.md#configuration) for more details.
### prepare
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/npm']`
CLI argument: `--prepare`
Define the list of [prepare plugins](plugins.md#prepare-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### publish
Type: `Array`, `String`, `Object`
@@ -154,3 +176,27 @@ CLI argument: `--publish`
Define the list of [publish plugins](plugins.md#publish-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### success
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/github']`
CLI argument: `--success`
Define the list of [success plugins](plugins.md#success-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### fail
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/github']`
CLI argument: `--fail`
Define the list of [fail plugins](plugins.md#fail-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
+10 -14
View File
@@ -2,32 +2,28 @@
## Local installation
For [Node modules projects](https://docs.npmjs.com/getting-started/creating-node-modules) we recommend to install **semantic-release** locally and to run the `semantic-release` command with a [npm script](https://docs.npmjs.com/misc/scripts):
For [Node modules projects](https://docs.npmjs.com/getting-started/creating-node-modules) we recommend installing **semantic-release** locally and running the `semantic-release` command with [npx](https://www.npmjs.com/package/npx):
```bash
$ npm install --save-dev semantic-release
```
In your `package.json`:
```json
"scripts": {
"semantic-release": "semantic-release"
}
```
Then in the CI environment:
```bash
$ npm run semantic-release
$ npx semantic-release
```
**Note:** `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently find the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
## Global installation
For other type of projects we recommend to install **semantic-release** globally directly in the CI environment:
For other type of projects we recommend installing **semantic-release** directly in the CI environment, also with [npx](https://www.npmjs.com/package/npx):
```bash
$ npm install -g semantic-release
$ semantic-release
$ npx semantic-release
```
**Note:**: For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npm install -g semantic-release@12`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
**Note:** For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npx semantic-release@12`, or `npm install -g semantic-release@12`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
**Note:** `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently install the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
+27 -7
View File
@@ -1,44 +1,64 @@
# Plugins
Each [release steps](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured, allowing to support different [commit message format](../../README.md#commit-message-format), release not generator and publishing platforms.
Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
## Plugin types
### verifyConditions plugin
Plugin responsible for verifying all the conditions to proceed with the release: configuration is correct, authentication token are valid, etc...
Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
Default implementation: [npm](https://github.com/semantic-release/npm#verifyconditions) and [github](https://github.com/semantic-release/github#verifyconditions).
### analyzeCommits plugin
Plugin responsible for determining the type of the next release (`major`, `minor` or `patch`).
Responsible for determining the type of the next release (`major`, `minor` or `patch`).
Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).
### verifyRelease plugin
Plugin responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published match certain expectations. For example the [cracks plugin](https://github.com/semantic-release/cracks) allows to verify that if a release contains breaking changes, its type must be `major`.
Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published match certain expectations. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
Default implementation: none.
### generateNotes plugin
Plugin responsible for generating release notes.
Responsible for generating release notes.
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
### prepare plugin
Responsible for preparing the release, including:
- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
- Create and push commits
Default implementation: [npm](https://github.com/semantic-release/npm#prepare).
### publish plugin
Plugin responsible for publishing the release.
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
Responsible for notifying of a new release.
Default implementation: [github](https://github.com/semantic-release/github#success).
### fail 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`.
Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along the other **semantic-release** [options](configuration.md#options) will apply to all plugins, while the one defined within the plugin `Object` will apply only to this specific plugin.
Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along with the other **semantic-release** [options](configuration.md#options) will apply to all plugins. Options defined within the plugin `Object` will apply to that specific plugin.
For example:
```json
+91 -39
View File
@@ -1,24 +1,31 @@
const {template, isPlainObject, castArray} = require('lodash');
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
const hookStd = require('hook-std');
const pkg = require('./package.json');
const hideSensitive = require('./lib/hide-sensitive');
const getConfig = require('./lib/get-config');
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');
const {unshallow, gitHead: getGitHead, tag, push} = 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.');
options.dryRun = true;
} else {
// When running on CI, prevent the `git` CLI to prompt for username/password. See #703.
process.env.GIT_ASKPASS = 'echo';
process.env.GIT_TERMINAL_PROMPT = 0;
}
if (isCi && isPr && !options.noCi) {
@@ -26,19 +33,26 @@ async function run(opts) {
return;
}
if (!await verify(options, branch, logger)) {
return;
if (branch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version wont be published.`
);
return false;
}
await verify(options);
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();
const lastRelease = await getLastRelease(logger);
const lastRelease = await getLastRelease(options.tagFormat, logger);
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
logger.log('Call plugin %s', 'analyze-commits');
@@ -53,68 +67,106 @@ async function run(opts) {
return;
}
const version = getNextVersion(type, lastRelease, logger);
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
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 {
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
logger.log('Call plugin %s', 'prepare');
await plugins.prepare(
{options, logger, lastRelease, commits, nextRelease},
{
getNextInput: async lastResult => {
const newGitHead = await getGitHead();
// If previous prepare plugin has created a commit (gitHead changed)
if (lastResult.nextRelease.gitHead !== newGitHead) {
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};
},
}
);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
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},
// Add nextRelease and plugin properties to published release
{transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step})}
);
await plugins.success(
{options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
{settleAll: true}
);
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};
});
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 => {
logger.log(`Running %s version %s`, pkg.name, pkg.version);
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;
}
+118
View File
@@ -0,0 +1,118 @@
const url = require('url');
const {inspect} = require('util');
const {toLower, isString} = require('lodash');
const pkg = require('../../package.json');
const RELEASE_TYPE = require('./release-types');
const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
const linkify = file => `${homepage}/blob/caribou/${file}`;
module.exports = {
ENOGITREPO: () => ({
message: 'Not running from a git repository.',
details: `The \`semantic-release\` command must be executed from a Git repository.
The current working directory is \`${process.cwd()}\`.
Please verify your CI configuration to make sure the \`semantic-release\` command is executed from the root of the cloned repository.`,
}),
ENOREPOURL: () => ({
message: 'The `repositoryUrl` option is required.',
details: `The [repositoryUrl option](${linkify(
'docs/usage/configuration.md#repositoryurl'
)}) cannot be determined from the semantic-release configuration, the \`package.json\` nor the [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes).
Please make sure to add the \`repositoryUrl\` to the [semantic-release configuration] (${linkify(
'docs/usage/configuration.md'
)}).`,
}),
EGITNOPERMISSION: ({options}) => ({
message: 'The push permission to the Git repository is required.',
details: `**semantic-release** cannot push the version tag to the branch \`${
options.branch
}\` on remote Git repository.
Please refer to the [authentication configuration documentation](${linkify(
'docs/usage/ci-configuration.md#authentication'
)}) to configure the Git credentials on your CI environment.`,
}),
EINVALIDTAGFORMAT: ({tagFormat}) => ({
message: 'Invalid `tagFormat` option.',
details: `The [tagFormat](${linkify(
'docs/usage/configuration.md#tagformat'
)}) must compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
}),
ETAGNOVERSION: ({tagFormat}) => ({
message: 'Invalid `tagFormat` option.',
details: `The [tagFormat](${linkify(
'docs/usage/configuration.md#tagformat'
)}) option must contain the variable \`version\` exactly once.
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
}),
EPLUGINCONF: ({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'
)})`,
}),
};
+67
View File
@@ -0,0 +1,67 @@
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',
},
},
prepare: {
default: ['@semantic-release/npm'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
},
publish: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
output: {
validator: output => !output || isPlainObject(output),
error: 'EPUBLISHOUTPUT',
},
},
success: {
default: ['@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
},
fail: {
default: ['@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
},
};
+1
View File
@@ -0,0 +1 @@
module.exports = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
+8 -20
View File
@@ -1,11 +1,10 @@
const readPkgUp = require('read-pkg-up');
const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = require('lodash');
const readPkgUp = require('read-pkg-up');
const cosmiconfig = require('cosmiconfig');
const resolveFrom = require('resolve-from');
const SemanticReleaseError = require('@semantic-release/error');
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');
@@ -26,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))
@@ -47,30 +46,19 @@ module.exports = async (opts, logger) => {
options = {
branch: 'master',
repositoryUrl: (await pkgRepoUrl()) || (await repoUrl()),
tagFormat: `v\${version}`,
// Remove `null` and `undefined` options so they can be replaced with default ones
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
};
debug('options values: %O', Object.keys(options));
debug('name: %O', options.name);
debug('branch: %O', options.branch);
debug('repositoryUrl: %O', options.repositoryUrl);
debug('analyzeCommits: %O', options.analyzeCommits);
debug('generateNotes: %O', options.generateNotes);
debug('verifyConditions: %O', options.verifyConditions);
debug('verifyRelease: %O', options.verifyRelease);
debug('publish: %O', options.publish);
options.repositoryUrl = await getGitAuthUrl(options);
if (!options.repositoryUrl) {
throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
}
options.repositoryUrl = getGitAuthUrl(options.repositoryUrl);
debug('options values: %O', options);
return {options, plugins: await plugins(options, pluginsPath, logger)};
};
async function pkgRepoUrl() {
const {pkg} = await readPkgUp();
return pkg && pkg.repository ? pkg.repository.url : undefined;
const {pkg} = await readPkgUp({normalize: false});
return pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
}
+7
View File
@@ -0,0 +1,7 @@
const SemanticReleaseError = require('@semantic-release/error');
const ERROR_DEFINITIONS = require('./definitions/errors');
module.exports = (code, ctx = {}) => {
const {message, details} = ERROR_DEFINITIONS[code](ctx);
return new SemanticReleaseError(message, code, details);
};
+41 -16
View File
@@ -1,29 +1,54 @@
const {parse, format} = require('url');
const {isUndefined} = require('lodash');
const gitUrlParse = require('git-url-parse');
const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
const GIT_TOKENS = ['GIT_CREDENTIALS', 'GH_TOKEN', 'GITHUB_TOKEN', 'GL_TOKEN', 'GITLAB_TOKEN'];
/**
* Generate the git repository URL with creadentials.
* If the `gitCredentials` is defined, returns a http or https URL with Basic Authentication (`https://username:passowrd@hostname:port/path.git`).
* If the `gitCredentials` is undefined, returns the `repositoryUrl`. In that case it's expected for the user to have setup the Git authentication on the CI (for example via SSH keys).
* Determine the the git repository URL to use to push, either:
* - The `repositoryUrl` as is if allowed to push
* - The `repositoryUrl` converted to `https` or `http` with Basic Authentication
*
* @param {String} gitCredentials Basic HTTP Authentication credentials, can be `username:password` or a token for certain Git providers.
* @param {String} repositoryUrl The git repository URL.
* In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
*
* @param {String} repositoryUrl The user provided Git repository URL.
* @return {String} The formatted Git repository URL.
*/
module.exports = repositoryUrl => {
const envVar = GIT_TOKENS.find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = ['GL_TOKEN', 'GITLAB_TOKEN'].includes(envVar)
? `gitlab-ci-token:${process.env[envVar]}`
: process.env[envVar];
module.exports = async ({repositoryUrl, branch}) => {
if (repositoryUrl) {
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
if (!gitCredentials) {
return repositoryUrl;
if (info && info.getDefaultRepresentation() === 'shortcut') {
// Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`)
repositoryUrl = info.https();
} else {
const {protocols} = gitUrlParse(repositoryUrl);
// Replace `git+https` and `git+http` with `https` or `http`
if (protocols.includes('http') || protocols.includes('https')) {
repositoryUrl = format({
...parse(repositoryUrl),
...{protocol: protocols.includes('https') ? 'https' : 'http'},
});
}
}
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
if (!await verifyAuth(repositoryUrl, branch)) {
const envVar = GIT_TOKENS.find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = ['GL_TOKEN', 'GITLAB_TOKEN'].includes(envVar)
? `gitlab-ci-token:${process.env[envVar]}`
: process.env[envVar];
const {protocols} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
// If credentials are set via anvironment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
return gitCredentials
? format({...parse(`${gitUrlParse(repositoryUrl).toString(protocol)}.git`), ...{auth: gitCredentials}})
: repositoryUrl;
}
}
const {protocols} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
return format({...parse(`${gitUrlParse(repositoryUrl).toString(protocol)}.git`), ...{auth: gitCredentials}});
return repositoryUrl;
};
+26 -8
View File
@@ -1,3 +1,4 @@
const {escapeRegExp, template} = require('lodash');
const semver = require('semver');
const pLocate = require('p-locate');
const debug = require('debug')('semantic-release:get-last-release');
@@ -15,21 +16,38 @@ const {gitTags, isRefInHistory, gitTagHead} = require('./git');
* Determine the Git tag and version of the last tagged release.
*
* - Obtain all the tags referencing commits in the current branch history
* - Filter out the ones that are not valid semantic version
* - Sort the tags
* - Retrive the highest tag
* - Filter out the ones that are not valid semantic version or doesn't match the `tagFormat`
* - Sort the versions
* - Retrive the highest version
*
* @param {Object} logger Global logger.
* @return {Promise<LastRelease>} The last tagged release or `undefined` if none is found.
*/
module.exports = async logger => {
const tags = (await gitTags()).filter(tag => semver.valid(semver.clean(tag))).sort(semver.rcompare);
module.exports = async (tagFormat, logger) => {
// Generate a regex to parse tags formatted with `tagFormat`
// by replacing the `version` variable in the template by `(.+)`.
// The `tagFormat` is compiled with space as the `version` as it's an invalid tag character,
// so it's guaranteed to no be present in the `tagFormat`.
const tagRegexp = escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)');
const tags = (await gitTags())
.map(tag => {
return {gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]};
})
.filter(
tag => tag.version && semver.valid(semver.clean(tag.version)) && !semver.prerelease(semver.clean(tag.version))
)
.sort((a, b) => semver.rcompare(a.version, b.version));
debug('found tags: %o', tags);
if (tags.length > 0) {
const gitTag = await pLocate(tags, tag => isRefInHistory(tag), {concurrency: 1, preserveOrder: true});
logger.log('Found git tag version %s', gitTag);
return {gitTag, gitHead: await gitTagHead(gitTag), version: semver.valid(semver.clean(gitTag))};
const {gitTag, version} = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {
concurrency: 1,
preserveOrder: true,
});
logger.log('Found git tag %s associated with version %s', gitTag, version);
return {gitHead: await gitTagHead(gitTag), gitTag, version};
}
logger.log('No git tag version found');
+47 -27
View File
@@ -1,5 +1,8 @@
const execa = require('execa');
const debug = require('debug')('semantic-release:get-version-head');
const debug = require('debug')('semantic-release:git');
const envCi = require('env-ci');
const timeout = envCi().isCi ? 10000 : 0;
/**
* Get the commit sha for a given tag.
@@ -9,7 +12,11 @@ const debug = require('debug')('semantic-release:get-version-head');
* @return {string} The commit sha of the tag in parameter or `null`.
*/
async function gitTagHead(tagName) {
return execa.stdout('git', ['rev-list', '-1', tagName], {reject: false});
try {
return await execa.stdout('git', ['rev-list', '-1', tagName], {timeout});
} catch (err) {
debug(err);
}
}
/**
@@ -17,7 +24,7 @@ async function gitTagHead(tagName) {
* @throws {Error} If the `git` command fails.
*/
async function gitTags() {
return (await execa.stdout('git', ['tag']))
return (await execa.stdout('git', ['tag'], {timeout}))
.split('\n')
.map(tag => tag.trim())
.filter(tag => Boolean(tag));
@@ -28,38 +35,50 @@ async function gitTags() {
*
* @param {string} ref The reference to look for.
*
* @return {boolean} `true` if the reference is in the history of the current branch, `false` otherwise.
* @return {boolean} `true` if the reference is in the history of the current branch, falsy otherwise.
*/
async function isRefInHistory(ref) {
return (await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], {reject: false})).code === 0;
try {
return (await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], {timeout})).code === 0;
} catch (err) {
debug(err);
}
}
/**
* Unshallow the git repository (retriving every commits and tags).
*/
async function unshallow() {
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false, timeout});
}
/**
* @return {string} the sha of the HEAD commit.
*/
async function gitHead() {
return execa.stdout('git', ['rev-parse', 'HEAD']);
return execa.stdout('git', ['rev-parse', 'HEAD'], {timeout});
}
/**
* @return {string} The value of the remote git URL.
*/
async function repoUrl() {
return execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false});
try {
return await execa.stdout('git', ['remote', 'get-url', 'origin'], {timeout});
} catch (err) {
debug(err);
}
}
/**
* @return {Boolean} `true` if the current working directory is in a git repository, `false` otherwise.
* @return {Boolean} `true` if the current working directory is in a git repository, falsy otherwise.
*/
async function isGitRepo() {
return (await execa('git', ['rev-parse', '--git-dir'], {reject: false})).code === 0;
try {
return (await execa('git', ['rev-parse', '--git-dir'], {timeout})).code === 0;
} catch (err) {
debug(err);
}
}
/**
@@ -68,10 +87,14 @@ async function isGitRepo() {
* @param {String} origin The remote repository URL.
* @param {String} branch The repositoru branch for which to verify write access.
*
* @return {Boolean} `true` is authorized to push, `false` otherwise.
* @return {Boolean} `true` is authorized to push, falsy otherwise.
*/
async function verifyAuth(origin, branch) {
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`], {reject: false})).code === 0;
try {
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`], {timeout})).code === 0;
} catch (err) {
debug(err);
}
}
/**
@@ -81,7 +104,7 @@ async function verifyAuth(origin, branch) {
* @throws {Error} if the tag creation failed.
*/
async function tag(tagName) {
await execa('git', ['tag', tagName]);
await execa('git', ['tag', tagName], {timeout});
}
/**
@@ -92,24 +115,21 @@ async function tag(tagName) {
* @throws {Error} if the push failed.
*/
async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`], {timeout});
}
/**
* Delete a tag locally and remotely.
* Verify a tag name is a valid Git reference.
*
* @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.
* @param {string} tagName the tag name to verify.
* @return {boolean} `true` if valid, falsy otherwise.
*/
async function deleteTag(origin, tagName) {
// Delete the local tag
let shell = await execa('git', ['tag', '-d', tagName], {reject: false});
debug('delete local tag', shell);
// Delete the tag remotely
shell = await execa('git', ['push', '-d', origin, tagName], {reject: false});
debug('delete remote tag', shell);
async function verifyTagName(tagName) {
try {
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], {timeout})).code === 0;
} catch (err) {
debug(err);
}
}
module.exports = {
@@ -123,5 +143,5 @@ module.exports = {
verifyAuth,
tag,
push,
deleteTag,
verifyTagName,
};
+4 -1
View File
@@ -1,6 +1,9 @@
const {escapeRegExp} = require('lodash');
const toReplace = Object.keys(process.env).filter(envVar => /token|password|credential|secret|private/i.test(envVar));
const toReplace = Object.keys(process.env).filter(
envVar => /token|password|credential|secret|private/i.test(envVar) && process.env[envVar].trim()
);
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(process.env[envVar])).join('|'), 'g');
module.exports = output => {
-55
View File
@@ -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.',
},
},
};
+23 -14
View File
@@ -1,31 +1,40 @@
const {isArray, isObject, omit} = require('lodash');
const SemanticReleaseError = require('@semantic-release/error');
const PLUGINS_DEFINITION = require('./definitions');
const {isArray, isObject, omit, castArray, isUndefined} = require('lodash');
const AggregateError = require('aggregate-error');
const getError = require('../get-error');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
const pipeline = require('./pipeline');
const normalize = require('./normalize');
module.exports = (options, pluginsPath, logger) =>
Object.keys(PLUGINS_DEFINITION).reduce((plugins, pluginType) => {
const {config, output, default: def} = PLUGINS_DEFINITION[pluginType];
module.exports = (options, pluginsPath, logger) => {
const errors = [];
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])) {
throw 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;
}, {});
if (errors.length > 0) {
throw new AggregateError(errors);
}
return plugins;
};
+26 -17
View File
@@ -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
View File
@@ -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;
};
+7
View File
@@ -0,0 +1,7 @@
const {isFunction} = require('lodash');
function extractErrors(err) {
return err && isFunction(err[Symbol.iterator]) ? [...err] : [err];
}
module.exports = {extractErrors};
+23 -18
View File
@@ -1,27 +1,32 @@
const SemanticReleaseError = require('@semantic-release/error');
const {isGitRepo, verifyAuth} = require('./git');
const {template} = require('lodash');
const AggregateError = require('aggregate-error');
const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
const getError = require('./get-error');
module.exports = async options => {
const errors = [];
module.exports = async (options, branch, logger) => {
if (!await isGitRepo()) {
logger.error('Semantic-release must run from a git repository.');
return false;
errors.push(getError('ENOGITREPO'));
} else if (!options.repositoryUrl) {
errors.push(getError('ENOREPOURL'));
} else if (!await verifyAuth(options.repositoryUrl, options.branch)) {
errors.push(getError('EGITNOPERMISSION', {options}));
}
if (!await verifyAuth(options.repositoryUrl, options.branch)) {
throw new SemanticReleaseError(
`The git credentials doesn't allow to push on the branch ${options.branch}.`,
'EGITNOPERMISSION'
);
// Verify that compiling the `tagFormat` produce a valid Git tag
if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) {
errors.push(getError('EINVALIDTAGFORMAT', {tagFormat: options.tagFormat}));
}
if (branch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version wont be published.`
);
return false;
// 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(getError('ETAGNOVERSION', {tagFormat: options.tagFormat}));
}
return true;
if (errors.length > 0) {
throw new AggregateError(errors);
}
};
+13 -26
View File
@@ -20,30 +20,30 @@
],
"dependencies": {
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/error": "^2.1.0",
"@semantic-release/github": "^4.0.0",
"@semantic-release/npm": "^3.0.0",
"@semantic-release/error": "^2.2.0",
"@semantic-release/github": "^4.1.0",
"@semantic-release/npm": "^3.2.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"aggregate-error": "^1.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"cosmiconfig": "^4.0.0",
"debug": "^3.1.0",
"env-ci": "^1.0.0",
"execa": "^0.9.0",
"execa": "^0.10.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"git-url-parse": "^8.0.0",
"git-url-parse": "^8.1.0",
"hook-std": "^0.4.0",
"hosted-git-info": "^2.6.0",
"lodash": "^4.17.4",
"marked": "^0.3.9",
"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"
"semver": "^5.4.1",
"yargs": "^11.0.0"
},
"devDependencies": {
"ava": "^0.25.0",
@@ -53,8 +53,6 @@
"cz-conventional-changelog": "^2.0.0",
"delay": "^2.0.0",
"dockerode": "^2.5.2",
"eslint-config-prettier": "^2.5.0",
"eslint-plugin-prettier": "^2.3.0",
"file-url": "^2.0.2",
"fs-extra": "^5.0.0",
"got": "^8.0.0",
@@ -63,11 +61,10 @@
"nock": "^9.0.2",
"nyc": "^11.2.1",
"p-retry": "^1.0.0",
"prettier": "~1.10.0",
"proxyquire": "^1.8.0",
"proxyquire": "^2.0.0",
"sinon": "^4.0.0",
"tempy": "^0.2.1",
"xo": "^0.18.2"
"xo": "^0.20.0"
},
"engines": {
"node": ">=8.3"
@@ -107,10 +104,7 @@
"all": true
},
"prettier": {
"printWidth": 120,
"singleQuote": true,
"bracketSpacing": false,
"trailingComma": "es5"
"printWidth": 120
},
"publishConfig": {
"tag": "next"
@@ -131,14 +125,7 @@
"test": "nyc ava -v"
},
"xo": {
"extends": [
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
"prettier": true,
"space": true
}
}
+237
View File
@@ -0,0 +1,237 @@
import test from 'ava';
import proxyquire from 'proxyquire';
import clearModule from 'clear-module';
import {stub} from 'sinon';
// Save the current process.env and process.argv
const envBackup = Object.assign({}, process.env);
const argvBackup = Object.assign({}, process.argv);
test.beforeEach(t => {
clearModule('yargs');
t.context.logs = '';
t.context.errors = '';
t.context.stdout = stub(process.stdout, 'write').callsFake(val => {
t.context.logs += val.toString();
});
t.context.stderr = stub(process.stderr, 'write').callsFake(val => {
t.context.errors += val.toString();
});
});
test.afterEach.always(t => {
process.env = envBackup;
process.argv = argvBackup;
t.context.stdout.restore();
t.context.stderr.restore();
delete process.exitCode;
});
test.serial('Pass options to semantic-release API', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
'',
'',
'-b',
'master',
'-r',
'https://github/com/owner/repo.git',
'-t',
`v\${version}`,
'-e',
'config1',
'config2',
'--verify-conditions',
'condition1',
'condition2',
'--analyze-commits',
'analyze',
'--verify-release',
'verify1',
'verify2',
'--generate-notes',
'notes',
'--prepare',
'prepare1',
'prepare2',
'--publish',
'publish1',
'publish2',
'--success',
'success1',
'success2',
'--fail',
'fail1',
'fail2',
'--debug',
'-d',
];
await cli();
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
t.is(run.args[0][0].analyzeCommits, 'analyze');
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']);
t.is(run.args[0][0].generateNotes, 'notes');
t.deepEqual(run.args[0][0].prepare, ['prepare1', 'prepare2']);
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
t.deepEqual(run.args[0][0].success, ['success1', 'success2']);
t.deepEqual(run.args[0][0].fail, ['fail1', 'fail2']);
t.is(run.args[0][0].debug, true);
t.is(run.args[0][0].dryRun, true);
t.is(process.exitCode, 0);
});
test.serial('Pass options to semantic-release API with alias arguments', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
'',
'',
'--branch',
'master',
'--repository-url',
'https://github/com/owner/repo.git',
'--tag-format',
`v\${version}`,
'--extends',
'config1',
'config2',
'--dry-run',
];
await cli();
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.is(run.args[0][0].dryRun, true);
t.is(process.exitCode, 0);
});
test.serial('Pass unknown options to semantic-release API', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
'',
'',
'--bool',
'--first-option',
'value1',
'--second-option',
'value2',
'--second-option',
'value3',
];
await cli();
t.is(run.args[0][0].bool, true);
t.is(run.args[0][0].firstOption, 'value1');
t.deepEqual(run.args[0][0].secondOption, ['value2', 'value3']);
t.is(process.exitCode, 0);
});
test.serial('Pass empty Array to semantic-release API for list option set to "false"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--publish', 'false'];
await cli();
t.deepEqual(run.args[0][0].publish, []);
t.is(process.exitCode, 0);
});
test.serial('Do not set properties in option for which arg is not in command line', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '-b', 'master'];
await cli();
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'ci'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'd'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'dry-run'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'debug'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'r'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 't'));
});
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--no-ci'];
await cli();
t.is(run.args[0][0].noCi, true);
t.is(process.exitCode, 0);
});
test.serial('Display help', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--help'];
await cli();
t.regex(t.context.logs, /Run automated package publishing/);
t.is(process.exitCode, 0);
});
test.serial('Returns error code and prints help if called with a command', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', 'pre'];
await cli();
t.regex(t.context.errors, /Run automated package publishing/);
t.regex(t.context.errors, /Too many non-option arguments/);
t.is(process.exitCode, 1);
});
test.serial('Return error code if multiple plugin are set for single plugin', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--analyze-commits', 'analyze1', 'analyze2'];
await cli();
t.regex(t.context.errors, /Run automated package publishing/);
t.regex(t.context.errors, /Too many non-option arguments/);
t.is(process.exitCode, 1);
});
test.serial('Return error code if semantic-release throw error', async t => {
const run = stub().rejects(new Error('semantic-release error'));
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', ''];
await cli();
t.regex(t.context.errors, /semantic-release error/);
t.is(process.exitCode, 1);
});
+133
View File
@@ -0,0 +1,133 @@
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 "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
t.false(plugins.publish.config.validator({}));
t.false(plugins.publish.config.validator({path: null}));
t.false(plugins.publish.config.validator());
t.true(plugins.publish.config.validator({path: 'plugin-path.js'}));
t.true(plugins.publish.config.validator('plugin-path.js'));
t.true(plugins.publish.config.validator(() => {}));
t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.success.config.validator({}));
t.false(plugins.success.config.validator({path: null}));
t.true(plugins.success.config.validator({path: 'plugin-path.js'}));
t.true(plugins.success.config.validator());
t.true(plugins.success.config.validator('plugin-path.js'));
t.true(plugins.success.config.validator(() => {}));
t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.fail.config.validator({}));
t.false(plugins.fail.config.validator({path: null}));
t.true(plugins.fail.config.validator({path: 'plugin-path.js'}));
t.true(plugins.fail.config.validator());
t.true(plugins.fail.config.validator('plugin-path.js'));
t.true(plugins.fail.config.validator(() => {}));
t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
t.false(plugins.analyzeCommits.output.validator('invalid'));
t.false(plugins.analyzeCommits.output.validator(1));
t.false(plugins.analyzeCommits.output.validator({}));
t.true(plugins.analyzeCommits.output.validator());
t.true(plugins.analyzeCommits.output.validator(null));
t.true(plugins.analyzeCommits.output.validator('major'));
});
test('The "generateNotes" plugin output, if defined, must be a string', t => {
t.false(plugins.generateNotes.output.validator(1));
t.false(plugins.generateNotes.output.validator({}));
t.true(plugins.generateNotes.output.validator());
t.true(plugins.generateNotes.output.validator(null));
t.true(plugins.generateNotes.output.validator(''));
t.true(plugins.generateNotes.output.validator('string'));
});
test('The "publish" plugin output, if defined, must be an object', t => {
t.false(plugins.publish.output.validator(1));
t.false(plugins.publish.output.validator('string'));
t.true(plugins.publish.output.validator({}));
t.true(plugins.publish.output.validator());
t.true(plugins.publish.output.validator(null));
t.true(plugins.publish.output.validator(''));
});
test('The "analyzeCommits" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error));
});
test('The "generateNotes" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.generateNotes.output.error));
});
test('The "publish" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.publish.output.error));
});
+5
View File
@@ -0,0 +1,5 @@
const AggregateError = require('aggregate-error');
module.exports = () => {
throw new AggregateError([new Error('a'), new Error('b')]);
};
+1
View File
@@ -0,0 +1 @@
module.exports = (pluginConfig, options) => options;
+89 -22
View File
@@ -13,12 +13,12 @@ const envBackup = Object.assign({}, process.env);
const cwd = process.cwd();
test.beforeEach(t => {
// Delete environment variables that could have been set on the machine running the tests
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
// Delete environment variables that could have been set on the machine running the tests
t.context.plugins = stub().returns({});
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
});
@@ -31,12 +31,13 @@ test.afterEach.always(() => {
});
test.serial('Default values, reading repositoryUrl from package.json', async t => {
const pkg = {repository: 'git@package.com:owner/module.git'};
process.env.GIT_CREDENTIALS = 'user:pass';
const pkg = {repository: 'https://host.null/owner/package.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
await gitAddConfig('remote.origin.url', 'git@host.null:owner/repo.git');
// Create package.json in repository root
await outputJson('./package.json', pkg);
@@ -44,24 +45,28 @@ test.serial('Default values, reading repositoryUrl from package.json', async t =
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@package.com:owner/module.git');
t.is(options.repositoryUrl, 'https://user:pass@host.null/owner/package.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
await gitAddConfig('remote.origin.url', 'https://host.null/owner/module.git');
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
t.is(options.repositoryUrl, 'https://user:pass@host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
const pkg = {repository: 'git+https://hostname.com/owner/module.git'};
process.env.GIT_CREDENTIALS = 'user:pass';
const pkg = {repository: 'https://host.null/owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
@@ -71,7 +76,56 @@ test.serial('Default values, reading repositoryUrl (http url) from package.json
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, pkg.repository);
t.is(options.repositoryUrl, 'https://user:pass@host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial('Default values, reading repositoryUrl (shorthand url) from package.json if not set in repo', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
const pkg = {repository: 'owner/module'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://user:pass@github.com/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
test.serial(
'Default values, reading repositoryUrl (gitlab shorthand url) from package.json if not set in repo',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
const pkg = {repository: 'gitlab:owner/module'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'https://user:pass@gitlab.com/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
}
);
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
const pkg = {repository: repositoryUrl};
// Create package.json in repository root
await outputJson('./package.json', pkg);
const {options} = await t.context.getConfig();
t.is(options.repositoryUrl, repositoryUrl);
});
test.serial('Read options from package.json', async t => {
@@ -79,7 +133,8 @@ test.serial('Read options from package.json', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -99,7 +154,8 @@ test.serial('Read options from .releaserc.yml', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -119,7 +175,8 @@ test.serial('Read options from .releaserc.json', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -139,7 +196,8 @@ test.serial('Read options from .releaserc.js', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -159,7 +217,8 @@ test.serial('Read options from release.config.js', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -184,8 +243,9 @@ test.serial('Prioritise CLI/API parameters over file configuration and git repo'
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_cli'},
branch: 'branch_cli',
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
};
const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
const pkg = {release, repository: 'git@host.null:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
await gitCommits(['First']);
@@ -208,7 +268,8 @@ test.serial('Read configuration from file path in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -235,7 +296,8 @@ test.serial('Read configuration from module path in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -262,7 +324,7 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
verifyRelease: 'verifyRelease1',
analyzeCommits: {path: 'analyzeCommits1', param: 'analyzeCommits_param1'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
@@ -270,6 +332,7 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
generateNotes: 'generateNotes2',
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
branch: 'test_branch',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -306,7 +369,8 @@ test.serial('Prioritize configuration from config file over "extends"', async t
generateNotes: 'generateNotesShareable',
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -333,7 +397,7 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
extends: './shareable2.json',
branch: 'branch_opts',
publish: [{path: 'publishOpts', param: 'publishOpts_param'}],
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const release = {
extends: './shareable1.json',
@@ -346,12 +410,13 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
generateNotes: 'generateNotesShareable1',
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
branch: 'test_branch1',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
analyzeCommits: 'analyzeCommits2',
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
branch: 'test_branch2',
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -374,11 +439,12 @@ test.serial('Allow to unset properties defined in shareable config with "null"',
extends: './shareable.json',
analyzeCommits: null,
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
@@ -407,11 +473,12 @@ test.serial('Allow to unset properties defined in shareable config with "undefin
extends: './shareable.json',
analyzeCommits: undefined,
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
};
// Create a git repository, set the current working directory at the root of the repo
+117 -30
View File
@@ -1,70 +1,157 @@
import test from 'ava';
import getAuthUrl from '../lib/get-git-auth-url';
import {gitRepo} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(() => {
// Restore process.env
process.env = {};
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Return the same "repositoryUrl" is no "gitCredentials" is defined', t => {
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'git@host.com:owner/repo.git');
test.serial('Return the same "git" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'git@host.null:owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('https://host.com/owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
});
test.serial('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('http://host.com/owner/repo.git'), 'http://user:pass@host.com/owner/repo.git');
test.serial('Return the same "https" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}), 'https://host.null/owner/repo.git');
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
t => {
'Return the "https" formatted URL if "gitCredentials" is not defined and repositoryUrl is a "git+https" URL',
async t => {
t.is(await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}), 'https://host.null/owner/repo.git');
}
);
test.serial('Handle "https" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
'https://host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
'git@host.null:group/subgroup/owner/repo.git'
);
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git+https://host.com/owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+http" URL',
t => {
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git+http://host.com/owner/repo.git'), 'http://user:pass@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git+http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', async t => {
process.env.GH_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://token@host.com/owner/repo.git');
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async t => {
process.env.GITHUB_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://token@host.com/owner/repo.git');
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async t => {
process.env.GL_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://gitlab-ci-token:token@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async t => {
process.env.GITLAB_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://gitlab-ci-token:token@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
t.is(await getAuthUrl({repositoryUrl}), repositoryUrl);
});
+56 -5
View File
@@ -17,7 +17,7 @@ test.afterEach.always(() => {
process.chdir(cwd);
});
test.serial('Get the highest valid tag', async t => {
test.serial('Get the highest non-prerelease valid tag', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create some commits and tags
@@ -29,11 +29,13 @@ test.serial('Get the highest valid tag', async t => {
await gitTagVersion('v1.0.0');
await gitCommits(['Fourth']);
await gitTagVersion('v3.0');
await gitCommits(['Fifth']);
await gitTagVersion('v3.0.0-beta.1');
const result = await getLastRelease(t.context.logger);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
t.deepEqual(t.context.log.args[0], ['Found git tag version %s', 'v2.0.0']);
t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']);
});
test.serial('Get the highest tag in the history of the current branch', async t => {
@@ -55,7 +57,7 @@ test.serial('Get the highest tag in the history of the current branch', async t
// Create the tag corresponding to version 2.0.0
await gitTagVersion('v2.0.0');
const result = await getLastRelease(t.context.logger);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
});
@@ -71,8 +73,57 @@ test.serial('Return empty object if no valid tag is found', async t => {
await gitCommits(['Third']);
await gitTagVersion('v3.0');
const result = await getLastRelease(t.context.logger);
const result = await getLastRelease(`v\${version}`, t.context.logger);
t.deepEqual(result, {});
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Get the highest valid tag corresponding to the "tagFormat"', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create some commits and tags
const [{hash: gitHead}] = await gitCommits(['First']);
await gitTagVersion('1.0.0');
t.deepEqual(await getLastRelease(`\${version}`, t.context.logger), {
gitHead,
gitTag: '1.0.0',
version: '1.0.0',
});
await gitTagVersion('foo-1.0.0-bar');
t.deepEqual(await getLastRelease(`foo-\${version}-bar`, t.context.logger), {
gitHead,
gitTag: 'foo-1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('foo-v1.0.0-bar');
t.deepEqual(await getLastRelease(`foo-v\${version}-bar`, t.context.logger), {
gitHead,
gitTag: 'foo-v1.0.0-bar',
version: '1.0.0',
});
await gitTagVersion('(.+)/1.0.0/(a-z)');
t.deepEqual(await getLastRelease(`(.+)/\${version}/(a-z)`, t.context.logger), {
gitHead,
gitTag: '(.+)/1.0.0/(a-z)',
version: '1.0.0',
});
await gitTagVersion('2.0.0-1.0.0-bar.1');
t.deepEqual(await getLastRelease(`2.0.0-\${version}-bar.1`, t.context.logger), {
gitHead,
gitTag: '2.0.0-1.0.0-bar.1',
version: '1.0.0',
});
await gitTagVersion('3.0.0-bar.1');
t.deepEqual(await getLastRelease(`\${version}-bar.1`, t.context.logger), {
gitHead,
gitTag: '3.0.0-bar.1',
version: '3.0.0',
});
});
+18 -16
View File
@@ -10,7 +10,7 @@ import {
push,
gitTags,
isGitRepo,
deleteTag,
verifyTagName,
} from '../lib/git';
import {
gitRepo,
@@ -87,7 +87,7 @@ test.serial('Verify if the commit `sha` is in the direct history of the current
await gitCheckout('master', false);
t.true(await isRefInHistory(commits[0].hash));
t.false(await isRefInHistory(otherCommits[0].hash));
t.falsy(await isRefInHistory(otherCommits[0].hash));
});
test.serial('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
@@ -138,18 +138,6 @@ test.serial('Add tag on head commit', async t => {
await t.is(await gitCommitTag(commits[0].hash), 'tag_name');
});
test.serial('Delete a tag', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
await gitCommits(['Test commit']);
await tag('tag_name');
await push(repo, 'master');
await deleteTag(repo, 'tag_name');
t.falsy(await gitTagHead('tag_name'));
t.falsy(await gitRemoteTagHead(repo, 'tag_name'));
});
test.serial('Push tag and commit to remote repository', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
@@ -168,11 +156,25 @@ test.serial('Return "true" if in a Git repository', async t => {
t.true(await isGitRepo());
});
test.serial('Return "false" if not in a Git repository', async t => {
test.serial('Return falsy if not in a Git repository', async t => {
const dir = tempy.directory();
process.chdir(dir);
t.false(await isGitRepo());
t.falsy(await isGitRepo());
});
test.serial('Return "true" for valid tag names', async t => {
t.true(await verifyTagName('1.0.0'));
t.true(await verifyTagName('v1.0.0'));
t.true(await verifyTagName('tag_name'));
t.true(await verifyTagName('tag/name'));
});
test.serial('Return falsy for invalid tag names', async t => {
t.falsy(await verifyTagName('?1.0.0'));
t.falsy(await verifyTagName('*1.0.0'));
t.falsy(await verifyTagName('[1.0.0]'));
t.falsy(await verifyTagName('1.0.0..'));
});
test.serial('Throws error if obtaining the tags fails', async t => {
+15
View File
@@ -40,3 +40,18 @@ test.serial('Accept "undefined" input', t => {
test.serial('Return same string if no environment variable has to be replaced', t => {
t.is(require('../lib/hide-sensitive')('test'), 'test');
});
test.serial('Exclude empty environment variables from the regexp', t => {
process.env.SOME_PASSWORD = 'password';
process.env.SOME_TOKEN = '';
t.is(
require('../lib/hide-sensitive')(`https://user:${process.env.SOME_PASSWORD}@host.com?token=`),
'https://user:[secure]@host.com?token='
);
});
test.serial('Exclude empty environment variables from the regexp if there is only empty ones', t => {
process.env.SOME_PASSWORD = '';
process.env.SOME_TOKEN = ' \n ';
t.is(require('../lib/hide-sensitive')(`https://host.com?token=`), 'https://host.com?token=');
});
+303 -93
View File
@@ -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,16 +66,21 @@ 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 prepare = stub().resolves();
const publish1 = stub().resolves(release1);
const success = stub().resolves();
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
const options = {
...config,
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
verifyRelease,
generateNotes,
publish,
prepare,
publish: [publish1, pluginNoop],
success,
};
const semanticRelease = proxyquire('..', {
@@ -116,21 +121,75 @@ 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(prepare.callCount, 1);
t.deepEqual(prepare.args[0][0], config);
t.deepEqual(prepare.args[0][1].options, options);
t.deepEqual(prepare.args[0][1].logger, t.context.logger);
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish1.callCount, 1);
t.deepEqual(publish1.args[0][0], config);
t.deepEqual(publish1.args[0][1].options, options);
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);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
});
test.serial('Use new gitHead, and recreate release notes if a publish plugin create a commit', async t => {
test.serial('Use custom tag format', async t => {
const repositoryUrl = await gitRepo(true);
await gitCommits(['First']);
await gitTagVersion('test-1.0.0');
await gitCommits(['Second']);
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
const notes = 'Release notes';
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`};
const options = {
...config,
verifyConditions: stub().resolves(),
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
});
test.serial('Use new gitHead, and recreate release notes if a prepare plugin create a commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
@@ -144,10 +203,11 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
const notes = 'Release notes';
const generateNotes = stub().resolves(notes);
const publish1 = stub().callsFake(async () => {
const prepare1 = stub().callsFake(async () => {
commits = (await gitCommits(['Third'])).concat(commits);
});
const publish2 = stub().resolves();
const prepare2 = stub().resolves();
const publish = stub().resolves();
const options = {
branch: 'master',
@@ -156,7 +216,10 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes,
publish: [publish1, publish2],
prepare: [prepare1, prepare2],
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -168,20 +231,74 @@ 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.is(prepare1.callCount, 1);
t.deepEqual(prepare1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
nextRelease.gitHead = await getGitHead();
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
t.is(publish2.callCount, 1);
t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
t.deepEqual(generateNotes.args[1][1].nextRelease, {...nextRelease, ...{notes}});
t.is(prepare2.callCount, 1);
t.deepEqual(prepare2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][1].nextRelease, {...nextRelease, ...{notes}});
// Verify the tag has been created on the local and remote repo and reference the last gitHead
t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash);
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,
prepare: stub().resolves(),
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);
@@ -191,19 +308,21 @@ 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('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2, error3]);
t.deepEqual(errors, [error1, error2, error3]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 3', 'ERR3']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [
@@ -211,6 +330,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 => {
@@ -225,26 +349,31 @@ 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('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual(errors, [error1, error2]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
t.is(fail.callCount, 1);
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
@@ -262,6 +391,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,
@@ -271,7 +401,9 @@ test.serial('Dry-run skips publish', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
};
const semanticRelease = proxyquire('..', {
@@ -286,6 +418,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(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 => {
@@ -306,6 +473,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,
@@ -315,7 +483,10 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -324,12 +495,13 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
});
t.truthy(await semanticRelease(options));
t.is(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(verifyConditions.callCount, 1);
t.is(analyzeCommits.callCount, 1);
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 => {
@@ -350,6 +522,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,
@@ -359,7 +532,10 @@ test.serial('Allow local releases with "noCi" option', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -378,6 +554,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 => {
@@ -392,7 +569,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();
@@ -401,11 +577,14 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
const options = {
branch: 'master',
repositoryUrl,
verifyConditions: [verifyConditions],
verifyConditions: stub().resolves(),
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -428,18 +607,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);
@@ -451,7 +618,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[t.context.log.args.length - 1][0],
"This run was triggered by a pull request and therefore a new version won't be published."
);
});
@@ -459,21 +626,17 @@ 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(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -483,7 +646,7 @@ test.serial('Returns falsy value if not running from the configured branch', asy
t.falsy(await semanticRelease(options));
t.is(
t.context.log.args[0][0],
t.context.log.args[1][0],
'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version wont be published.'
);
});
@@ -494,7 +657,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();
@@ -503,11 +665,14 @@ 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,
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -520,7 +685,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
t.is(verifyRelease.callCount, 0);
t.is(generateNotes.callCount, 0);
t.is(publish.callCount, 0);
t.is(t.context.log.args[6][0], 'There are no relevant changes, so no new version is released.');
t.is(t.context.log.args[7][0], 'There are no relevant changes, so no new version is released.');
});
test.serial('Exclude commits with [skip release] or [release skip] from analysis', async t => {
@@ -537,22 +702,18 @@ 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(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
@@ -587,12 +748,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[t.context.stdout.args.length - 2][0], /Console: The token \[secure\] is invalid/);
t.regex(t.context.stdout.args[t.context.stdout.args.length - 1][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();
@@ -601,11 +810,11 @@ test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot b
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const error = await t.throws(semanticRelease());
const errors = [...(await t.throws(semanticRelease()))];
// Verify error code and type
t.is(error.code, 'ENOREPOURL');
t.is(error.name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOREPOURL');
t.is(errors[0].name, 'SemanticReleaseError');
});
test.serial('Throw an Error if plugin returns an unexpected value', async t => {
@@ -626,6 +835,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('..', {
@@ -636,7 +847,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 => {
@@ -649,20 +860,19 @@ 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),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
};
const semanticRelease = proxyquire('..', {
+27 -35
View File
@@ -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 */
@@ -590,40 +603,19 @@ test.serial('Exit with 1 if missing permission to push to the remote repository'
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
const {repositoryUrl} = await gitbox.createRepo(packageName);
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
});
await gitbox.createRepo(packageName);
await writeJson('./package.json', {name: packageName, version: '0.0.0-dev'});
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
const {stdout, code} = await execa(cli, [], {env: {...env, ...{GH_TOKEN: 'user:wrong_pass'}}, reject: false});
const {stdout, code} = await execa(
cli,
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
{env: {...env, ...{GH_TOKEN: 'user:wrong_pass'}}, reject: false}
);
// Verify the type and message are logged
t.regex(stdout, /EGITNOPERMISSION/);
t.is(code, 1);
});
test.serial('CLI returns error code and prints help if called with a command', async t => {
t.log('$ semantic-release pre');
const {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 1);
});
test.serial('CLI prints help if called with --help', async t => {
t.log('$ semantic-release --help');
const {stdout, code} = await execa(cli, ['--help'], {env});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 0);
});
test.serial('CLI returns error code with invalid option', async t => {
t.log('$ semantic-release --unknown-option');
const {stderr, code} = await execa(cli, ['--unknown-option'], {env, reject: false});
t.regex(stderr, /unknown option/);
t.is(code, 1);
});
-77
View File
@@ -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'));
});
+71 -17
View File
@@ -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 => {
+91 -18
View File
@@ -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([...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,27 +131,41 @@ 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.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
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([...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.deepEqual([...errors], [error2, error3]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0 + 1));
t.true(step3.calledWith(0 + 1 + error2));
+21 -17
View File
@@ -27,7 +27,10 @@ test('Export default plugins', t => {
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
});
test('Export plugins based on config', t => {
@@ -47,7 +50,10 @@ test('Export plugins based on config', t => {
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
});
test.serial('Export plugins loaded from the dependency of a shareable config module', async t => {
@@ -75,7 +81,10 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
});
test.serial('Export plugins loaded from the dependency of a shareable config file', async t => {
@@ -100,7 +109,10 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
});
test('Use default when only options are passed for a single plugin', t => {
@@ -127,26 +139,18 @@ test('Merge global options with plugin options', async t => {
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
});
test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger));
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
const errors = [...t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger))];
t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EPLUGINCONF');
t.is(
error.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[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(() =>
getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)
);
const errors = [
...t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)),
];
t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EPLUGINCONF');
t.is(
error.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[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
});
+85
View File
@@ -0,0 +1,85 @@
import test from 'ava';
import tempy from 'tempy';
import verify from '../lib/verify';
import {gitRepo} from './helpers/git-utils';
// Save the current process.env
const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(() => {
// Delete environment variables that could have been set on the machine running the tests
delete process.env.GIT_CREDENTIALS;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GL_TOKEN;
delete process.env.GITLAB_TOKEN;
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
test.serial('Throw a AggregateError', async t => {
await gitRepo();
const errors = [...(await t.throws(verify({})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOREPOURL');
t.is(errors[1].name, 'SemanticReleaseError');
t.is(errors[1].code, 'EINVALIDTAGFORMAT');
t.is(errors[2].name, 'SemanticReleaseError');
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 = [...(await t.throws(verify({})))];
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}`};
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: 'test'};
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
});
test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`};
const errors = [...(await t.throws(verify(options)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
});
test.serial('Return "true" if all verification pass', async t => {
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
await t.notThrows(verify(options));
});