Compare commits
@@ -17,6 +17,11 @@
|
||||
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://waffle.io/semantic-release/semantic-release">
|
||||
<img alt="Waffle.io" src="https://badge.waffle.io/semantic-release/semantic-release.svg?columns=all">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/semantic-release">
|
||||
<img alt="npm latest version" src="https://img.shields.io/npm/v/semantic-release/latest.svg">
|
||||
@@ -76,23 +81,24 @@ If you need more control over the timing of releases you have a couple of option
|
||||
|
||||
### Release steps
|
||||
|
||||
After running the tests the command `semantic-release` will execute the following steps:
|
||||
After running the tests, the command `semantic-release` will execute the following steps:
|
||||
|
||||
| Step | Description |
|
||||
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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 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 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. |
|
||||
| Step | Description |
|
||||
|-------------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Verify Conditions | Verify all the conditions to proceed with the release. |
|
||||
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
|
||||
| Analyze commits | Determine the type of release based on the commits added since the last release. |
|
||||
| Verify release | Verify the release conformity. |
|
||||
| Generate notes | Generate release notes for the commits added since the last release. |
|
||||
| Create Git tag | Create a Git tag corresponding to the new release version. |
|
||||
| Prepare | Prepare the release. |
|
||||
| Publish | Publish the release. |
|
||||
| Notify | Notify of new releases or errors. |
|
||||
|
||||
## Documentation
|
||||
|
||||
- Usage
|
||||
- [Getting started](docs/usage/getting-started.md#getting-started)
|
||||
- [Installation](docs/usage/installation.md#installation)
|
||||
- [CI Configuration](docs/usage/ci-configuration.md#ci-configuration)
|
||||
- [Configuration](docs/usage/configuration.md#configuration)
|
||||
@@ -105,13 +111,11 @@ After running the tests the command `semantic-release` will execute the followin
|
||||
- [CI configurations](docs/recipes/README.md)
|
||||
- [Package managers and languages](docs/recipes/README.md)
|
||||
- Developer guide
|
||||
- [JavaScript API](docs/developer-guide/js-api.md)
|
||||
- [Plugins](docs/developer-guide/plugin.md)
|
||||
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
|
||||
- Resources
|
||||
- [Videos](docs/resources.md#videos)
|
||||
- [Articles](docs/resources.md#articles)
|
||||
- [Tutorials](docs/resources.md#tutorials)
|
||||
- Support
|
||||
- [Resources](docs/support/resources.md)
|
||||
- [Frequently Asked Questions](docs/support/FAQ.md)
|
||||
- [Troubleshooting](docs/support/troubleshooting.md)
|
||||
- [Node version requirement](docs/support/node-version.md)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- [Package managers and languages](docs/recipes/README.md)
|
||||
|
||||
## Developer guide
|
||||
- [JavaScript API](docs/developer-guide/js-api.md)
|
||||
- [Plugin](docs/developer-guide/plugin.md)
|
||||
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ execa
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(error => {
|
||||
console.error(`[semantic-release]: Git version ${MIN_GIT_VERSION} is required. No git binary found.`);
|
||||
console.error(err);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const {argv} = require('process');
|
||||
const {argv, env, stderr} = require('process');
|
||||
const util = require('util');
|
||||
const hideSensitive = require('./lib/hide-sensitive');
|
||||
|
||||
const stringList = {
|
||||
type: 'string',
|
||||
@@ -20,12 +22,13 @@ Usage:
|
||||
.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('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'})
|
||||
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
|
||||
.option('ci', {describe: 'Toggle CI verifications', 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('generate-notes', {...stringList, group: 'Plugins'})
|
||||
.option('prepare', {...stringList, group: 'Plugins'})
|
||||
.option('publish', {...stringList, group: 'Plugins'})
|
||||
.option('success', {...stringList, group: 'Plugins'})
|
||||
@@ -55,9 +58,9 @@ Usage:
|
||||
}
|
||||
await require('.')(opts);
|
||||
return 0;
|
||||
} catch (err) {
|
||||
if (err.name !== 'YError') {
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
if (error.name !== 'YError') {
|
||||
stderr.write(hideSensitive(env)(util.inspect(error, {colors: true})));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Developer guide
|
||||
|
||||
- [JavaScript API](js-api.md)
|
||||
- [Plugins](plugin.md)
|
||||
- [Shareable configuration](shareable-configuration.md)
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# JavaScript API
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const semanticRelease = require('semantic-release');
|
||||
const {WritableStreamBuffer} = require('stream-buffers');
|
||||
|
||||
const stdoutBuffer = WritableStreamBuffer();
|
||||
const stderrBuffer = WritableStreamBuffer();
|
||||
|
||||
try {
|
||||
const result = await semanticRelease({
|
||||
// Core options
|
||||
branch: 'master',
|
||||
repositoryUrl: 'https://github.com/me/my-package.git',
|
||||
// Shareable config
|
||||
extends: 'my-shareable-config',
|
||||
// Plugin options
|
||||
githubUrl: 'https://my-ghe.com',
|
||||
githubApiPathPrefix: '/api-prefix'
|
||||
}, {
|
||||
// Run semantic-release from `/path/to/git/repo/root` without having to change local process `cwd` with `process.chdir()`
|
||||
cwd: '/path/to/git/repo/root',
|
||||
// Pass the variable `MY_ENV_VAR` to semantic-release without having to modify the local `process.env`
|
||||
env: {...process.env, MY_ENV_VAR: 'MY_ENV_VAR_VALUE'},
|
||||
// Store stdout and stderr to use later instead of writing to `process.stdout` and `process.stderr`
|
||||
stdout: stdoutBuffer,
|
||||
stderr: stderrBuffer
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const {lastRelease, commits, nextRelease, releases} = result;
|
||||
|
||||
console.log(`Published ${nextRelease.type} release version ${nextRelease.version} containing ${commits.length} commits.`);
|
||||
|
||||
if (lastRelease.version) {
|
||||
console.log(`The last release was "${lastRelease.version}".`);
|
||||
}
|
||||
|
||||
for (const release of releases) {
|
||||
console.log(`The release was published with plugin "${pluginName}".`);
|
||||
}
|
||||
} else {
|
||||
console.log('No release published.');
|
||||
}
|
||||
|
||||
// Get stdout and stderr content
|
||||
const logs = stdoutBuffer.getContentsAsString('utf8');
|
||||
const errors = stderrBuffer.getContentsAsString('utf8');
|
||||
} catch (err) {
|
||||
console.error('The automated release failed with %O', err)
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### semanticRelease([options], [config]) => Promise<Result>
|
||||
|
||||
Run **semantic-release** and returns a `Promise` that resolves to a [Result](#result) object.
|
||||
|
||||
#### options
|
||||
|
||||
Type: `Object`
|
||||
|
||||
**semantic-release** options.
|
||||
|
||||
Can be used to set any [core option](../usage/configuration.md#configuration) or [plugin options](../usage/plugins.md#configuration).
|
||||
|
||||
Each option, will take precedence over options configured in the [configuration file](../usage/configuration.md#configuration) and [shareable configurations](../usage/configuration.md#extends).
|
||||
|
||||
#### config
|
||||
|
||||
Type: `Object`
|
||||
|
||||
**semantic-release** configuration specific for API usage.
|
||||
|
||||
##### cwd
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `process.cwd()`
|
||||
|
||||
The current working directory to use. It should be configured to the root of the Git repository to release from.
|
||||
|
||||
It allows to run **semantic-release** from a specific path without having to change the local process `cwd` with `process.chdir()`.
|
||||
|
||||
##### env
|
||||
|
||||
Type: `Object`<br>
|
||||
Default: `process.env`
|
||||
|
||||
The environment variables to use.
|
||||
|
||||
It allows to run **semantic-release** with specific environment variables without having to modify the local `process.env`.
|
||||
|
||||
##### stdout
|
||||
|
||||
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
|
||||
Default: `process.stdout`
|
||||
|
||||
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log information.
|
||||
|
||||
It allows to configure **semantic-release** to write logs to a specific stream rather than the local `process.stdout`.
|
||||
|
||||
##### stderr
|
||||
|
||||
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
|
||||
Default: `process.stderr`
|
||||
|
||||
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log errors.
|
||||
|
||||
It allows to configure **semantic-release** to write errors to a specific stream rather than the local `process.stderr`.
|
||||
|
||||
### Result
|
||||
|
||||
Type: `Object` `Boolean`<br>
|
||||
|
||||
And object with [`lastRelease`](#lastrelease), [`nextRelease`](#nextrelease), [`commits`](#commits) and [`releases`](#releases) if a release is published or `false` if no release was published.
|
||||
|
||||
#### lastRelease
|
||||
|
||||
Type: `Object`
|
||||
|
||||
Information related to the last release found:
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|----------------------------------------------------------------------------------------------------|
|
||||
| version | `String` | The version of the last release. |
|
||||
| gitHead | `String` | The sha of the last commit being part of the last release. |
|
||||
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the last release. |
|
||||
|
||||
**Notes**: If no previous release is found, `lastRelease` will be an empty `Object`.
|
||||
|
||||
Example:
|
||||
```js
|
||||
{
|
||||
gitHead: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
|
||||
version: '1.0.0',
|
||||
gitTag: 'v1.0.0',
|
||||
}
|
||||
```
|
||||
|
||||
#### commits
|
||||
|
||||
Type: `Array<Object>`
|
||||
|
||||
The list of commit included in the new release.<br>
|
||||
Each commit object has the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
|-----------------|----------|-------------------------------------------------|
|
||||
| commit | `Object` | The commit abbreviated and full hash. |
|
||||
| commit.long | `String` | The commit hash. |
|
||||
| commit.short | `String` | The commit abbreviated hash. |
|
||||
| tree | `Object` | The commit abbreviated and full tree hash. |
|
||||
| tree.long | `String` | The commit tree hash. |
|
||||
| tree.short | `String` | The commit abbreviated tree hash. |
|
||||
| author | `Object` | The commit author information. |
|
||||
| author.name | `String` | The commit author name. |
|
||||
| author.email | `String` | The commit author email. |
|
||||
| author.short | `String` | The commit author date. |
|
||||
| committer | `Object` | The committer information. |
|
||||
| committer.name | `String` | The committer name. |
|
||||
| committer.email | `String` | The committer email. |
|
||||
| committer.short | `String` | The committer date. |
|
||||
| subject | `String` | The commit subject. |
|
||||
| body | `String` | The commit body. |
|
||||
| message | `String` | The commit full message (`subject` and `body`). |
|
||||
| hash | `String` | The commit hash. |
|
||||
| committerDate | `String` | The committer date. |
|
||||
|
||||
Example:
|
||||
```js
|
||||
[
|
||||
{
|
||||
commit: {
|
||||
long: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
short: '68eb2c4'
|
||||
},
|
||||
tree: {
|
||||
long: '7ab515d12bd2cf431745511ac4ee13fed15ab578',
|
||||
short: '7ab515d'
|
||||
},
|
||||
author: {
|
||||
name: 'Me',
|
||||
email: 'me@email.com',
|
||||
date: 2018-07-22T20:52:44.000Z
|
||||
},
|
||||
committer: {
|
||||
name: 'Me',
|
||||
email: 'me@email.com',
|
||||
date: 2018-07-22T20:52:44.000Z
|
||||
},
|
||||
subject: 'feat: a new feature',
|
||||
body: 'Description of the new feature',
|
||||
hash: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
message: 'feat: a new feature\n\nDescription of the new feature',
|
||||
committerDate: 2018-07-22T20:52:44.000Z
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### nextRelease
|
||||
|
||||
Type: `Object`
|
||||
|
||||
Information related to the newly published release:
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|---------------------------------------------------------------------------------------------------|
|
||||
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
|
||||
| version | `String` | The version of the new release. |
|
||||
| gitHead | `String` | The sha of the last commit being part of the new release. |
|
||||
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the new release. |
|
||||
| notes | `String` | The release notes for the new release. |
|
||||
|
||||
Example:
|
||||
```js
|
||||
{
|
||||
type: 'minor',
|
||||
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
version: '1.1.0',
|
||||
gitTag: 'v1.1.0',
|
||||
notes: 'Release notes for version 1.1.0...',
|
||||
}
|
||||
```
|
||||
|
||||
#### releases
|
||||
|
||||
Type: `Array<Object>`
|
||||
|
||||
The list of releases published, one release per [publish plugin](../usage/plugins.md#publish-plugin).<br>
|
||||
Each release object has the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
|------------|----------|-----------------------------------------------------------------------------------------------|
|
||||
| name | `String` | **Optional.** The release name, only if set by the corresponding `publish` plugin. |
|
||||
| url | `String` | **Optional.** The release URL, only if set by the corresponding `publish` plugin. |
|
||||
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
|
||||
| version | `String` | The version of the release. |
|
||||
| gitHead | `String` | The sha of the last commit being part of the release. |
|
||||
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the release. |
|
||||
| notes | `String` | The release notes for the release. |
|
||||
| pluginName | `String` | The name of the plugin that published the release. |
|
||||
|
||||
Example:
|
||||
```js
|
||||
[
|
||||
{
|
||||
name: 'GitHub release',
|
||||
url: 'https://github.com/me/my-package/releases/tag/v1.1.0',
|
||||
type: 'minor',
|
||||
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
version: '1.1.0',
|
||||
gitTag: 'v1.1.0',
|
||||
notes: 'Release notes for version 1.1.0...',
|
||||
pluginName: '@semantic-release/github'
|
||||
},
|
||||
{
|
||||
name: 'npm package (@latest dist-tag)',
|
||||
url: 'https://www.npmjs.com/package/my-package',
|
||||
type: 'minor',
|
||||
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
version: '1.1.0',
|
||||
gitTag: 'v1.1.0',
|
||||
notes: 'Release notes for version 1.1.0...',
|
||||
pluginName: '@semantic-release/npm'
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -1,52 +1,61 @@
|
||||
# Plugins list
|
||||
|
||||
## Default plugins
|
||||
|
||||
- [@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
|
||||
|
||||
- [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer)
|
||||
- `analyzeCommits`: Determine the type of release by analyzing commits with [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog)
|
||||
- [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator)
|
||||
- `generateNotes`: Generate release notes for the commits added since the last release with [conventional-changelog](https://github.com/conventional-changelog/conventional-changelog)
|
||||
- [@semantic-release/github](https://github.com/semantic-release/github)
|
||||
- `verifyConditions`: Verify the presence and the validity of the GitHub authentication and release configuration
|
||||
- `publish`: Publish a [GitHub release](https://help.github.com/articles/about-releases)
|
||||
- `success`: Add a comment to GitHub issues and pull requests resolved in the release
|
||||
- `fail`: Open a GitHub issue when a release fails
|
||||
- [@semantic-release/npm](https://github.com/semantic-release/npm)
|
||||
- `verifyConditions`: Verify the presence and the validity of the npm authentication and release configuration
|
||||
- `prepare`: Update the package.json version and create the npm package tarball
|
||||
- `publish`: Publish the package on the npm registry
|
||||
- [@semantic-release/gitlab](https://github.com/semantic-release/gitlab)
|
||||
- [verifyConditions](https://github.com/semantic-release/gitlab#verifyconditions): Verify the presence and the validity of the GitLab authentication and release configuration
|
||||
- [publish](https://github.com/semantic-release/gitlab#publish): Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
|
||||
- `verifyConditions`: Verify the presence and the validity of the GitLab authentication and release configuration
|
||||
- `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
|
||||
- [prepare](https://github.com/semantic-release/git#prepare): Push a release commit and tag, including configurable files
|
||||
- `verifyConditions`: Verify the presence and the validity of the Git authentication and release configuration
|
||||
- `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
|
||||
- [prepare](https://github.com/semantic-release/changelog#prepare): Create or update the changelog file in the local project repository
|
||||
- `verifyConditions`: Verify the presence and the validity of the configuration
|
||||
- `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
|
||||
- [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
|
||||
- `verifyConditions`: Execute a shell command to verify if the release should happen
|
||||
- `analyzeCommits`: Execute a shell command to determine the type of release
|
||||
- `verifyRelease`: Execute a shell command to verifying a release that was determined before and is about to be published.
|
||||
- `generateNotes`: Execute a shell command to generate the release note
|
||||
- `prepare`: Execute a shell command to prepare the release
|
||||
- `publish`: Execute a shell command to publish the release
|
||||
- `success`: Execute a shell command to notify of a new release
|
||||
- `fail`: Execute a shell command to notify of a failed release
|
||||
|
||||
## Community plugins
|
||||
|
||||
[Open a Pull Request](https://github.com/semantic-release/semantic-release/blob/caribou/CONTRIBUTING.md#submitting-a-pull-request) to add your plugin to the list.
|
||||
|
||||
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker) Set of semantic-release plugins for publishing a docker image to Docker Hub
|
||||
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
|
||||
- [publish](https://github.com/felixfbecker/semantic-release-docker#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
|
||||
- [semantic-release-vsce](https://github.com/raix/semantic-release-vsce) Set of semantic-release plugins for publishing Visual Studio Code extensions to the marketplace
|
||||
- **verifyConditions** Verify the presence and the validity of the vsce authentication and release configuration
|
||||
- **prepare** Create a `.vsix` for distribution
|
||||
- **publish** Publish the package to the Visual Studio Code marketplace
|
||||
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
|
||||
- [verifyConditions](https://github.com/piercus/semantic-release-verify-deps) Check the dependencies format against a regexp before a release
|
||||
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome) Set of semantic-release plugins for publishing a Chrome extension release.
|
||||
- [verifyConditions](https://github.com/GabrielDuarteM/semantic-release-chrome#verifyconditions) Verify the presence of the authentication (set via environment variables).
|
||||
- [prepare](https://github.com/GabrielDuarteM/semantic-release-chrome#prepare) Write the correct version to the manifest.json and creates a zip file of the whole dist folder.
|
||||
- [publish](https://github.com/GabrielDuarteM/semantic-release-chrome#publish) Uploads the generated zip file to the webstore, and publish the item.
|
||||
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker)
|
||||
- `verifyConditions`: Verify that all needed configuration is present and login to the Docker registry.
|
||||
- `publish`: Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
|
||||
- [semantic-release-gcr](https://github.com/carlos-cubas/semantic-release-gcr)
|
||||
- `verifyConditions`: Verify that all needed configuration is present and login to the Docker registry.
|
||||
- `publish`: Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
|
||||
- [semantic-release-vsce](https://github.com/raix/semantic-release-vsce)
|
||||
- `verifyConditions`: Verify the presence and the validity of the vsce authentication and release configuration
|
||||
- `prepare`: Create a `.vsix` for distribution
|
||||
- `publish`: Publish the package to the Visual Studio Code marketplace
|
||||
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
|
||||
- `verifyConditions`: Check the dependencies format against a regexp before a release
|
||||
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome)
|
||||
- `verifyConditions`: Verify the presence of the authentication (set via environment variables)
|
||||
- `prepare`: Write the correct version to the manifest.json and creates a zip file of the whole dist folder
|
||||
- `publish`: Uploads the generated zip file to the webstore, and publish the item
|
||||
- [semantic-release-firefox](https://github.com/felixfbecker/semantic-release-firefox)
|
||||
- `verifyConditions`: Verify the presence of the authentication (set via environment variables)
|
||||
- `prepare`: Write the correct version to the manifest.json, creates a xpi file of the dist folder and a zip of the sources
|
||||
- `publish`: Submit the generated archives to the webstore for review, and publish the item including release notes
|
||||
- [semantic-release-gerrit](https://github.com/pascalMN/semantic-release-gerrit)
|
||||
- `generateNotes`: Generate release notes with Gerrit reviews URL
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ See the [CI configuration recipes](../recipes/README.md#ci-configurations) for m
|
||||
|
||||
## Can I run semantic-release on my local machine rather than on a CI server?
|
||||
|
||||
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:
|
||||
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#ci) 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> npx semantic-release --no-ci
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Usage
|
||||
|
||||
- [Getting started](getting-started.md#getting-started)
|
||||
- [Installation](installation.md#installation)
|
||||
- [CI Configuration](ci-configuration.md#ci-configuration)
|
||||
- [Configuration](configuration.md#configuration)
|
||||
|
||||
@@ -10,14 +10,12 @@ See [CI configuration recipes](../recipes/README.md#ci-configurations) for more
|
||||
|
||||
**semantic-release** requires push access to the project Git repository in order to create [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). The Git authentication can be set with one of the following environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
|
||||
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
|
||||
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
|
||||
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
|
||||
|
||||
`GIT_CREDENTIALS` must be the Git username and password in the format `<username>:<password>`.
|
||||
| Variable | Description |
|
||||
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
|
||||
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
|
||||
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
|
||||
| `GIT_CREDENTIALS` | [URL encoded](https://en.wikipedia.org/wiki/Percent-encoding) Git username and password in the format `<username>:<password>`. The username and password must each be individually URL encoded, not the `:` separating them. |
|
||||
|
||||
Alternatively the Git authentication can be set up via [SSH keys](../recipes/git-auth-ssh-keys.md).
|
||||
|
||||
@@ -33,18 +31,3 @@ See each plugin documentation for the environment variables to set up.
|
||||
The authentication token/credentials have to be made available in the CI service via environment variables.
|
||||
|
||||
See [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on how to configure environment variables in your CI service.
|
||||
|
||||
## Automatic setup with `semantic-release-cli`
|
||||
|
||||
[`semantic-release-cli`](https://github.com/semantic-release/cli) allows for easy [installation](installation.md) of **semantic-release** in your Node project as well as setting up the CI configuration:
|
||||
|
||||
```bash
|
||||
npm install -g semantic-release-cli
|
||||
|
||||
cd your-module
|
||||
semantic-release-cli setup
|
||||
```
|
||||
|
||||

|
||||
|
||||
See the [semantic-release-cli](https://github.com/semantic-release/cli#what-it-does) documentation for more details.
|
||||
|
||||
+56
-139
@@ -1,19 +1,25 @@
|
||||
# Configuration
|
||||
|
||||
In order to customize **semantic-release**’s behavior, [options](#options) and [plugins](plugins.md) can be set via:
|
||||
**semantic-release** configuration consist of:
|
||||
- Git repository options ([URL](#repositoryurl), [release branch](#branch) and [tag format](#tagformat))
|
||||
- [plugins](#plugins) definition
|
||||
- run mode ([debug](#debug), [dry run](#dryrun) and [local (no CI)](#ci))
|
||||
|
||||
All those options can be configured directly or by extending a [shareable configuration](shareable-configurations.md).
|
||||
|
||||
Additionally, metadata of Git tags generated by **semantic-release** can be customized via standard [Git environment variables](#git-environment-variables).
|
||||
|
||||
## Configuration file
|
||||
|
||||
**semantic-release**’s [options](#options), mode and [plugins](plugins.md) can be set via:
|
||||
- A `.releaserc` file, written in YAML or JSON, with optional extensions: .`yaml`/`.yml`/`.json`/`.js`
|
||||
- A `release.config.js` file that exports an object
|
||||
- A `release` key in the project's `package.json` file
|
||||
- CLI arguments
|
||||
|
||||
Alternatively some options can be set via CLI arguments.
|
||||
|
||||
The following three examples are the same.
|
||||
|
||||
Via CLI argument:
|
||||
|
||||
```bash
|
||||
$ semantic-release --branch next
|
||||
```
|
||||
|
||||
Via `release` key in the project's `package.json` file:
|
||||
|
||||
```json
|
||||
@@ -38,27 +44,23 @@ Via `.releaserc` file:
|
||||
$ semantic-release
|
||||
```
|
||||
|
||||
Via CLI argument:
|
||||
|
||||
```bash
|
||||
$ semantic-release --branch next
|
||||
```
|
||||
|
||||
**Note**: CLI arguments take precedence over options configured in the configuration file.
|
||||
|
||||
**Note**: Plugin options cannot be defined via CLI arguments and must be defined in the configuration file.
|
||||
|
||||
**Note**: When configuring via `package.json`, the configuration must be under the `release` property. However, when using a `.releaserc` or a `release.config.js` file, the configuration must be set without a `release` property.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------|
|
||||
| `GIT_AUTHOR_NAME` | The author name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
|
||||
| `GIT_AUTHOR_EMAIL` | The author email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
|
||||
| `GIT_COMMITTER_NAME` | The committer name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
|
||||
| `GIT_COMMITTER_EMAIL` | The committer email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
|
||||
|
||||
## Options
|
||||
|
||||
### extends
|
||||
|
||||
Type: `Array`, `String`
|
||||
|
||||
Type: `Array`, `String`<br>
|
||||
CLI arguments: `-e`, `--extends`
|
||||
|
||||
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configurations are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
|
||||
@@ -67,20 +69,16 @@ List of modules or file paths containing a [shareable configuration](shareable-c
|
||||
|
||||
### branch
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `master`
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `master`<br>
|
||||
CLI arguments: `-b`, `--branch`
|
||||
|
||||
The branch on which releases should happen.
|
||||
|
||||
### repositoryUrl
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)<br>
|
||||
CLI arguments: `-r`, `--repository-url`
|
||||
|
||||
The git repository URL.
|
||||
@@ -89,138 +87,57 @@ Any valid git url format is supported (See [Git protocols](https://git-scm.com/b
|
||||
|
||||
### tagFormat
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `v${version}`
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `v${version}`<br>
|
||||
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).
|
||||
|
||||
### plugins
|
||||
|
||||
Type: `Array`<br>
|
||||
Default: `['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/npm', '@semantic-release/github']`<br>
|
||||
CLI arguments: `-p`, `--plugins`
|
||||
|
||||
Define the list of plugins to use. Plugins will run in series, in the order defined, for each [steps](../../README.md#release-steps) if they implement it.
|
||||
|
||||
Plugins configuration can defined by wrapping the name and an options object in an array.
|
||||
|
||||
See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### dryRun
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
Default: `false` if running in a CI environment, `true` otherwise
|
||||
|
||||
Type: `Boolean`<br>
|
||||
Default: `false` if running in a CI environment, `true` otherwise<br>
|
||||
CLI arguments: `-d`, `--dry-run`
|
||||
|
||||
Dry-run mode, skip publishing, print next version and release notes.
|
||||
|
||||
### noCi
|
||||
### ci
|
||||
|
||||
Type: `Boolean`
|
||||
Type: `Boolean`<br>
|
||||
Default: `true`<br>
|
||||
CLI arguments: `--ci` / `--no-ci`
|
||||
|
||||
Default: `false`
|
||||
Set to `false` to skip Continuous Integration environment verifications. This allows for making releases from a local machine.
|
||||
|
||||
CLI arguments: `--no-ci`
|
||||
|
||||
Skip Continuous Integration environment verifications. This allows for making releases from a local machine.
|
||||
**Note**: The CLI arguments `--no-ci` is equivalent to `--ci false`.
|
||||
|
||||
### debug
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `Boolean`<br>
|
||||
Default: `false`<br>
|
||||
CLI argument: `--debug`
|
||||
|
||||
Output debugging information. It can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
|
||||
|
||||
### verifyConditions
|
||||
## Git environment variables
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/npm', '@semantic-release/github']`
|
||||
|
||||
CLI argument: `--verify-conditions`
|
||||
|
||||
Define the list of [verify conditions plugins](plugins.md#verifyconditions-plugin). Plugins will run in series, in the order defined in the `Array`.
|
||||
|
||||
See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### analyzeCommits
|
||||
|
||||
Type: `String`, `Object`
|
||||
|
||||
Default: `'@semantic-release/commit-analyzer'`
|
||||
|
||||
CLI argument: `--analyze-commits`
|
||||
|
||||
Define the [analyze commits plugin](plugins.md#analyzecommits-plugin).
|
||||
|
||||
See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### verifyRelease
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `[]`
|
||||
|
||||
CLI argument: `--verify-release`
|
||||
|
||||
Define the list of [verify release plugins](plugins.md#verifyrelease-plugin). Plugins will run in series, in the order defined in the `Array`.
|
||||
|
||||
See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### generateNotes
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/release-notes-generator']`
|
||||
|
||||
CLI argument: `--generate-notes`
|
||||
|
||||
Define the [generate notes plugins](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`
|
||||
|
||||
Default: `['@semantic-release/npm', '@semantic-release/github']`
|
||||
|
||||
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.
|
||||
| Variable | Description | Default |
|
||||
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------|
|
||||
| `GIT_AUTHOR_NAME` | The author name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
|
||||
| `GIT_AUTHOR_EMAIL` | The author email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
|
||||
| `GIT_COMMITTER_NAME` | The committer name associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot. |
|
||||
| `GIT_COMMITTER_EMAIL` | The committer email associated with the [Git release tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging). See [Git environment variables](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_committing). | @semantic-release-bot email address. |
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Getting started
|
||||
|
||||
In order to use **semantic-release** you must follows this steps:
|
||||
- [Install](./installation.md#installation) **semantic-release** in your project
|
||||
- Configure your Continuous Integration service to [run **semantic-release**](./ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded)
|
||||
- Configure your Git repository and package manager repository [authentication](ci-configuration.md#authentication) in your Continuous Integration service
|
||||
- Configure **semantic-release** [options and plugins](./configuration.md#configuration)
|
||||
|
||||
Alternatively those steps can be easily done with the [**semantic-release** interactive CLI](https://github.com/semantic-release/cli):
|
||||
|
||||
```bash
|
||||
npm install -g semantic-release-cli
|
||||
|
||||
cd your-module
|
||||
semantic-release-cli setup
|
||||
```
|
||||
|
||||

|
||||
|
||||
See the [semantic-release-cli](https://github.com/semantic-release/cli#what-it-does) documentation for more details.
|
||||
|
||||
**Note**: only a limited number of options, CI services and plugins is currently supported by `semantic-release-cli`.
|
||||
+63
-83
@@ -1,98 +1,78 @@
|
||||
# Plugins
|
||||
|
||||
Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
|
||||
Each [release step](../../README.md#release-steps) is implemented by configurable plugins. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
|
||||
|
||||
See [plugins list](../extending/plugins-list.md).
|
||||
A plugin is a npm module that can implement one or more of the following steps:
|
||||
|
||||
## Plugin types
|
||||
| Step | Accept multiple | Required | Description |
|
||||
|--------------------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `verifyConditions` | Yes | No | Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc... |
|
||||
| `analyzeCommits` | No | Yes | Responsible for determining the type of the next release (`major`, `minor` or `patch`). |
|
||||
| `verifyRelease` | Yes | No | Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. |
|
||||
| `generateNotes` | Yes | No | Responsible for generating the content of the release note. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of each plugin output. |
|
||||
| `prepare` | Yes | No | Responsible for preparing the release, for example creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets and pushing a commit. |
|
||||
| `publish` | Yes | No | Responsible for publishing the release. |
|
||||
| `success` | Yes | No | Responsible for notifying of a new release. |
|
||||
| `fail` | Yes | No | Responsible for notifying of a failed release. |
|
||||
|
||||
### verifyConditions plugin
|
||||
See [available plugins](../extending/plugins-list.md).
|
||||
|
||||
Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
|
||||
## Plugins configuration
|
||||
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).
|
||||
Each plugin must be installed and configured with the [`plugins` options](./configuration.md#plugins) by specifying the list of plugins by npm module name.
|
||||
|
||||
### analyzeCommits plugin
|
||||
```bash
|
||||
$ npm install @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm -D
|
||||
```
|
||||
|
||||
Responsible for determining the type of the next release (`major`, `minor` or `patch`).
|
||||
|
||||
Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).
|
||||
|
||||
### verifyRelease plugin
|
||||
|
||||
Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
|
||||
|
||||
Default implementation: none.
|
||||
|
||||
### generateNotes plugin
|
||||
|
||||
Responsible for generating release notes. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of plugin output.
|
||||
|
||||
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
|
||||
|
||||
### prepare plugin
|
||||
|
||||
Responsible for preparing the release, including:
|
||||
- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
|
||||
- Create and push commits
|
||||
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).
|
||||
|
||||
### publish plugin
|
||||
|
||||
Responsible for publishing the release.
|
||||
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).
|
||||
|
||||
### success plugin
|
||||
|
||||
Responsible for notifying of a new release.
|
||||
|
||||
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).
|
||||
|
||||
### fail plugin
|
||||
|
||||
Responsible for notifying of a failed release.
|
||||
|
||||
Default implementation: [@semantic-release/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 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
|
||||
{
|
||||
"release": {
|
||||
"verifyConditions": [
|
||||
{
|
||||
"path": "@semantic-release/exec",
|
||||
"cmd": "verify-conditions.sh"
|
||||
},
|
||||
"@semantic-release/npm",
|
||||
"@semantic-release/github"
|
||||
],
|
||||
"analyzeCommits": "custom-plugin",
|
||||
"verifyRelease": [
|
||||
{
|
||||
"path": "@semantic-release/exec",
|
||||
"cmd": "verify-release.sh"
|
||||
},
|
||||
],
|
||||
"generateNotes": "./build/my-plugin.js",
|
||||
"githubUrl": "https://my-ghe.com",
|
||||
"githubApiPathPrefix": "/api-prefix"
|
||||
}
|
||||
"plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm"]
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin ordering
|
||||
|
||||
For each [release step](../../README.md#release-steps) the plugins that implement that step will be executed in the order in which the are defined.
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/npm",
|
||||
"@semantic-release/git"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration **semantic-release** will:
|
||||
- execute the `verifyConditions` implementation of `@semantic-release/npm` then `@semantic-release/git`
|
||||
- execute the `analyzeCommits` implementation of `@semantic-release/commit-analyzer`
|
||||
- execute the `prepare` implementation of `@semantic-release/npm` then `@semantic-release/git`
|
||||
- execute the `generateNotes` implementation of `@semantic-release/release-notes-generator`
|
||||
- execute the `publish` implementation of `@semantic-release/npm`
|
||||
|
||||
## Plugin options
|
||||
|
||||
A plugin options can specified by wrapping the name and an options object in an array. Options configured this way will be passed only to that specific plugin.
|
||||
|
||||
Global plugin options can defined at the root of the **semantic-release** configuration object. Options configured this way will be passed to all plugins.
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
["@semantic-release/github", {
|
||||
"assets": ["dist/**"]
|
||||
}],
|
||||
"@semantic-release/git"
|
||||
],
|
||||
"preset": "angular"
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration:
|
||||
- the `custom-plugin` npm module will be used to [analyze commits](#analyzecommits-plugin)
|
||||
- the `./build/my-plugin.js` script will be used to [generate release notes](#generatenotes-plugin)
|
||||
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec), [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/github`](https://github.com/semantic-release/github) plugins will be used to [verify conditions](#verifyconditions-plugin)
|
||||
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin will be used to [verify the release](#verifyrelease-plugin)
|
||||
- the `cmd` option will be set to `verify-conditions.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify conditions](#verifyconditions-plugin)
|
||||
- the `cmd` option will be set to `verify-release.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify the release](#verifyrelease-plugin)
|
||||
- the `githubUrl` and `githubApiPathPrefix` options will be set to respectively `https://my-ghe.com` and `/api-prefix` for all plugins
|
||||
- All plugins will receive the `preset` option, which will be used by both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` (and ignored by `@semantic-release/github` and `@semantic-release/git`)
|
||||
- The `@semantic-release/github` plugin will receive the `assets` options (`@semantic-release/git` will not receive it and therefore will use it's default value for that option)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const process = require('process');
|
||||
const {template} = require('lodash');
|
||||
const {template, pick} = require('lodash');
|
||||
const marked = require('marked');
|
||||
const TerminalRenderer = require('marked-terminal');
|
||||
const envCi = require('env-ci');
|
||||
@@ -13,7 +12,7 @@ const getCommits = require('./lib/get-commits');
|
||||
const getLastRelease = require('./lib/get-last-release');
|
||||
const {extractErrors} = require('./lib/utils');
|
||||
const getGitAuthUrl = require('./lib/get-git-auth-url');
|
||||
const logger = require('./lib/logger');
|
||||
const getLogger = require('./lib/get-logger');
|
||||
const {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
|
||||
const getError = require('./lib/get-error');
|
||||
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
|
||||
@@ -21,8 +20,8 @@ const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
|
||||
marked.setOptions({renderer: new TerminalRenderer()});
|
||||
|
||||
async function run(context, plugins) {
|
||||
const {isCi, branch: ciBranch, isPr} = envCi();
|
||||
const {cwd, env, options, logger} = context;
|
||||
const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
|
||||
|
||||
if (!isCi && !options.dryRun && !options.noCi) {
|
||||
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
@@ -42,7 +41,7 @@ async function run(context, plugins) {
|
||||
|
||||
if (isCi && isPr && !options.noCi) {
|
||||
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ciBranch !== options.branch) {
|
||||
@@ -53,6 +52,7 @@ async function run(context, plugins) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
logger.success(`Run automated release from branch ${ciBranch}`);
|
||||
|
||||
await verify(context);
|
||||
|
||||
@@ -60,19 +60,18 @@ async function run(context, plugins) {
|
||||
|
||||
try {
|
||||
await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
|
||||
logger.log(
|
||||
"The local branch %s is behind the remote one, therefore a new version won't be published.",
|
||||
options.branch
|
||||
`The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr);
|
||||
logger.error(`The command "${error.cmd}" failed with the error message ${error.stderr}.`);
|
||||
throw getError('EGITNOPERMISSION', {options});
|
||||
}
|
||||
|
||||
logger.log('Run automated release from branch %s', options.branch);
|
||||
logger.success(`Allowed to push to the Git repository`);
|
||||
|
||||
await plugins.verifyConditions(context);
|
||||
|
||||
@@ -85,7 +84,7 @@ async function run(context, plugins) {
|
||||
|
||||
if (!nextRelease.type) {
|
||||
logger.log('There are no relevant changes, so no new version is released.');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
context.nextRelease = nextRelease;
|
||||
nextRelease.version = getNextVersion(context);
|
||||
@@ -95,35 +94,36 @@ async function run(context, plugins) {
|
||||
|
||||
if (options.dryRun) {
|
||||
const notes = await plugins.generateNotes(context);
|
||||
logger.log('Release note for version %s:\n', nextRelease.version);
|
||||
logger.log(`Release note for version ${nextRelease.version}:`);
|
||||
if (notes) {
|
||||
logger.stdout(`${marked(notes)}\n`);
|
||||
context.stdout.write(marked(notes));
|
||||
}
|
||||
} else {
|
||||
nextRelease.notes = await plugins.generateNotes(context);
|
||||
await plugins.prepare(context);
|
||||
|
||||
// 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, {cwd, env});
|
||||
await push(options.repositoryUrl, options.branch, {cwd, env});
|
||||
logger.success(`Created tag ${nextRelease.gitTag}`);
|
||||
|
||||
context.releases = await plugins.publish(context);
|
||||
|
||||
await plugins.success(context);
|
||||
|
||||
logger.log('Published release: %s', nextRelease.version);
|
||||
logger.success(`Published release ${nextRelease.version}`);
|
||||
}
|
||||
return true;
|
||||
|
||||
return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
|
||||
}
|
||||
|
||||
function logErrors({logger}, err) {
|
||||
function logErrors({logger, stderr}, 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);
|
||||
logger.error(`${error.code} ${error.message}`);
|
||||
if (error.details) {
|
||||
logger.stderr(`${marked(error.details)}\n`);
|
||||
stderr.write(marked(error.details));
|
||||
}
|
||||
} else {
|
||||
logger.error('An error occurred while running semantic-release: %O', error);
|
||||
@@ -131,21 +131,25 @@ function logErrors({logger}, err) {
|
||||
}
|
||||
}
|
||||
|
||||
async function callFail(context, plugins, error) {
|
||||
const errors = extractErrors(error).filter(error => error.semanticRelease);
|
||||
async function callFail(context, plugins, err) {
|
||||
const errors = extractErrors(err).filter(err => err.semanticRelease);
|
||||
if (errors.length > 0) {
|
||||
try {
|
||||
await plugins.fail({...context, errors});
|
||||
} catch (err) {
|
||||
logErrors(context, err);
|
||||
} catch (error) {
|
||||
logErrors(context, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) => {
|
||||
const context = {cwd, env, logger};
|
||||
context.logger.log(`Running %s version %s`, pkg.name, pkg.version);
|
||||
const {unhook} = hookStd({silent: false}, hideSensitive(context.env));
|
||||
module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
|
||||
const {unhook} = hookStd(
|
||||
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
|
||||
hideSensitive(env)
|
||||
);
|
||||
const context = {cwd, env, stdout: stdout || process.stdout, stderr: stderr || process.stderr};
|
||||
context.logger = getLogger(context);
|
||||
context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
|
||||
try {
|
||||
const {plugins, options} = await getConfig(context, opts);
|
||||
context.options = options;
|
||||
@@ -153,15 +157,15 @@ module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) =>
|
||||
const result = await run(context, plugins);
|
||||
unhook();
|
||||
return result;
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (!options.dryRun) {
|
||||
await callFail(context, plugins, err);
|
||||
await callFail(context, plugins, error);
|
||||
}
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
} catch (err) {
|
||||
logErrors(context, err);
|
||||
} catch (error) {
|
||||
logErrors(context, error);
|
||||
unhook();
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,4 +10,14 @@ const RELEASE_NOTES_SEPARATOR = '\n\n';
|
||||
|
||||
const SECRET_REPLACEMENT = '[secure]';
|
||||
|
||||
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT};
|
||||
const SECRET_MIN_SIZE = 5;
|
||||
|
||||
module.exports = {
|
||||
RELEASE_TYPE,
|
||||
FIRST_RELEASE,
|
||||
COMMIT_NAME,
|
||||
COMMIT_EMAIL,
|
||||
RELEASE_NOTES_SEPARATOR,
|
||||
SECRET_REPLACEMENT,
|
||||
SECRET_MIN_SIZE,
|
||||
};
|
||||
|
||||
@@ -9,11 +9,11 @@ const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infin
|
||||
const linkify = file => `${homepage}/blob/caribou/${file}`;
|
||||
|
||||
module.exports = {
|
||||
ENOGITREPO: () => ({
|
||||
ENOGITREPO: ({cwd}) => ({
|
||||
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()}\`.
|
||||
The current working directory is \`${cwd}\`.
|
||||
|
||||
Please verify your CI configuration to make sure the \`semantic-release\` command is executed from the root of the cloned repository.`,
|
||||
}),
|
||||
@@ -55,13 +55,23 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`
|
||||
|
||||
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
|
||||
}),
|
||||
EPLUGINCONF: ({type, pluginConf}) => ({
|
||||
EPLUGINCONF: ({type, multiple, required, pluginConf}) => ({
|
||||
message: `The \`${type}\` plugin configuration is invalid.`,
|
||||
details: `The [${type} plugin configuration](${linkify(
|
||||
`docs/usage/plugins.md#${toLower(type)}-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.
|
||||
details: `The [${type} plugin configuration](${linkify(`docs/usage/plugins.md#${toLower(type)}-plugin`)}) ${
|
||||
required ? 'is required and ' : ''
|
||||
}must be ${
|
||||
multiple ? 'a single or an array of plugins' : 'a single plugin'
|
||||
} definition. A plugin definition is an npm module name, optionnaly wrapped in an array with an object.
|
||||
|
||||
Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`,
|
||||
Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`,
|
||||
}),
|
||||
EPLUGINSCONF: ({plugin}) => ({
|
||||
message: 'The `plugins` configuration is invalid.',
|
||||
details: `The [plugins](${linkify(
|
||||
'docs/usage/configuration.md#plugins'
|
||||
)}) option must be an array of plugin definions. A plugin definition is an npm module name, optionnaly wrapped in an array with an object.
|
||||
|
||||
The invalid configuration is \`${stringify(plugin)}\`.`,
|
||||
}),
|
||||
EPLUGIN: ({pluginName, type}) => ({
|
||||
message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`,
|
||||
|
||||
+23
-20
@@ -1,18 +1,19 @@
|
||||
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
|
||||
const {isString, isPlainObject} = require('lodash');
|
||||
const {gitHead} = require('../git');
|
||||
const hideSensitive = require('../hide-sensitive');
|
||||
const {hideSensitiveValues} = require('../utils');
|
||||
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
|
||||
|
||||
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
|
||||
|
||||
module.exports = {
|
||||
verifyConditions: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
},
|
||||
analyzeCommits: {
|
||||
default: '@semantic-release/commit-analyzer',
|
||||
configValidator: conf => Boolean(conf) && validatePluginConfig(conf),
|
||||
default: ['@semantic-release/commit-analyzer'],
|
||||
multiple: false,
|
||||
required: true,
|
||||
outputValidator: output => !output || RELEASE_TYPE.includes(output),
|
||||
preprocess: ({commits, ...inputs}) => ({
|
||||
...inputs,
|
||||
@@ -21,13 +22,13 @@ module.exports = {
|
||||
postprocess: ([result]) => result,
|
||||
},
|
||||
verifyRelease: {
|
||||
default: false,
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
},
|
||||
generateNotes: {
|
||||
default: ['@semantic-release/release-notes-generator'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
outputValidator: output => !output || isString(output),
|
||||
pipelineConfig: () => ({
|
||||
getNextInput: ({nextRelease, ...context}, notes) => ({
|
||||
@@ -38,11 +39,11 @@ module.exports = {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
postprocess: results => results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR),
|
||||
postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)),
|
||||
},
|
||||
prepare: {
|
||||
default: ['@semantic-release/npm'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: ({generateNotes}, logger) => ({
|
||||
getNextInput: async context => {
|
||||
const newGitHead = await gitHead({cwd: context.cwd});
|
||||
@@ -59,8 +60,8 @@ module.exports = {
|
||||
}),
|
||||
},
|
||||
publish: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
outputValidator: output => !output || isPlainObject(output),
|
||||
pipelineConfig: () => ({
|
||||
// Add `nextRelease` and plugin properties to published release
|
||||
@@ -72,13 +73,15 @@ module.exports = {
|
||||
}),
|
||||
},
|
||||
success: {
|
||||
default: ['@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}),
|
||||
},
|
||||
fail: {
|
||||
default: ['@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}),
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ module.exports = async ({cwd, env, lastRelease: {gitHead}, logger}) => {
|
||||
commit.gitTags = commit.gitTags.trim();
|
||||
return commit;
|
||||
});
|
||||
logger.log('Found %s commits since last release', commits.length);
|
||||
logger.log(`Found ${commits.length} commits since last release`);
|
||||
debug('Parsed commits: %o', commits);
|
||||
return commits;
|
||||
};
|
||||
|
||||
+9
-3
@@ -1,4 +1,4 @@
|
||||
const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = require('lodash');
|
||||
const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash');
|
||||
const readPkgUp = require('read-pkg-up');
|
||||
const cosmiconfig = require('cosmiconfig');
|
||||
const resolveFrom = require('resolve-from');
|
||||
@@ -35,7 +35,7 @@ module.exports = async (context, opts) => {
|
||||
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
|
||||
// so those plugin will be loaded relatively to the config file
|
||||
Object.entries(extendsOpts).reduce((pluginsPath, [option, value]) => {
|
||||
if (PLUGINS_DEFINITIONS[option]) {
|
||||
if (PLUGINS_DEFINITIONS[option] || option === 'plugins') {
|
||||
castArray(value)
|
||||
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
|
||||
.map(plugin => (isString(plugin) ? plugin : plugin.path))
|
||||
@@ -57,8 +57,14 @@ module.exports = async (context, opts) => {
|
||||
branch: 'master',
|
||||
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/npm',
|
||||
'@semantic-release/github',
|
||||
],
|
||||
// Remove `null` and `undefined` options so they can be replaced with default ones
|
||||
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
|
||||
...pickBy(options, option => !isNil(option)),
|
||||
};
|
||||
|
||||
debug('options values: %O', options);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const {parse, format} = require('url');
|
||||
const {isUndefined} = require('lodash');
|
||||
const {isNil} = require('lodash');
|
||||
const gitUrlParse = require('git-url-parse');
|
||||
const hostedGitInfo = require('hosted-git-info');
|
||||
const {verifyAuth} = require('./git');
|
||||
@@ -43,8 +43,8 @@ module.exports = async ({cwd, env, options: {repositoryUrl, branch}}) => {
|
||||
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
|
||||
try {
|
||||
await verifyAuth(repositoryUrl, branch, {cwd, env});
|
||||
} catch (err) {
|
||||
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(env[envVar]));
|
||||
} catch (error) {
|
||||
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isNil(env[envVar]));
|
||||
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
|
||||
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
|
||||
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
|
||||
|
||||
@@ -42,7 +42,7 @@ module.exports = async ({cwd, env, options: {tagFormat}, logger}) => {
|
||||
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true});
|
||||
|
||||
if (tag) {
|
||||
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
|
||||
logger.log(`Found git tag ${tag.gitTag} associated with version ${tag.version}`);
|
||||
return {gitHead: await gitTagHead(tag.gitTag, {cwd, env}), ...tag};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const {Signale} = require('signale');
|
||||
const figures = require('figures');
|
||||
|
||||
module.exports = ({stdout, stderr}) =>
|
||||
new Signale({
|
||||
config: {displayTimestamp: true, underlineMessage: true, displayLabel: false},
|
||||
disabled: false,
|
||||
interactive: false,
|
||||
scope: 'semantic-release',
|
||||
stream: [stdout],
|
||||
types: {
|
||||
error: {badge: figures.cross, color: 'red', label: '', stream: [stderr]},
|
||||
log: {badge: figures.info, color: 'magenta', label: '', stream: [stdout]},
|
||||
success: {badge: figures.tick, color: 'green', label: '', stream: [stdout]},
|
||||
},
|
||||
});
|
||||
@@ -5,10 +5,10 @@ module.exports = ({nextRelease: {type}, lastRelease, logger}) => {
|
||||
let version;
|
||||
if (lastRelease.version) {
|
||||
version = semver.inc(lastRelease.version, type);
|
||||
logger.log('The next release version is %s', version);
|
||||
logger.log(`The next release version is ${version}`);
|
||||
} else {
|
||||
version = FIRST_RELEASE;
|
||||
logger.log('There is no previous release, the next release version is %s', version);
|
||||
logger.log(`There is no previous release, the next release version is ${version}`);
|
||||
}
|
||||
|
||||
return version;
|
||||
|
||||
+19
-19
@@ -12,8 +12,8 @@ const debug = require('debug')('semantic-release:git');
|
||||
async function gitTagHead(tagName, execaOpts) {
|
||||
try {
|
||||
return await execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +44,13 @@ async function isRefInHistory(ref, execaOpts) {
|
||||
try {
|
||||
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], execaOpts);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 1) {
|
||||
} catch (error) {
|
||||
if (error.code === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
debug(err);
|
||||
throw err;
|
||||
debug(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ async function isRefInHistory(ref, execaOpts) {
|
||||
async function fetch(repositoryUrl, execaOpts) {
|
||||
try {
|
||||
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], execaOpts);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
await execa('git', ['fetch', '--tags', repositoryUrl], execaOpts);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ async function fetch(repositoryUrl, execaOpts) {
|
||||
*
|
||||
* @return {string} the sha of the HEAD commit.
|
||||
*/
|
||||
async function gitHead(execaOpts) {
|
||||
function gitHead(execaOpts) {
|
||||
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ async function gitHead(execaOpts) {
|
||||
async function repoUrl(execaOpts) {
|
||||
try {
|
||||
return await execa.stdout('git', ['config', '--get', 'remote.origin.url'], execaOpts);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ async function repoUrl(execaOpts) {
|
||||
async function isGitRepo(execaOpts) {
|
||||
try {
|
||||
return (await execa('git', ['rev-parse', '--git-dir'], execaOpts)).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,9 @@ async function isGitRepo(execaOpts) {
|
||||
async function verifyAuth(repositoryUrl, branch, execaOpts) {
|
||||
try {
|
||||
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`], execaOpts);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
throw err;
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +163,8 @@ async function push(repositoryUrl, branch, execaOpts) {
|
||||
async function verifyTagName(tagName, execaOpts) {
|
||||
try {
|
||||
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOpts)).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ async function isBranchUpToDate(branch, execaOpts) {
|
||||
(await execa.stdout('git', ['ls-remote', '--heads', 'origin', branch], execaOpts)).match(/^(\w+)?/)[1],
|
||||
execaOpts
|
||||
);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
const {escapeRegExp} = require('lodash');
|
||||
const {SECRET_REPLACEMENT} = require('./definitions/constants');
|
||||
const {escapeRegExp, size, isString} = require('lodash');
|
||||
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
|
||||
|
||||
module.exports = env => {
|
||||
const toReplace = Object.keys(env).filter(
|
||||
envVar => /token|password|credential|secret|private/i.test(envVar) && env[envVar].trim()
|
||||
envVar => /token|password|credential|secret|private/i.test(envVar) && size(env[envVar].trim()) >= SECRET_MIN_SIZE
|
||||
);
|
||||
|
||||
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g');
|
||||
return output => {
|
||||
return output && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output;
|
||||
};
|
||||
return output =>
|
||||
output && isString(output) && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output;
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* Logger with `log` and `error` function.
|
||||
*/
|
||||
module.exports = {
|
||||
log(...args) {
|
||||
const [format, ...rest] = args;
|
||||
console.log(
|
||||
`${chalk.grey('[Semantic release]:')}${
|
||||
typeof format === 'string' ? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}` : ''
|
||||
}`,
|
||||
...(typeof format === 'string' ? [] : [format]).concat(rest)
|
||||
);
|
||||
},
|
||||
error(...args) {
|
||||
const [format, ...rest] = args;
|
||||
console.error(
|
||||
`${chalk.grey('[Semantic release]:')}${typeof format === 'string' ? ` ${chalk.red(format)}` : ''}`,
|
||||
...(typeof format === 'string' ? [] : [format]).concat(rest)
|
||||
);
|
||||
},
|
||||
stdout(...args) {
|
||||
console.log(args);
|
||||
},
|
||||
stderr(...args) {
|
||||
console.error(args);
|
||||
},
|
||||
};
|
||||
+66
-19
@@ -1,47 +1,94 @@
|
||||
const {identity, isPlainObject, omit, castArray, isUndefined} = require('lodash');
|
||||
const {identity, isPlainObject, omit, castArray, isNil, isString} = require('lodash');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const getError = require('../get-error');
|
||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||
const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('./utils');
|
||||
const pipeline = require('./pipeline');
|
||||
const normalize = require('./normalize');
|
||||
|
||||
module.exports = ({cwd, options, logger}, pluginsPath) => {
|
||||
module.exports = (context, pluginsPath) => {
|
||||
let {options, logger} = context;
|
||||
const errors = [];
|
||||
const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
|
||||
|
||||
const plugins = options.plugins
|
||||
? castArray(options.plugins).reduce((plugins, plugin) => {
|
||||
if (validatePlugin(plugin)) {
|
||||
const [name, config] = parseConfig(plugin);
|
||||
plugin = isString(name) ? loadPlugin(context, name, pluginsPath) : name;
|
||||
|
||||
if (isPlainObject(plugin)) {
|
||||
Object.entries(plugin).forEach(([type, func]) => {
|
||||
if (PLUGINS_DEFINITIONS[type]) {
|
||||
Reflect.defineProperty(func, 'pluginName', {
|
||||
value: isPlainObject(name) ? 'Inline plugin' : name,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
});
|
||||
plugins[type] = [...(PLUGINS_DEFINITIONS[type].multiple ? plugins[type] || [] : []), [func, config]];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
errors.push(getError('EPLUGINSCONF', {plugin}));
|
||||
}
|
||||
} else {
|
||||
errors.push(getError('EPLUGINSCONF', {plugin}));
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}, {})
|
||||
: [];
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
|
||||
options = {...plugins, ...options};
|
||||
|
||||
const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce(
|
||||
(
|
||||
plugins,
|
||||
[type, {configValidator, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
|
||||
pluginsConf,
|
||||
[type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
|
||||
) => {
|
||||
let pluginOpts;
|
||||
|
||||
if (isUndefined(options[type])) {
|
||||
if (isNil(options[type]) && def) {
|
||||
pluginOpts = def;
|
||||
} else {
|
||||
const defaultPaths = castArray(def);
|
||||
// If an object is passed and the path is missing, set the default one for single plugins
|
||||
if (isPlainObject(options[type]) && !options[type].path && defaultPaths.length === 1) {
|
||||
[options[type].path] = defaultPaths;
|
||||
// If an object is passed and the path is missing, merge it with step options
|
||||
if (isPlainObject(options[type]) && !options[type].path) {
|
||||
options[type] = castArray(plugins[type]).map(
|
||||
plugin => (plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin)
|
||||
);
|
||||
}
|
||||
if (configValidator && !configValidator(options[type])) {
|
||||
errors.push(getError('EPLUGINCONF', {type, pluginConf: options[type]}));
|
||||
return plugins;
|
||||
if (!validateStep({multiple, required}, options[type])) {
|
||||
errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]}));
|
||||
return pluginsConf;
|
||||
}
|
||||
pluginOpts = options[type];
|
||||
}
|
||||
|
||||
const steps = castArray(pluginOpts).map(pluginOpt =>
|
||||
normalize({cwd, options: omit(options, Object.keys(PLUGINS_DEFINITIONS)), logger}, type, pluginOpt, pluginsPath)
|
||||
normalize(
|
||||
{...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')},
|
||||
type,
|
||||
pluginOpt,
|
||||
pluginsPath
|
||||
)
|
||||
);
|
||||
|
||||
plugins[type] = async input =>
|
||||
postprocess(await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)));
|
||||
pluginsConf[type] = async input =>
|
||||
postprocess(
|
||||
await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)),
|
||||
input
|
||||
);
|
||||
|
||||
return plugins;
|
||||
return pluginsConf;
|
||||
},
|
||||
{}
|
||||
plugins
|
||||
);
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
return plugins;
|
||||
|
||||
return pluginsConf;
|
||||
};
|
||||
|
||||
+28
-24
@@ -1,30 +1,18 @@
|
||||
const {dirname} = require('path');
|
||||
const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash');
|
||||
const resolveFrom = require('resolve-from');
|
||||
const {isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash');
|
||||
const getError = require('../get-error');
|
||||
const {extractErrors} = require('../utils');
|
||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||
const {loadPlugin, parseConfig} = require('./utils');
|
||||
|
||||
module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
||||
module.exports = (context, type, pluginOpt, pluginsPath) => {
|
||||
const {stdout, stderr, options, logger} = context;
|
||||
if (!pluginOpt) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt;
|
||||
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
|
||||
|
||||
if (!isFunction(pluginOpt)) {
|
||||
if (pluginsPath[path]) {
|
||||
logger.log('Load plugin "%s" from %s in shareable config %s', type, path, pluginsPath[path]);
|
||||
} else {
|
||||
logger.log('Load plugin "%s" from %s', type, path);
|
||||
}
|
||||
}
|
||||
|
||||
const basePath = pluginsPath[path]
|
||||
? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path]))
|
||||
: __dirname;
|
||||
const plugin = isFunction(path) ? path : require(resolveFrom.silent(basePath, path) || resolveFrom(cwd, path));
|
||||
const [name, config] = parseConfig(pluginOpt);
|
||||
const pluginName = name.pluginName ? name.pluginName : isFunction(name) ? `[Function: ${name.name}]` : name;
|
||||
const plugin = loadPlugin(context, name, pluginsPath);
|
||||
|
||||
let func;
|
||||
if (isFunction(plugin)) {
|
||||
@@ -38,18 +26,34 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
||||
const validator = async input => {
|
||||
const {outputValidator} = PLUGINS_DEFINITIONS[type] || {};
|
||||
try {
|
||||
logger.log('Call plugin "%s"', type);
|
||||
const result = await func(cloneDeep(input));
|
||||
logger.log(`Start step "${type}" of plugin "${pluginName}"`);
|
||||
const result = await func({
|
||||
...cloneDeep(omit(input, ['stdout', 'stderr', 'logger'])),
|
||||
stdout,
|
||||
stderr,
|
||||
logger: logger.scope(logger.scopeName, pluginName),
|
||||
});
|
||||
if (outputValidator && !outputValidator(result)) {
|
||||
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
|
||||
}
|
||||
logger.success(`Completed step "${type}" of plugin "${pluginName}"`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
|
||||
throw err;
|
||||
} catch (error) {
|
||||
logger.error(`Failed step "${type}" of plugin "${pluginName}"`);
|
||||
extractErrors(error).forEach(err => Object.assign(err, {pluginName}));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
|
||||
|
||||
if (!isFunction(pluginOpt)) {
|
||||
if (pluginsPath[name]) {
|
||||
logger.success(`Loaded plugin "${type}" from "${pluginName}" in shareable config "${pluginsPath[name]}"`);
|
||||
} else {
|
||||
logger.success(`Loaded plugin "${type}" from "${pluginName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return validator;
|
||||
};
|
||||
|
||||
@@ -36,12 +36,12 @@ module.exports = (steps, {settleAll = false, getNextInput = identity, transform
|
||||
// Call the step with the input computed at the end of the previous iteration and save intermediary result
|
||||
result = await transform(await step(lastInput), step, lastInput);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (settleAll) {
|
||||
errors.push(...extractErrors(err));
|
||||
result = err;
|
||||
errors.push(...extractErrors(error));
|
||||
result = error;
|
||||
} else {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Prepare input for the next step, passing the input of the last iteration (or initial parameter for the first iteration) and the result of the current one
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
const {dirname} = require('path');
|
||||
const {isString, isFunction, castArray, isArray, isPlainObject, isNil} = require('lodash');
|
||||
const resolveFrom = require('resolve-from');
|
||||
|
||||
const validateStepArrayDefinition = conf =>
|
||||
isArray(conf) &&
|
||||
(conf.length === 1 || conf.length === 2) &&
|
||||
(isString(conf[0]) || isFunction(conf[0])) &&
|
||||
(isNil(conf[1]) || isPlainObject(conf[1]));
|
||||
|
||||
const validateSingleStep = conf => {
|
||||
if (validateStepArrayDefinition(conf)) {
|
||||
return true;
|
||||
}
|
||||
conf = castArray(conf);
|
||||
|
||||
if (conf.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [name, config] = parseConfig(conf[0]);
|
||||
return (isString(name) || isFunction(name)) && isPlainObject(config);
|
||||
};
|
||||
|
||||
const validateMultipleStep = conf => {
|
||||
return conf.every(conf => validateSingleStep(conf));
|
||||
};
|
||||
|
||||
function validatePlugin(conf) {
|
||||
return (
|
||||
isString(conf) ||
|
||||
(isArray(conf) &&
|
||||
(conf.length === 1 || conf.length === 2) &&
|
||||
(isString(conf[0]) || isPlainObject(conf[0])) &&
|
||||
(isNil(conf[1]) || isPlainObject(conf[1]))) ||
|
||||
(isPlainObject(conf) && (isNil(conf.path) || isString(conf.path) || isPlainObject(conf.path)))
|
||||
);
|
||||
}
|
||||
|
||||
function validateStep({multiple, required}, conf) {
|
||||
conf = castArray(conf).filter(Boolean);
|
||||
if (required) {
|
||||
return conf.length >= 1 && (multiple ? validateMultipleStep : validateSingleStep)(conf);
|
||||
}
|
||||
return conf.length === 0 || (multiple ? validateMultipleStep : validateSingleStep)(conf);
|
||||
}
|
||||
|
||||
function loadPlugin({cwd}, name, pluginsPath) {
|
||||
const basePath = pluginsPath[name]
|
||||
? dirname(resolveFrom.silent(__dirname, pluginsPath[name]) || resolveFrom(cwd, pluginsPath[name]))
|
||||
: __dirname;
|
||||
return isFunction(name) ? name : require(resolveFrom.silent(basePath, name) || resolveFrom(cwd, name));
|
||||
}
|
||||
|
||||
function parseConfig(plugin) {
|
||||
let path;
|
||||
let config;
|
||||
if (isArray(plugin)) {
|
||||
[path, config] = plugin;
|
||||
} else if (isPlainObject(plugin) && !isNil(plugin.path)) {
|
||||
({path, ...config} = plugin);
|
||||
} else {
|
||||
path = plugin;
|
||||
}
|
||||
return [path, config || {}];
|
||||
}
|
||||
|
||||
module.exports = {validatePlugin, validateStep, loadPlugin, parseConfig};
|
||||
+14
-1
@@ -1,7 +1,20 @@
|
||||
const {isFunction} = require('lodash');
|
||||
const hideSensitive = require('./hide-sensitive');
|
||||
|
||||
function extractErrors(err) {
|
||||
return err && isFunction(err[Symbol.iterator]) ? [...err] : [err];
|
||||
}
|
||||
|
||||
module.exports = {extractErrors};
|
||||
function hideSensitiveValues(env, objs) {
|
||||
const hideFunction = hideSensitive(env);
|
||||
return objs.map(obj => {
|
||||
Object.getOwnPropertyNames(obj).forEach(prop => {
|
||||
if (obj[prop]) {
|
||||
obj[prop] = hideFunction(obj[prop]);
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {extractErrors, hideSensitiveValues};
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ module.exports = async ({cwd, env, options: {repositoryUrl, tagFormat}}) => {
|
||||
const errors = [];
|
||||
|
||||
if (!(await isGitRepo({cwd, env}))) {
|
||||
errors.push(getError('ENOGITREPO'));
|
||||
errors.push(getError('ENOGITREPO', {cwd}));
|
||||
} else if (!repositoryUrl) {
|
||||
errors.push(getError('ENOREPOURL'));
|
||||
}
|
||||
|
||||
+18
-16
@@ -19,53 +19,55 @@
|
||||
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
|
||||
],
|
||||
"dependencies": {
|
||||
"@semantic-release/commit-analyzer": "^6.0.0",
|
||||
"@semantic-release/commit-analyzer": "^6.1.0",
|
||||
"@semantic-release/error": "^2.2.0",
|
||||
"@semantic-release/github": "^5.0.0",
|
||||
"@semantic-release/npm": "^4.0.0",
|
||||
"@semantic-release/release-notes-generator": "^7.0.0",
|
||||
"@semantic-release/github": "^5.1.0",
|
||||
"@semantic-release/npm": "^5.0.5",
|
||||
"@semantic-release/release-notes-generator": "^7.1.0",
|
||||
"aggregate-error": "^1.0.0",
|
||||
"chalk": "^2.3.0",
|
||||
"cosmiconfig": "^5.0.1",
|
||||
"debug": "^3.1.0",
|
||||
"env-ci": "^2.0.0",
|
||||
"execa": "^0.10.0",
|
||||
"debug": "^4.0.0",
|
||||
"env-ci": "^3.0.0",
|
||||
"execa": "^1.0.0",
|
||||
"figures": "^2.0.0",
|
||||
"find-versions": "^2.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"git-log-parser": "^1.2.0",
|
||||
"git-url-parse": "^10.0.1",
|
||||
"hook-std": "^1.0.1",
|
||||
"hook-std": "^1.1.0",
|
||||
"hosted-git-info": "^2.7.1",
|
||||
"lodash": "^4.17.4",
|
||||
"marked": "^0.4.0",
|
||||
"marked": "^0.5.0",
|
||||
"marked-terminal": "^3.0.0",
|
||||
"p-locate": "^3.0.0",
|
||||
"p-reduce": "^1.0.0",
|
||||
"read-pkg-up": "^4.0.0",
|
||||
"resolve-from": "^4.0.0",
|
||||
"semver": "^5.4.1",
|
||||
"signale": "^1.2.1",
|
||||
"yargs": "^12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.25.0",
|
||||
"clear-module": "^3.0.0",
|
||||
"codecov": "^3.0.0",
|
||||
"commitizen": "^2.9.6",
|
||||
"commitizen": "^3.0.0",
|
||||
"cz-conventional-changelog": "^2.0.0",
|
||||
"delay": "^3.0.0",
|
||||
"delay": "^4.0.0",
|
||||
"dockerode": "^2.5.2",
|
||||
"file-url": "^2.0.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"got": "^8.0.0",
|
||||
"got": "^9.0.0",
|
||||
"js-yaml": "^3.10.0",
|
||||
"mockserver-client": "^5.1.1",
|
||||
"nock": "^9.0.2",
|
||||
"nock": "^10.0.0",
|
||||
"nyc": "^12.0.1",
|
||||
"p-retry": "^2.0.0",
|
||||
"proxyquire": "^2.0.0",
|
||||
"sinon": "^6.0.0",
|
||||
"stream-buffers": "^3.0.2",
|
||||
"tempy": "^0.2.1",
|
||||
"xo": "^0.21.0"
|
||||
"xo": "^0.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.3"
|
||||
|
||||
+25
-1
@@ -1,6 +1,8 @@
|
||||
import test from 'ava';
|
||||
import {escapeRegExp} from 'lodash';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||
|
||||
const requireNoCache = proxyquire.noPreserveCache();
|
||||
|
||||
@@ -31,6 +33,9 @@ test.serial('Pass options to semantic-release API', async t => {
|
||||
'https://github/com/owner/repo.git',
|
||||
'-t',
|
||||
`v\${version}`,
|
||||
'-p',
|
||||
'plugin1',
|
||||
'plugin2',
|
||||
'-e',
|
||||
'config1',
|
||||
'config2',
|
||||
@@ -66,11 +71,12 @@ test.serial('Pass options to semantic-release API', async t => {
|
||||
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].plugins, ['plugin1', 'plugin2']);
|
||||
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].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']);
|
||||
@@ -92,6 +98,9 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
|
||||
'https://github/com/owner/repo.git',
|
||||
'--tag-format',
|
||||
`v\${version}`,
|
||||
'--plugins',
|
||||
'plugin1',
|
||||
'plugin2',
|
||||
'--extends',
|
||||
'config1',
|
||||
'config2',
|
||||
@@ -104,6 +113,7 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
|
||||
t.is(run.args[0][0].branch, 'master');
|
||||
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
|
||||
t.is(run.args[0][0].tagFormat, `v\${version}`);
|
||||
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
|
||||
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
|
||||
t.is(run.args[0][0].dryRun, true);
|
||||
|
||||
@@ -149,6 +159,8 @@ test.serial('Do not set properties in option for which arg is not in command lin
|
||||
t.false('debug' in run.args[0][0]);
|
||||
t.false('r' in run.args[0][0]);
|
||||
t.false('t' in run.args[0][0]);
|
||||
t.false('p' in run.args[0][0]);
|
||||
t.false('e' in run.args[0][0]);
|
||||
});
|
||||
|
||||
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
|
||||
@@ -208,3 +220,15 @@ test.serial('Return error code if semantic-release throw error', async t => {
|
||||
t.regex(t.context.errors, /semantic-release error/);
|
||||
t.is(exitCode, 1);
|
||||
});
|
||||
|
||||
test.serial('Hide sensitive environment variable values from the logs', async t => {
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
const run = stub().rejects(new Error(`Throw error: Exposing token ${env.MY_TOKEN}`));
|
||||
const argv = ['', ''];
|
||||
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv, env: {...process.env, ...env}}});
|
||||
|
||||
const exitCode = await cli();
|
||||
|
||||
t.regex(t.context.errors, new RegExp(`Throw error: Exposing token ${escapeRegExp(SECRET_REPLACEMENT)}`));
|
||||
t.is(exitCode, 1);
|
||||
});
|
||||
|
||||
@@ -1,94 +1,6 @@
|
||||
import test from 'ava';
|
||||
import plugins from '../../lib/definitions/plugins';
|
||||
import {RELEASE_NOTES_SEPARATOR} from '../../lib/definitions/constants';
|
||||
|
||||
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyConditions.configValidator({}));
|
||||
t.false(plugins.verifyConditions.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.verifyConditions.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyConditions.configValidator());
|
||||
t.true(plugins.verifyConditions.configValidator('plugin-path.js'));
|
||||
t.true(plugins.verifyConditions.configValidator(() => {}));
|
||||
t.true(plugins.verifyConditions.configValidator([{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.configValidator({}));
|
||||
t.false(plugins.analyzeCommits.configValidator({path: null}));
|
||||
t.false(plugins.analyzeCommits.configValidator([]));
|
||||
t.false(plugins.analyzeCommits.configValidator());
|
||||
|
||||
t.true(plugins.analyzeCommits.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.analyzeCommits.configValidator('plugin-path.js'));
|
||||
t.true(plugins.analyzeCommits.configValidator(() => {}));
|
||||
});
|
||||
|
||||
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyRelease.configValidator({}));
|
||||
t.false(plugins.verifyRelease.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyRelease.configValidator());
|
||||
t.true(plugins.verifyRelease.configValidator('plugin-path.js'));
|
||||
t.true(plugins.verifyRelease.configValidator(() => {}));
|
||||
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.generateNotes.configValidator({}));
|
||||
t.false(plugins.generateNotes.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.generateNotes.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.generateNotes.configValidator());
|
||||
t.true(plugins.generateNotes.configValidator('plugin-path.js'));
|
||||
t.true(plugins.generateNotes.configValidator(() => {}));
|
||||
t.true(plugins.generateNotes.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyRelease.configValidator({}));
|
||||
t.false(plugins.verifyRelease.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyRelease.configValidator());
|
||||
t.true(plugins.verifyRelease.configValidator('plugin-path.js'));
|
||||
t.true(plugins.verifyRelease.configValidator(() => {}));
|
||||
t.true(plugins.verifyRelease.configValidator([{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.configValidator({}));
|
||||
t.false(plugins.publish.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.publish.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.publish.configValidator());
|
||||
t.true(plugins.publish.configValidator('plugin-path.js'));
|
||||
t.true(plugins.publish.configValidator(() => {}));
|
||||
t.true(plugins.publish.configValidator([{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.configValidator({}));
|
||||
t.false(plugins.success.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.success.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.success.configValidator());
|
||||
t.true(plugins.success.configValidator('plugin-path.js'));
|
||||
t.true(plugins.success.configValidator(() => {}));
|
||||
t.true(plugins.success.configValidator([{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.configValidator({}));
|
||||
t.false(plugins.fail.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.fail.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.fail.configValidator());
|
||||
t.true(plugins.fail.configValidator('plugin-path.js'));
|
||||
t.true(plugins.fail.configValidator(() => {}));
|
||||
t.true(plugins.fail.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
import {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} from '../../lib/definitions/constants';
|
||||
|
||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||
t.false(plugins.analyzeCommits.outputValidator('invalid'));
|
||||
@@ -120,10 +32,22 @@ test('The "publish" plugin output, if defined, must be an object', t => {
|
||||
t.true(plugins.publish.outputValidator(''));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugins output are concatenated with separator', t => {
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(plugins.generateNotes.postprocess(['', 'note']), 'note');
|
||||
t.is(plugins.generateNotes.postprocess([undefined, 'note']), 'note');
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', undefined, 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(plugins.generateNotes.postprocess(['', 'note'], {env}), 'note');
|
||||
t.is(plugins.generateNotes.postprocess([undefined, 'note'], {env}), 'note');
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(
|
||||
plugins.generateNotes.postprocess(['note 1', undefined, 'note 2'], {env}),
|
||||
`note 1${RELEASE_NOTES_SEPARATOR}note 2`
|
||||
);
|
||||
|
||||
t.is(
|
||||
plugins.generateNotes.postprocess(
|
||||
[`Note 1: Exposing token ${env.MY_TOKEN}`, `Note 2: Exposing token ${SECRET_REPLACEMENT}`],
|
||||
{env}
|
||||
),
|
||||
`Note 1: Exposing token ${SECRET_REPLACEMENT}${RELEASE_NOTES_SEPARATOR}Note 2: Exposing token ${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
Vendored
-1
@@ -1,2 +1 @@
|
||||
module.exports = () => {};
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
|
||||
class InheritedError extends SemanticReleaseError {
|
||||
constructor(message, code, newProperty) {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
|
||||
+30
-3
@@ -8,6 +8,13 @@ import {stub} from 'sinon';
|
||||
import yaml from 'js-yaml';
|
||||
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
|
||||
|
||||
const DEFAULT_PLUGINS = [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/npm',
|
||||
'@semantic-release/github',
|
||||
];
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.plugins = stub().returns({});
|
||||
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
|
||||
@@ -69,6 +76,7 @@ test('Read options from package.json', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: options});
|
||||
@@ -89,6 +97,7 @@ test('Read options from .releaserc.yml', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json in repository root
|
||||
await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options));
|
||||
@@ -109,6 +118,7 @@ test('Read options from .releaserc.json', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json in repository root
|
||||
await outputJson(path.resolve(cwd, '.releaserc.json'), options);
|
||||
@@ -129,6 +139,7 @@ test('Read options from .releaserc.js', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json in repository root
|
||||
await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`);
|
||||
@@ -149,6 +160,7 @@ test('Read options from release.config.js', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json in repository root
|
||||
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`);
|
||||
@@ -176,6 +188,7 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async
|
||||
branch: 'branch_cli',
|
||||
repositoryUrl: 'http://cli-url.com/owner/package',
|
||||
tagFormat: `cli\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'};
|
||||
// Create package.json in repository root
|
||||
@@ -199,6 +212,7 @@ test('Read configuration from file path in "extends"', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json and shareable.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||
@@ -226,6 +240,7 @@ test('Read configuration from module path in "extends"', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json and shareable.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||
@@ -259,6 +274,7 @@ test('Read configuration from an array of paths in "extends"', async t => {
|
||||
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
|
||||
branch: 'test_branch',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json and shareable.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||
@@ -296,6 +312,7 @@ test('Prioritize configuration from config file over "extends"', async t => {
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json and shareable.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||
@@ -341,6 +358,7 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
|
||||
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
|
||||
branch: 'test_branch2',
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json, shareable1.json and shareable2.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||
@@ -366,11 +384,13 @@ test('Allow to unset properties defined in shareable config with "null"', async
|
||||
analyzeCommits: null,
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'https://host.null/owner/module.git',
|
||||
plugins: null,
|
||||
};
|
||||
const options1 = {
|
||||
generateNotes: 'generateNotes',
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: ['test-plugin'],
|
||||
};
|
||||
// Create package.json and shareable.json in repository root
|
||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||
@@ -378,19 +398,25 @@ test('Allow to unset properties defined in shareable config with "null"', async
|
||||
|
||||
const {options} = await t.context.getConfig({cwd});
|
||||
|
||||
// Verify the options contains the plugin config from shareable.json
|
||||
t.deepEqual(options, {...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits'])});
|
||||
// Verify the plugins module is called with the plugin options from shareable.json
|
||||
// Verify the options contains the plugin config from shareable.json and the default `plugins`
|
||||
t.deepEqual(options, {
|
||||
...omit(options1, ['analyzeCommits']),
|
||||
...omit(pkgOptions, ['extends', 'analyzeCommits']),
|
||||
plugins: DEFAULT_PLUGINS,
|
||||
});
|
||||
// Verify the plugins module is called with the plugin options from shareable.json and the default `plugins`
|
||||
t.deepEqual(t.context.plugins.args[0][0], {
|
||||
options: {
|
||||
...omit(options1, 'analyzeCommits'),
|
||||
...omit(pkgOptions, ['extends', 'analyzeCommits']),
|
||||
plugins: DEFAULT_PLUGINS,
|
||||
},
|
||||
cwd,
|
||||
});
|
||||
t.deepEqual(t.context.plugins.args[0][1], {
|
||||
generateNotes: './shareable.json',
|
||||
analyzeCommits: './shareable.json',
|
||||
'test-plugin': './shareable.json',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -407,6 +433,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a
|
||||
generateNotes: 'generateNotes',
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: false,
|
||||
};
|
||||
// Create package.json and release.config.js in repository root
|
||||
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`);
|
||||
|
||||
@@ -27,7 +27,7 @@ test('Get the highest non-prerelease valid tag', async t => {
|
||||
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: 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 %s associated with version %s', 'v2.0.0', '2.0.0']);
|
||||
t.deepEqual(t.context.log.args[0], ['Found git tag v2.0.0 associated with version 2.0.0']);
|
||||
});
|
||||
|
||||
test('Get the highest tag in the history of the current branch', async t => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import test from 'ava';
|
||||
import {spy} from 'sinon';
|
||||
import getLogger from '../lib/get-logger';
|
||||
|
||||
test('Expose "error", "success" and "log" functions', t => {
|
||||
const stdout = spy();
|
||||
const stderr = spy();
|
||||
const logger = getLogger({stdout: {write: stdout}, stderr: {write: stderr}});
|
||||
|
||||
logger.log('test log');
|
||||
logger.success('test success');
|
||||
logger.error('test error');
|
||||
|
||||
t.regex(stdout.args[0][0], /.*test log/);
|
||||
t.regex(stdout.args[1][0], /.*test success/);
|
||||
t.regex(stderr.args[0][0], /.*test error/);
|
||||
});
|
||||
@@ -69,7 +69,7 @@ export async function initBareRepo(repositoryUrl, branch = 'master') {
|
||||
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
|
||||
*/
|
||||
export async function gitCommits(messages, execaOpts) {
|
||||
await pReduce(messages, async (_, message) =>
|
||||
await pReduce(messages, (_, message) =>
|
||||
execa.stdout('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)
|
||||
);
|
||||
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
|
||||
@@ -112,7 +112,7 @@ export async function gitCheckout(branch, create = true, execaOpts) {
|
||||
*
|
||||
* @return {String} The sha of the head commit in the current git repository.
|
||||
*/
|
||||
export async function gitHead(execaOpts) {
|
||||
export function gitHead(execaOpts) {
|
||||
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function gitAddConfig(name, value, execaOpts) {
|
||||
*
|
||||
* @return {String} The sha of the commit associated with `tagName` on the local repository.
|
||||
*/
|
||||
export async function gitTagHead(tagName, execaOpts) {
|
||||
export function gitTagHead(tagName, execaOpts) {
|
||||
return execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ export async function gitRemoteTagHead(repositoryUrl, tagName, execaOpts) {
|
||||
*
|
||||
* @return {String} The tag associatedwith the sha in parameter or `null`.
|
||||
*/
|
||||
export async function gitCommitTag(gitHead, execaOpts) {
|
||||
export function gitCommitTag(gitHead, execaOpts) {
|
||||
return execa.stdout('git', ['describe', '--tags', '--exact-match', gitHead], execaOpts);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ async function start() {
|
||||
minTimeout: 1000,
|
||||
factor: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
throw new Error(`Couldn't start mock-server after 2 min`);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ async function mock(
|
||||
* @param {Object} expectation The expectation created with `mock` function.
|
||||
* @return {Promise} A Promise that resolves if the expectation is met or reject otherwise.
|
||||
*/
|
||||
async function verify(expectation) {
|
||||
function verify(expectation) {
|
||||
return client.verify(expectation);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import got from 'got';
|
||||
import delay from 'delay';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
|
||||
const IMAGE = 'semanticrelease/npm-registry-docker:latest';
|
||||
const SERVER_PORT = 15986;
|
||||
const COUCHDB_PORT = 5984;
|
||||
const SERVER_HOST = 'localhost';
|
||||
const COUCHDB_USER = 'admin';
|
||||
const COUCHDB_PASSWORD = 'password';
|
||||
const NPM_USERNAME = 'integration';
|
||||
const NPM_PASSWORD = 'suchsecure';
|
||||
const NPM_EMAIL = 'integration@test.com';
|
||||
@@ -15,7 +17,7 @@ const docker = new Docker();
|
||||
let container;
|
||||
|
||||
/**
|
||||
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
|
||||
* Download the `npm-registry-docker` Docker image, create a new container and start it.
|
||||
*/
|
||||
async function start() {
|
||||
await getStream(await docker.pull(IMAGE));
|
||||
@@ -24,10 +26,11 @@ async function start() {
|
||||
Tty: true,
|
||||
Image: IMAGE,
|
||||
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
|
||||
Env: [`COUCHDB_USER=${COUCHDB_USER}`, `COUCHDB_PASSWORD=${COUCHDB_PASSWORD}`],
|
||||
});
|
||||
|
||||
await container.start();
|
||||
await delay(3000);
|
||||
await delay(4000);
|
||||
|
||||
try {
|
||||
// Wait for the registry to be ready
|
||||
@@ -36,14 +39,14 @@ async function start() {
|
||||
minTimeout: 1000,
|
||||
factor: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
|
||||
} catch (error) {
|
||||
throw new Error(`Couldn't start npm-registry-docker after 2 min`);
|
||||
}
|
||||
|
||||
// Create user
|
||||
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
|
||||
json: true,
|
||||
auth: 'admin:admin',
|
||||
auth: `${COUCHDB_USER}:${COUCHDB_PASSWORD}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
_id: `org.couchdb.user:${NPM_USERNAME}`,
|
||||
@@ -66,7 +69,7 @@ const authEnv = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop and remote the `npm-docker-couchdb` Docker container.
|
||||
* Stop and remote the `npm-registry-docker` Docker container.
|
||||
*/
|
||||
async function stop() {
|
||||
await container.stop();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import test from 'ava';
|
||||
import {repeat} from 'lodash';
|
||||
import hideSensitive from '../lib/hide-sensitive';
|
||||
import {SECRET_REPLACEMENT, SECRET_MIN_SIZE} from '../lib/definitions/constants';
|
||||
|
||||
test('Replace multiple sensitive environment variable values', t => {
|
||||
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: 'secret'};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=${env.SOME_TOKEN}`),
|
||||
'https://user:[secure]@host.com?token=[secure]'
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com?token=${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,13 +15,16 @@ test('Replace multiple occurences of sensitive environment variable values', t =
|
||||
const env = {secretKey: 'secret'};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.secretKey}@host.com?token=${env.secretKey}`),
|
||||
'https://user:[secure]@host.com?token=[secure]'
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com?token=${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
test('Escape regexp special characters', t => {
|
||||
const env = {SOME_CREDENTIALS: 'p$^{.+}\\w[a-z]o.*rd'};
|
||||
t.is(hideSensitive(env)(`https://user:${env.SOME_CREDENTIALS}@host.com`), 'https://user:[secure]@host.com');
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.SOME_CREDENTIALS}@host.com`),
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com`
|
||||
);
|
||||
});
|
||||
|
||||
test('Accept "undefined" input', t => {
|
||||
@@ -34,10 +39,20 @@ test('Exclude empty environment variables from the regexp', t => {
|
||||
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: ''};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=`),
|
||||
'https://user:[secure]@host.com?token='
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com?token=`
|
||||
);
|
||||
});
|
||||
|
||||
test('Exclude empty environment variables from the regexp if there is only empty ones', t => {
|
||||
t.is(hideSensitive({SOME_PASSWORD: '', SOME_TOKEN: ' \n '})(`https://host.com?token=`), 'https://host.com?token=');
|
||||
});
|
||||
|
||||
test('Exclude environment variables with value shorter than SECRET_MIN_SIZE from the regexp', t => {
|
||||
const SHORT_TOKEN = repeat('a', SECRET_MIN_SIZE - 1);
|
||||
const LONG_TOKEN = repeat('b', SECRET_MIN_SIZE);
|
||||
const env = {SHORT_TOKEN, LONG_TOKEN};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${SHORT_TOKEN}@host.com?token=${LONG_TOKEN}`),
|
||||
`https://user:${SHORT_TOKEN}@host.com?token=${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
+279
-66
@@ -1,9 +1,11 @@
|
||||
import test from 'ava';
|
||||
import {escapeRegExp, isString} from 'lodash';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {spy, stub} from 'sinon';
|
||||
import {WritableStreamBuffer} from 'stream-buffers';
|
||||
import AggregateError from 'aggregate-error';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
import {COMMIT_NAME, COMMIT_EMAIL} from '../lib/definitions/constants';
|
||||
import {COMMIT_NAME, COMMIT_EMAIL, SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||
import {
|
||||
gitHead as getGitHead,
|
||||
gitTagHead,
|
||||
@@ -22,9 +24,13 @@ test.beforeEach(t => {
|
||||
// Stub the logger functions
|
||||
t.context.log = spy();
|
||||
t.context.error = spy();
|
||||
t.context.stdout = spy();
|
||||
t.context.stderr = spy();
|
||||
t.context.logger = {log: t.context.log, error: t.context.error, stdout: t.context.stdout, stderr: t.context.stderr};
|
||||
t.context.success = spy();
|
||||
t.context.logger = {
|
||||
log: t.context.log,
|
||||
error: t.context.error,
|
||||
success: t.context.success,
|
||||
scope: () => t.context.logger,
|
||||
};
|
||||
});
|
||||
|
||||
test('Plugins are called with expected values', async t => {
|
||||
@@ -58,6 +64,7 @@ test('Plugins are called with expected values', async t => {
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
plugins: false,
|
||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
@@ -68,10 +75,15 @@ test('Plugins are called with expected values', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, extendEnv: false, env}));
|
||||
const result = await semanticRelease(options, {
|
||||
cwd,
|
||||
env,
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
});
|
||||
|
||||
t.is(verifyConditions1.callCount, 1);
|
||||
t.deepEqual(verifyConditions1.args[0][0], config);
|
||||
@@ -159,6 +171,16 @@ test('Plugins are called with expected values', async t => {
|
||||
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
|
||||
]);
|
||||
|
||||
t.deepEqual(result, {
|
||||
lastRelease,
|
||||
commits: [commits[0]],
|
||||
nextRelease: {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`},
|
||||
releases: [
|
||||
{...release1, ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: '[Function: proxy]'},
|
||||
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||
@@ -193,10 +215,17 @@ test('Use custom tag format', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||
@@ -237,11 +266,18 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(generateNotes.callCount, 2);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
@@ -295,11 +331,13 @@ test('Call all "success" plugins even if one errors out', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options, {cwd, env: {}}));
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
);
|
||||
|
||||
t.is(success1.callCount, 1);
|
||||
t.deepEqual(success1.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]);
|
||||
@@ -322,20 +360,25 @@ test('Log all "verifyConditions" errors', async t => {
|
||||
const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
plugins: false,
|
||||
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
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], [
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR2 error 2']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR3 error 3']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 3], [
|
||||
'An error occurred while running semantic-release: %O',
|
||||
error1,
|
||||
]);
|
||||
@@ -371,14 +414,18 @@ test('Log all "verifyRelease" errors', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
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.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']);
|
||||
t.is(fail.callCount, 1);
|
||||
t.deepEqual(fail.args[0][0], config);
|
||||
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
|
||||
@@ -419,10 +466,17 @@ test('Dry-run skips publish and success', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
t.is(verifyConditions.callCount, 1);
|
||||
@@ -457,14 +511,18 @@ test('Dry-run skips fail', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
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.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']);
|
||||
t.is(fail.callCount, 0);
|
||||
});
|
||||
|
||||
@@ -504,10 +562,17 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: false, branch: 'master'}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
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);
|
||||
@@ -518,7 +583,7 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
|
||||
t.is(success.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
|
||||
test('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
@@ -547,12 +612,19 @@ test.serial('Dry-run does not print changelog if "generateNotes" return "undefin
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version %s:\n', '2.0.0']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version 2.0.0:']);
|
||||
});
|
||||
|
||||
test('Allow local releases with "noCi" option', async t => {
|
||||
@@ -591,10 +663,17 @@ test('Allow local releases with "noCi" option', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: false, branch: 'master', isPr: true}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
t.not(
|
||||
@@ -643,10 +722,17 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
|
||||
@@ -665,23 +751,28 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
|
||||
t.is(publish.args[0][1].nextRelease.notes, notes2);
|
||||
});
|
||||
|
||||
test('Returns falsy value if triggered by a PR', async t => {
|
||||
test('Returns false if triggered by a PR', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease({cwd, repositoryUrl}, {cwd, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(
|
||||
{cwd, repositoryUrl},
|
||||
{cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}
|
||||
)
|
||||
);
|
||||
t.is(
|
||||
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."
|
||||
);
|
||||
});
|
||||
|
||||
test('Returns falsy value if triggered on an outdated clone', async t => {
|
||||
test('Returns false if triggered on an outdated clone', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||
const repoDir = cwd;
|
||||
@@ -694,18 +785,22 @@ test('Returns falsy value if triggered on an outdated clone', async t => {
|
||||
await gitPush(repositoryUrl, 'master', {cwd});
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease({repositoryUrl}, {cwd: repoDir, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(
|
||||
{repositoryUrl},
|
||||
{cwd: repoDir, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}
|
||||
)
|
||||
);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
|
||||
"The local branch %s is behind the remote one, therefore a new version won't be published.",
|
||||
'master',
|
||||
"The local branch master is behind the remote one, therefore a new version won't be published.",
|
||||
]);
|
||||
});
|
||||
|
||||
test('Returns falsy value if not running from the configured branch', async t => {
|
||||
test('Returns false 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 {cwd, repositoryUrl} = await gitRepo(true);
|
||||
const options = {
|
||||
@@ -722,18 +817,25 @@ test('Returns falsy value if not running from the configured branch', async t =>
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
t.is(
|
||||
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 won’t be published.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Returns falsy value if there is no relevant changes', async t => {
|
||||
test('Returns false if there is no relevant changes', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
@@ -759,11 +861,18 @@ test('Returns falsy value if there is no relevant changes', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.is(verifyRelease.callCount, 0);
|
||||
t.is(generateNotes.callCount, 0);
|
||||
@@ -807,10 +916,15 @@ test('Exclude commits with [skip release] or [release skip] from analysis', asyn
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
await semanticRelease(options, {cwd, env: {}});
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
});
|
||||
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.is(analyzeCommits.args[0][1].commits.length, 2);
|
||||
@@ -830,15 +944,17 @@ test('Log both plugins errors and errors thrown by "fail" plugin', async t => {
|
||||
fail: [stub().rejects(failError1), stub().rejects(failError2)],
|
||||
};
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options, {cwd, env: {}}));
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
);
|
||||
|
||||
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']);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][0], 'ERR Plugin error');
|
||||
t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2);
|
||||
});
|
||||
|
||||
test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
||||
@@ -853,11 +969,13 @@ test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
||||
fail,
|
||||
};
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options, {cwd, env: {}}));
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
);
|
||||
|
||||
t.true(fail.notCalled);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
|
||||
@@ -868,10 +986,14 @@ test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found
|
||||
const {cwd} = await gitRepo();
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease({}, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease({}, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
// Verify error code and type
|
||||
t.is(errors[0].code, 'ENOREPOURL');
|
||||
@@ -902,13 +1024,97 @@ test('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const error = await t.throws(semanticRelease(options, {cwd, env: {}}), Error);
|
||||
const error = await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}),
|
||||
Error
|
||||
);
|
||||
t.regex(error.details, /string/);
|
||||
});
|
||||
|
||||
test('Hide sensitive information passed to "fail" plugin', async t => {
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
|
||||
const fail = stub().resolves();
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: stub().throws(
|
||||
new SemanticReleaseError(
|
||||
`Message: Exposing token ${env.MY_TOKEN}`,
|
||||
'ERR',
|
||||
`Details: Exposing token ${env.MY_TOKEN}`
|
||||
)
|
||||
),
|
||||
success: stub().resolves(),
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}),
|
||||
Error
|
||||
);
|
||||
|
||||
const error = fail.args[0][1].errors[0];
|
||||
|
||||
t.is(error.message, `Message: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
t.is(error.details, `Details: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
|
||||
Object.getOwnPropertyNames(error).forEach(prop => {
|
||||
if (isString(error[prop])) {
|
||||
t.notRegex(error[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Hide sensitive information passed to "success" plugin', async t => {
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
await gitCommits(['feat: initial release'], {cwd});
|
||||
await gitTagVersion('v1.0.0', undefined, {cwd});
|
||||
await gitCommits(['feat: new feature'], {cwd});
|
||||
await gitPush(repositoryUrl, 'master', {cwd});
|
||||
|
||||
const success = stub().resolves();
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: false,
|
||||
verifyRelease: false,
|
||||
prepare: false,
|
||||
publish: stub().resolves({
|
||||
name: `Name: Exposing token ${env.MY_TOKEN}`,
|
||||
url: `URL: Exposing token ${env.MY_TOKEN}`,
|
||||
}),
|
||||
success,
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
await semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()});
|
||||
|
||||
const release = success.args[0][1].releases[0];
|
||||
|
||||
t.is(release.name, `Name: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
t.is(release.url, `URL: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
|
||||
Object.getOwnPropertyNames(release).forEach(prop => {
|
||||
if (isString(release[prop])) {
|
||||
t.notRegex(release[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Get all commits including the ones not in the shallow clone', async t => {
|
||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||
await gitTagVersion('v1.0.0', undefined, {cwd});
|
||||
@@ -935,10 +1141,17 @@ test('Get all commits including the ones not in the shallow clone', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(analyzeCommits.args[0][1].commits.length, 3);
|
||||
});
|
||||
|
||||
+13
-10
@@ -4,6 +4,7 @@ import test from 'ava';
|
||||
import {escapeRegExp} from 'lodash';
|
||||
import {writeJson, readJson} from 'fs-extra';
|
||||
import execa from 'execa';
|
||||
import {WritableStreamBuffer} from 'stream-buffers';
|
||||
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
|
||||
import gitbox from './helpers/gitbox';
|
||||
@@ -16,11 +17,9 @@ const requireNoCache = proxyquire.noPreserveCache();
|
||||
|
||||
// Environment variables used with semantic-release cli (similar to what a user would setup)
|
||||
const env = {
|
||||
...npmRegistry.authEnv,
|
||||
GH_TOKEN: gitbox.gitCredential,
|
||||
GITHUB_URL: mockServer.url,
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
NPM_USERNAME: 'integration',
|
||||
NPM_PASSWORD: 'suchsecure',
|
||||
TRAVIS: 'true',
|
||||
CI: 'true',
|
||||
TRAVIS_BRANCH: 'master',
|
||||
@@ -29,9 +28,9 @@ const env = {
|
||||
// Environment variables used only for the local npm command used to do verification
|
||||
const testEnv = {
|
||||
...process.env,
|
||||
...npmRegistry.authEnv,
|
||||
npm_config_registry: npmRegistry.url,
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
|
||||
LEGACY_TOKEN: Buffer.from(`${env.NPM_USERNAME}:${env.NPM_PASSWORD}`, 'utf8').toString('base64'),
|
||||
};
|
||||
|
||||
const cli = require.resolve('../bin/semantic-release');
|
||||
@@ -467,6 +466,10 @@ test('Run via JS API', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {
|
||||
fail: false,
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -488,7 +491,7 @@ test('Run via JS API', async t => {
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit'], {cwd});
|
||||
t.log('$ Call semantic-release via API');
|
||||
await semanticRelease({fail: false, success: false}, {cwd, env});
|
||||
await semanticRelease(undefined, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()});
|
||||
|
||||
// Verify package.json and has been updated
|
||||
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
|
||||
@@ -552,9 +555,9 @@ test('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit'], {cwd});
|
||||
t.log('$ semantic-release');
|
||||
const {stdout, code} = await execa(cli, [], {env, cwd, reject: false});
|
||||
const {stderr, code} = await execa(cli, [], {env, cwd, reject: false});
|
||||
// Verify the type and message are logged
|
||||
t.regex(stdout, /EINHERITED Inherited error/);
|
||||
t.regex(stderr, /EINHERITED Inherited error/);
|
||||
t.is(code, 1);
|
||||
});
|
||||
|
||||
@@ -570,13 +573,13 @@ test('Exit with 1 if missing permission to push to the remote repository', async
|
||||
await gitCommits(['feat: Initial commit'], {cwd});
|
||||
await gitPush('origin', 'master', {cwd});
|
||||
t.log('$ semantic-release');
|
||||
const {stdout, code} = await execa(
|
||||
const {stderr, code} = await execa(
|
||||
cli,
|
||||
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
|
||||
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false}
|
||||
);
|
||||
// Verify the type and message are logged
|
||||
t.regex(stdout, /EGITNOPERMISSION/);
|
||||
t.regex(stderr, /EGITNOPERMISSION/);
|
||||
t.is(code, 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import test from 'ava';
|
||||
import {stub} from 'sinon';
|
||||
import logger from '../lib/logger';
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.log = stub(console, 'log');
|
||||
t.context.error = stub(console, 'error');
|
||||
});
|
||||
|
||||
test.afterEach.always(t => {
|
||||
t.context.log.restore();
|
||||
t.context.error.restore();
|
||||
});
|
||||
|
||||
test.serial('Basic log', t => {
|
||||
logger.log('test log');
|
||||
logger.error('test error');
|
||||
|
||||
t.regex(t.context.log.args[0][0], /.*test log/);
|
||||
t.regex(t.context.error.args[0][0], /.*test error/);
|
||||
});
|
||||
|
||||
test.serial('Log object', t => {
|
||||
const obj = {a: 1, b: '2'};
|
||||
logger.log(obj);
|
||||
logger.error(obj);
|
||||
|
||||
t.is(t.context.log.args[0][1], obj);
|
||||
t.is(t.context.error.args[0][1], obj);
|
||||
});
|
||||
|
||||
test.serial('Log with string formatting', t => {
|
||||
logger.log('test log %s', 'log value');
|
||||
logger.error('test error %s', 'error value');
|
||||
|
||||
t.regex(t.context.log.args[0][0], /.*test log/);
|
||||
t.regex(t.context.error.args[0][0], /.*test error/);
|
||||
t.is(t.context.log.args[0][1], 'log value');
|
||||
t.is(t.context.error.args[0][1], 'error value');
|
||||
});
|
||||
|
||||
test.serial('Log with error stacktrace and properties', t => {
|
||||
const error = new Error('error message');
|
||||
logger.error(error);
|
||||
const otherError = new Error('other error message');
|
||||
logger.error('test error %O', otherError);
|
||||
|
||||
t.is(t.context.error.args[0][1], error);
|
||||
t.regex(t.context.error.args[1][0], /.*test error/);
|
||||
t.is(t.context.error.args[1][1], otherError);
|
||||
});
|
||||
@@ -8,7 +8,15 @@ const cwd = process.cwd();
|
||||
test.beforeEach(t => {
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.logger = {log: t.context.log};
|
||||
t.context.error = stub();
|
||||
t.context.success = stub();
|
||||
t.context.stderr = {write: stub()};
|
||||
t.context.logger = {
|
||||
log: t.context.log,
|
||||
error: t.context.error,
|
||||
success: t.context.success,
|
||||
scope: () => t.context.logger,
|
||||
};
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from string', t => {
|
||||
@@ -21,7 +29,7 @@ test('Normalize and load plugin from string', t => {
|
||||
|
||||
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']);
|
||||
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']);
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from object', t => {
|
||||
@@ -34,7 +42,7 @@ test('Normalize and load plugin from object', t => {
|
||||
|
||||
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']);
|
||||
t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop"']);
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from a base file path', t => {
|
||||
@@ -44,11 +52,8 @@ test('Normalize and load plugin from a base file path', t => {
|
||||
|
||||
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',
|
||||
'verifyConditions',
|
||||
'./plugin-noop',
|
||||
'./test/fixtures',
|
||||
t.deepEqual(t.context.success.args[0], [
|
||||
'Loaded plugin "verifyConditions" from "./plugin-noop" in shareable config "./test/fixtures"',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -90,12 +95,17 @@ test('Normalize and load plugin that retuns multiple functions', t => {
|
||||
);
|
||||
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
|
||||
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/multi-plugin"']);
|
||||
});
|
||||
|
||||
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
|
||||
const analyzeCommits = stub().resolves(2);
|
||||
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'analyzeCommits', analyzeCommits, {});
|
||||
const plugin = normalize(
|
||||
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
|
||||
'analyzeCommits',
|
||||
analyzeCommits,
|
||||
{}
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
@@ -108,7 +118,12 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the
|
||||
|
||||
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
|
||||
const generateNotes = stub().resolves(2);
|
||||
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'generateNotes', generateNotes, {});
|
||||
const plugin = normalize(
|
||||
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
|
||||
'generateNotes',
|
||||
generateNotes,
|
||||
{}
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
@@ -120,11 +135,15 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the
|
||||
});
|
||||
|
||||
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
|
||||
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'publish', './plugin-identity', {
|
||||
'./plugin-identity': './test/fixtures',
|
||||
});
|
||||
const publish = stub().resolves(2);
|
||||
const plugin = normalize(
|
||||
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
|
||||
'publish',
|
||||
publish,
|
||||
{}
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin(2));
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
t.is(error.code, 'EPUBLISHOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
@@ -133,14 +152,34 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin
|
||||
t.regex(error.details, /2/);
|
||||
});
|
||||
|
||||
test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
|
||||
test('Plugin is called with "pluginConfig" (with object definition) and input', async t => {
|
||||
const pluginFunction = stub().resolves();
|
||||
const pluginConf = {path: pluginFunction, conf: 'confValue'};
|
||||
const options = {global: 'globalValue'};
|
||||
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
|
||||
await plugin('param');
|
||||
await plugin({param: 'param'});
|
||||
|
||||
t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
|
||||
t.true(
|
||||
pluginFunction.calledWithMatch(
|
||||
{conf: 'confValue', global: 'globalValue'},
|
||||
{param: 'param', logger: t.context.logger}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Plugin is called with "pluginConfig" (with array definition) and input', async t => {
|
||||
const pluginFunction = stub().resolves();
|
||||
const pluginConf = [pluginFunction, {conf: 'confValue'}];
|
||||
const options = {global: 'globalValue'};
|
||||
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
|
||||
await plugin({param: 'param'});
|
||||
|
||||
t.true(
|
||||
pluginFunction.calledWithMatch(
|
||||
{conf: 'confValue', global: 'globalValue'},
|
||||
{param: 'param', logger: t.context.logger}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Prevent plugins to modify "pluginConfig"', async t => {
|
||||
|
||||
+171
-12
@@ -11,7 +11,8 @@ const cwd = process.cwd();
|
||||
test.beforeEach(t => {
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.logger = {log: t.context.log};
|
||||
t.context.success = stub();
|
||||
t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger};
|
||||
});
|
||||
|
||||
test('Export default plugins', t => {
|
||||
@@ -28,7 +29,7 @@ test('Export default plugins', t => {
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Export plugins based on config', t => {
|
||||
test('Export plugins based on steps config', t => {
|
||||
const plugins = getPlugins(
|
||||
{
|
||||
cwd,
|
||||
@@ -54,6 +55,108 @@ test('Export plugins based on config', t => {
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Export plugins based on "plugins" config (array)', async t => {
|
||||
const plugin1 = {verifyConditions: stub(), publish: stub()};
|
||||
const plugin2 = {verifyConditions: stub(), verifyRelease: stub()};
|
||||
const plugins = getPlugins(
|
||||
{cwd, logger: t.context.logger, options: {plugins: [plugin1, [plugin2, {}]], verifyRelease: () => {}}},
|
||||
{}
|
||||
);
|
||||
|
||||
await plugins.verifyConditions({});
|
||||
t.true(plugin1.verifyConditions.calledOnce);
|
||||
t.true(plugin2.verifyConditions.calledOnce);
|
||||
|
||||
await plugins.publish({});
|
||||
t.true(plugin1.publish.calledOnce);
|
||||
|
||||
await plugins.verifyRelease({});
|
||||
t.true(plugin2.verifyRelease.notCalled);
|
||||
|
||||
// Verify the module returns a function for each plugin
|
||||
t.is(typeof plugins.verifyConditions, 'function');
|
||||
t.is(typeof plugins.analyzeCommits, 'function');
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.prepare, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Export plugins based on "plugins" config (single definition)', async t => {
|
||||
const plugin1 = {verifyConditions: stub(), publish: stub()};
|
||||
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {});
|
||||
|
||||
await plugins.verifyConditions({});
|
||||
t.true(plugin1.verifyConditions.calledOnce);
|
||||
|
||||
await plugins.publish({});
|
||||
t.true(plugin1.publish.calledOnce);
|
||||
|
||||
// Verify the module returns a function for each plugin
|
||||
t.is(typeof plugins.verifyConditions, 'function');
|
||||
t.is(typeof plugins.analyzeCommits, 'function');
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.prepare, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Use only last definition of single plugin steps declared in "plugins" config', async t => {
|
||||
const plugin1 = {analyzeCommits: stub()};
|
||||
const plugin2 = {analyzeCommits: stub()};
|
||||
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1, plugin2]}}, {});
|
||||
|
||||
await plugins.analyzeCommits({commits: []});
|
||||
t.true(plugin1.analyzeCommits.notCalled);
|
||||
t.true(plugin2.analyzeCommits.calledOnce);
|
||||
|
||||
// Verify the module returns a function for each plugin
|
||||
t.is(typeof plugins.verifyConditions, 'function');
|
||||
t.is(typeof plugins.analyzeCommits, 'function');
|
||||
t.is(typeof plugins.verifyRelease, 'function');
|
||||
t.is(typeof plugins.generateNotes, 'function');
|
||||
t.is(typeof plugins.prepare, 'function');
|
||||
t.is(typeof plugins.publish, 'function');
|
||||
t.is(typeof plugins.success, 'function');
|
||||
t.is(typeof plugins.fail, 'function');
|
||||
});
|
||||
|
||||
test('Merge global options, "plugins" options and step options', async t => {
|
||||
const plugin1 = [{verifyConditions: stub(), publish: stub()}, {pluginOpt1: 'plugin1'}];
|
||||
const plugin2 = [{verifyConditions: stub()}, {pluginOpt2: 'plugin2'}];
|
||||
const plugin3 = [stub(), {pluginOpt3: 'plugin3'}];
|
||||
const plugins = getPlugins(
|
||||
{
|
||||
cwd,
|
||||
logger: t.context.logger,
|
||||
options: {globalOpt: 'global', plugins: [plugin1, plugin2], verifyRelease: [plugin3]},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
await plugins.verifyConditions({});
|
||||
t.deepEqual(plugin1[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
|
||||
t.deepEqual(plugin2[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt2: 'plugin2'});
|
||||
|
||||
await plugins.publish({});
|
||||
t.deepEqual(plugin1[0].publish.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
|
||||
|
||||
await plugins.verifyRelease({});
|
||||
t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'});
|
||||
});
|
||||
|
||||
test('Unknown steps of plugins configured in "plugins" are ignored', t => {
|
||||
const plugin1 = {verifyConditions: () => {}, unknown: () => {}};
|
||||
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {});
|
||||
|
||||
t.is(typeof plugins.verifyConditions, 'function');
|
||||
t.is(plugins.unknown, undefined);
|
||||
});
|
||||
|
||||
test('Export plugins loaded from the dependency of a shareable config module', async t => {
|
||||
const cwd = tempy.directory();
|
||||
await copy(
|
||||
@@ -120,11 +223,23 @@ test('Export plugins loaded from the dependency of a shareable config file', asy
|
||||
test('Use default when only options are passed for a single plugin', t => {
|
||||
const analyzeCommits = {};
|
||||
const generateNotes = {};
|
||||
const publish = {};
|
||||
const success = () => {};
|
||||
const fail = [() => {}];
|
||||
|
||||
const plugins = getPlugins(
|
||||
{cwd, logger: t.context.logger, options: {analyzeCommits, generateNotes, success, fail}},
|
||||
{
|
||||
cwd,
|
||||
logger: t.context.logger,
|
||||
options: {
|
||||
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
|
||||
analyzeCommits,
|
||||
generateNotes,
|
||||
publish,
|
||||
success,
|
||||
fail,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
@@ -158,18 +273,21 @@ 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 plugins configuration are missing a path for plugin pipeline', t => {
|
||||
const errors = [...t.throws(() => getPlugins({cwd, logger: t.context.logger, options: {verifyConditions: {}}}, {}))];
|
||||
|
||||
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 => {
|
||||
test('Throw an error for each invalid plugin configuration', t => {
|
||||
const errors = [
|
||||
...t.throws(() =>
|
||||
getPlugins(
|
||||
{cwd, logger: t.context.logger, options: {verifyConditions: [{path: '@semantic-release/npm'}, {}]}},
|
||||
{
|
||||
cwd,
|
||||
logger: t.context.logger,
|
||||
options: {
|
||||
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
|
||||
verifyConditions: 1,
|
||||
analyzeCommits: [],
|
||||
verifyRelease: [{}],
|
||||
generateNotes: [{path: null}],
|
||||
},
|
||||
},
|
||||
{}
|
||||
)
|
||||
),
|
||||
@@ -177,4 +295,45 @@ test('Throw an error if an array of plugin configuration is missing a path for p
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINCONF');
|
||||
t.is(errors[2].name, 'SemanticReleaseError');
|
||||
t.is(errors[2].code, 'EPLUGINCONF');
|
||||
t.is(errors[3].name, 'SemanticReleaseError');
|
||||
t.is(errors[3].code, 'EPLUGINCONF');
|
||||
});
|
||||
|
||||
test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', t => {
|
||||
const errors = [
|
||||
...t.throws(() =>
|
||||
getPlugins(
|
||||
{
|
||||
cwd,
|
||||
logger: t.context.logger,
|
||||
options: {plugins: ['./test/fixtures/multi-plugin', './test/fixtures/plugin-noop', () => {}]},
|
||||
},
|
||||
{}
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINSCONF');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINSCONF');
|
||||
});
|
||||
|
||||
test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', t => {
|
||||
const errors = [
|
||||
...t.throws(() =>
|
||||
getPlugins({cwd, logger: t.context.logger, options: {plugins: [1, {path: 1}, [() => {}, {}, {}]]}}, {})
|
||||
),
|
||||
];
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINSCONF');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINSCONF');
|
||||
t.is(errors[2].name, 'SemanticReleaseError');
|
||||
t.is(errors[2].code, 'EPLUGINSCONF');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
import test from 'ava';
|
||||
import {validatePlugin, validateStep, loadPlugin, parseConfig} from '../../lib/plugins/utils';
|
||||
|
||||
test('validatePlugin', t => {
|
||||
const path = 'plugin-module';
|
||||
const options = {option1: 'value1', option2: 'value2'};
|
||||
|
||||
t.true(validatePlugin(path), 'String definition');
|
||||
t.true(validatePlugin({publish: () => {}}), 'Object definition');
|
||||
t.true(validatePlugin([path]), 'Array definition');
|
||||
t.true(validatePlugin([path, options]), 'Array definition with options');
|
||||
t.true(validatePlugin([{publish: () => {}}, options]), 'Array definition with options and path as object');
|
||||
t.true(validatePlugin({path}), 'Object with path definition');
|
||||
t.true(validatePlugin({path, ...options}), 'Object with path definition with options');
|
||||
t.true(
|
||||
validatePlugin({path: {publish: () => {}}, ...options}),
|
||||
'Object with path definition with options and path as object'
|
||||
);
|
||||
|
||||
t.false(validatePlugin(1), 'String definition, wrong path');
|
||||
t.false(validatePlugin([]), 'Array definition, missing path');
|
||||
t.false(validatePlugin([path, options, {}]), 'Array definition, additional parameter');
|
||||
t.false(validatePlugin([1]), 'Array definition, wrong path');
|
||||
t.false(validatePlugin([path, 1]), 'Array definition, wrong options');
|
||||
t.false(validatePlugin({path: 1}), 'Object definition, wrong path');
|
||||
});
|
||||
|
||||
test('validateStep: multiple/optional plugin configuration', t => {
|
||||
const type = {multiple: true, required: false};
|
||||
|
||||
// Empty config
|
||||
t.true(validateStep(type));
|
||||
t.true(validateStep(type, []));
|
||||
|
||||
// Single value definition
|
||||
t.true(validateStep(type, 'plugin-path.js'));
|
||||
t.true(validateStep(type, () => {}));
|
||||
t.true(validateStep(type, ['plugin-path.js']));
|
||||
t.true(validateStep(type, [() => {}]));
|
||||
t.false(validateStep(type, {}));
|
||||
t.false(validateStep(type, [{}]));
|
||||
|
||||
// Array type definition
|
||||
t.true(validateStep(type, [['plugin-path.js']]));
|
||||
t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
|
||||
t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
|
||||
t.false(validateStep(type, [['plugin-path.js', 1]]));
|
||||
|
||||
// Object type definition
|
||||
t.true(validateStep(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
|
||||
t.true(validateStep(type, {path: () => {}, options: 'value'}));
|
||||
t.false(validateStep(type, {path: null}));
|
||||
|
||||
// Considered as an Array of 2 definitions and not as one Array definition in case of a muliple plugin type
|
||||
t.false(validateStep(type, [() => {}, {options: 'value'}]));
|
||||
t.false(validateStep(type, ['plugin-path.js', {options: 'value'}]));
|
||||
|
||||
// Multiple definitions
|
||||
t.true(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', 1],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
{},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: null},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('validateStep: multiple/required plugin configuration', t => {
|
||||
const type = {multiple: true, required: true};
|
||||
|
||||
// Empty config
|
||||
t.false(validateStep(type));
|
||||
t.false(validateStep(type, []));
|
||||
|
||||
// Single value definition
|
||||
t.true(validateStep(type, 'plugin-path.js'));
|
||||
t.true(validateStep(type, () => {}));
|
||||
t.true(validateStep(type, ['plugin-path.js']));
|
||||
t.true(validateStep(type, [() => {}]));
|
||||
t.false(validateStep(type, {}));
|
||||
t.false(validateStep(type, [{}]));
|
||||
|
||||
// Array type definition
|
||||
t.true(validateStep(type, [['plugin-path.js']]));
|
||||
t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
|
||||
t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
|
||||
t.false(validateStep(type, [['plugin-path.js', 1]]));
|
||||
|
||||
// Object type definition
|
||||
t.true(validateStep(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
|
||||
t.true(validateStep(type, {path: () => {}, options: 'value'}));
|
||||
t.false(validateStep(type, {path: null}));
|
||||
|
||||
// Considered as an Array of 2 definitions and not as one Array definition in the case of a muliple plugin type
|
||||
t.false(validateStep(type, [() => {}, {options: 'value'}]));
|
||||
t.false(validateStep(type, ['plugin-path.js', {options: 'value'}]));
|
||||
|
||||
// Multiple definitions
|
||||
t.true(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', 1],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
{},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: null},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('validateStep: single/required plugin configuration', t => {
|
||||
const type = {multiple: false, required: true};
|
||||
|
||||
// Empty config
|
||||
t.false(validateStep(type));
|
||||
t.false(validateStep(type, []));
|
||||
|
||||
// Single value definition
|
||||
t.true(validateStep(type, 'plugin-path.js'));
|
||||
t.true(validateStep(type, () => {}));
|
||||
t.true(validateStep(type, ['plugin-path.js']));
|
||||
t.true(validateStep(type, [() => {}]));
|
||||
t.false(validateStep(type, {}));
|
||||
t.false(validateStep(type, [{}]));
|
||||
|
||||
// Array type definition
|
||||
t.true(validateStep(type, [['plugin-path.js']]));
|
||||
t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
|
||||
t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
|
||||
t.false(validateStep(type, [['plugin-path.js', 1]]));
|
||||
|
||||
// Object type definition
|
||||
t.true(validateStep(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
|
||||
t.true(validateStep(type, {path: () => {}, options: 'value'}));
|
||||
t.false(validateStep(type, {path: null}));
|
||||
|
||||
// Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type
|
||||
t.true(validateStep(type, [() => {}, {options: 'value'}]));
|
||||
t.true(validateStep(type, ['plugin-path.js', {options: 'value'}]));
|
||||
|
||||
// Multiple definitions
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('validateStep: single/optional plugin configuration', t => {
|
||||
const type = {multiple: false, required: false};
|
||||
|
||||
// Empty config
|
||||
t.true(validateStep(type));
|
||||
t.true(validateStep(type, []));
|
||||
|
||||
// Single value definition
|
||||
t.true(validateStep(type, 'plugin-path.js'));
|
||||
t.true(validateStep(type, () => {}));
|
||||
t.true(validateStep(type, ['plugin-path.js']));
|
||||
t.true(validateStep(type, [() => {}]));
|
||||
t.false(validateStep(type, {}));
|
||||
t.false(validateStep(type, [{}]));
|
||||
|
||||
// Array type definition
|
||||
t.true(validateStep(type, [['plugin-path.js']]));
|
||||
t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
|
||||
t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
|
||||
t.false(validateStep(type, [['plugin-path.js', 1]]));
|
||||
|
||||
// Object type definition
|
||||
t.true(validateStep(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
|
||||
t.true(validateStep(type, {path: () => {}, options: 'value'}));
|
||||
t.false(validateStep(type, {path: null}));
|
||||
|
||||
// Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type
|
||||
t.true(validateStep(type, [() => {}, {options: 'value'}]));
|
||||
t.true(validateStep(type, ['plugin-path.js', {options: 'value'}]));
|
||||
|
||||
// Multiple definitions
|
||||
t.false(
|
||||
validateStep(type, [
|
||||
'plugin-path.js',
|
||||
() => {},
|
||||
['plugin-path.js'],
|
||||
['plugin-path.js', {options: 'value'}],
|
||||
[() => {}, {options: 'value'}],
|
||||
{path: 'plugin-path.js'},
|
||||
{path: 'plugin-path.js', options: 'value'},
|
||||
{path: () => {}, options: 'value'},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('loadPlugin', t => {
|
||||
const cwd = process.cwd();
|
||||
const func = () => {};
|
||||
|
||||
t.is(require('../fixtures/plugin-noop'), loadPlugin({cwd: './test/fixtures'}, './plugin-noop', {}), 'From cwd');
|
||||
t.is(
|
||||
require('../fixtures/plugin-noop'),
|
||||
loadPlugin({cwd}, './plugin-noop', {'./plugin-noop': './test/fixtures'}),
|
||||
'From a shareable config context'
|
||||
);
|
||||
t.is(func, loadPlugin({cwd}, func, {}), 'Defined as a function');
|
||||
});
|
||||
|
||||
test('parseConfig', t => {
|
||||
const path = 'plugin-module';
|
||||
const options = {option1: 'value1', option2: 'value2'};
|
||||
|
||||
t.deepEqual(parseConfig(path), [path, {}], 'String definition');
|
||||
t.deepEqual(parseConfig({path}), [path, {}], 'Object definition');
|
||||
t.deepEqual(parseConfig({path, ...options}), [path, options], 'Object definition with options');
|
||||
t.deepEqual(parseConfig([path]), [path, {}], 'Array definition');
|
||||
t.deepEqual(parseConfig([path, options]), [path, options], 'Array definition with options');
|
||||
});
|
||||
Reference in New Issue
Block a user