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">
|
<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>
|
</a>
|
||||||
</p>
|
</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">
|
<p align="center">
|
||||||
<a href="https://www.npmjs.com/package/semantic-release">
|
<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">
|
<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
|
### Release steps
|
||||||
|
|
||||||
After running the tests the command `semantic-release` will execute the following steps:
|
After running the tests, the command `semantic-release` will execute the following steps:
|
||||||
|
|
||||||
| Step | Description |
|
| Step | Description |
|
||||||
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------|---------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| Verify Conditions | Verify all the conditions to proceed with the release with the [verify conditions plugins](docs/usage/plugins.md#verifyconditions-plugin). |
|
| Verify Conditions | Verify all the conditions to proceed with the release. |
|
||||||
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
|
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
|
||||||
| Analyze commits | Determine the type of release with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
|
| Analyze commits | Determine the type of release based on the commits added since the last release. |
|
||||||
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
|
| Verify release | Verify the release conformity. |
|
||||||
| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
|
| Generate notes | Generate release notes for the commits added since the last release. |
|
||||||
| Create Git tag | Create a Git tag corresponding to the new release version |
|
| Create Git tag | Create a Git tag corresponding to the new release version. |
|
||||||
| Prepare | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin). |
|
| Prepare | Prepare the release. |
|
||||||
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
|
| Publish | Publish the release. |
|
||||||
| Notify | Notify of new releases or errors with the [success](docs/usage/plugins.md#success-plugin) and [fail](docs/usage/plugins.md#fail-plugin) plugins. |
|
| Notify | Notify of new releases or errors. |
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- Usage
|
- Usage
|
||||||
|
- [Getting started](docs/usage/getting-started.md#getting-started)
|
||||||
- [Installation](docs/usage/installation.md#installation)
|
- [Installation](docs/usage/installation.md#installation)
|
||||||
- [CI Configuration](docs/usage/ci-configuration.md#ci-configuration)
|
- [CI Configuration](docs/usage/ci-configuration.md#ci-configuration)
|
||||||
- [Configuration](docs/usage/configuration.md#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)
|
- [CI configurations](docs/recipes/README.md)
|
||||||
- [Package managers and languages](docs/recipes/README.md)
|
- [Package managers and languages](docs/recipes/README.md)
|
||||||
- Developer guide
|
- Developer guide
|
||||||
|
- [JavaScript API](docs/developer-guide/js-api.md)
|
||||||
- [Plugins](docs/developer-guide/plugin.md)
|
- [Plugins](docs/developer-guide/plugin.md)
|
||||||
- [Shareable configuration](docs/developer-guide/shareable-configuration.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
|
- Support
|
||||||
|
- [Resources](docs/support/resources.md)
|
||||||
- [Frequently Asked Questions](docs/support/FAQ.md)
|
- [Frequently Asked Questions](docs/support/FAQ.md)
|
||||||
- [Troubleshooting](docs/support/troubleshooting.md)
|
- [Troubleshooting](docs/support/troubleshooting.md)
|
||||||
- [Node version requirement](docs/support/node-version.md)
|
- [Node version requirement](docs/support/node-version.md)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
- [Package managers and languages](docs/recipes/README.md)
|
- [Package managers and languages](docs/recipes/README.md)
|
||||||
|
|
||||||
## Developer guide
|
## Developer guide
|
||||||
|
- [JavaScript API](docs/developer-guide/js-api.md)
|
||||||
- [Plugin](docs/developer-guide/plugin.md)
|
- [Plugin](docs/developer-guide/plugin.md)
|
||||||
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
|
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ execa
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(error => {
|
||||||
console.error(`[semantic-release]: Git version ${MIN_GIT_VERSION} is required. No git binary found.`);
|
console.error(`[semantic-release]: Git version ${MIN_GIT_VERSION} is required. No git binary found.`);
|
||||||
console.error(err);
|
console.error(error);
|
||||||
process.exit(1);
|
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 = {
|
const stringList = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -20,12 +22,13 @@ Usage:
|
|||||||
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
|
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
|
||||||
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
|
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
|
||||||
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
|
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
|
||||||
|
.option('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'})
|
||||||
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
|
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
|
||||||
.option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'})
|
.option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'})
|
||||||
.option('verify-conditions', {...stringList, group: 'Plugins'})
|
.option('verify-conditions', {...stringList, group: 'Plugins'})
|
||||||
.option('analyze-commits', {type: 'string', group: 'Plugins'})
|
.option('analyze-commits', {type: 'string', group: 'Plugins'})
|
||||||
.option('verify-release', {...stringList, 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('prepare', {...stringList, group: 'Plugins'})
|
||||||
.option('publish', {...stringList, group: 'Plugins'})
|
.option('publish', {...stringList, group: 'Plugins'})
|
||||||
.option('success', {...stringList, group: 'Plugins'})
|
.option('success', {...stringList, group: 'Plugins'})
|
||||||
@@ -55,9 +58,9 @@ Usage:
|
|||||||
}
|
}
|
||||||
await require('.')(opts);
|
await require('.')(opts);
|
||||||
return 0;
|
return 0;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (err.name !== 'YError') {
|
if (error.name !== 'YError') {
|
||||||
console.error(err);
|
stderr.write(hideSensitive(env)(util.inspect(error, {colors: true})));
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Developer guide
|
# Developer guide
|
||||||
|
|
||||||
|
- [JavaScript API](js-api.md)
|
||||||
- [Plugins](plugin.md)
|
- [Plugins](plugin.md)
|
||||||
- [Shareable configuration](shareable-configuration.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
|
# 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
|
## 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)
|
- [@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
|
- `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)
|
- `publish`: Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
|
||||||
- [@semantic-release/git](https://github.com/semantic-release/git)
|
- [@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
|
- `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
|
- `prepare`: Push a release commit and tag, including configurable files
|
||||||
- [@semantic-release/changelog](https://github.com/semantic-release/changelog)
|
- [@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
|
- `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
|
- `prepare`: Create or update the changelog file in the local project repository
|
||||||
- [@semantic-release/exec](https://github.com/semantic-release/exec)
|
- [@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
|
- `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
|
- `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.
|
- `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
|
- `generateNotes`: 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
|
- `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
|
- `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
|
- `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
|
- `fail`: Execute a shell command to notify of a failed release
|
||||||
|
|
||||||
## Community plugins
|
## 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.
|
[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
|
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker)
|
||||||
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
|
- `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.
|
- `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
|
- [semantic-release-gcr](https://github.com/carlos-cubas/semantic-release-gcr)
|
||||||
- **verifyConditions** Verify the presence and the validity of the vsce authentication and release configuration
|
- `verifyConditions`: Verify that all needed configuration is present and login to the Docker registry.
|
||||||
- **prepare** Create a `.vsix` for distribution
|
- `publish`: Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
|
||||||
- **publish** Publish the package to the Visual Studio Code marketplace
|
- [semantic-release-vsce](https://github.com/raix/semantic-release-vsce)
|
||||||
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
|
- `verifyConditions`: Verify the presence and the validity of the vsce authentication and release configuration
|
||||||
- [verifyConditions](https://github.com/piercus/semantic-release-verify-deps) Check the dependencies format against a regexp before a release
|
- `prepare`: Create a `.vsix` for distribution
|
||||||
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome) Set of semantic-release plugins for publishing a Chrome extension release.
|
- `publish`: Publish the package to the Visual Studio Code marketplace
|
||||||
- [verifyConditions](https://github.com/GabrielDuarteM/semantic-release-chrome#verifyconditions) Verify the presence of the authentication (set via environment variables).
|
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
|
||||||
- [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.
|
- `verifyConditions`: Check the dependencies format against a regexp before a release
|
||||||
- [publish](https://github.com/GabrielDuarteM/semantic-release-chrome#publish) Uploads the generated zip file to the webstore, and publish the item.
|
- [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?
|
## 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
|
```bash
|
||||||
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npx semantic-release --no-ci
|
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npx semantic-release --no-ci
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|
- [Getting started](getting-started.md#getting-started)
|
||||||
- [Installation](installation.md#installation)
|
- [Installation](installation.md#installation)
|
||||||
- [CI Configuration](ci-configuration.md#ci-configuration)
|
- [CI Configuration](ci-configuration.md#ci-configuration)
|
||||||
- [Configuration](configuration.md#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:
|
**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 |
|
| 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). |
|
| `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). |
|
| `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). |
|
| `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` | [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. |
|
||||||
|
|
||||||
`GIT_CREDENTIALS` must be the Git username and password in the format `<username>:<password>`.
|
|
||||||
|
|
||||||
Alternatively the Git authentication can be set up via [SSH keys](../recipes/git-auth-ssh-keys.md).
|
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.
|
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.
|
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
|
# 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 `.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.config.js` file that exports an object
|
||||||
- A `release` key in the project's `package.json` file
|
- 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.
|
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:
|
Via `release` key in the project's `package.json` file:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -38,27 +44,23 @@ Via `.releaserc` file:
|
|||||||
$ semantic-release
|
$ semantic-release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Via CLI argument:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ semantic-release --branch next
|
||||||
|
```
|
||||||
|
|
||||||
**Note**: CLI arguments take precedence over options configured in the configuration file.
|
**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**: 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.
|
**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
|
## Options
|
||||||
|
|
||||||
### extends
|
### extends
|
||||||
|
|
||||||
Type: `Array`, `String`
|
Type: `Array`, `String`<br>
|
||||||
|
|
||||||
CLI arguments: `-e`, `--extends`
|
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.
|
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
|
### branch
|
||||||
|
|
||||||
Type: `String`
|
Type: `String`<br>
|
||||||
|
Default: `master`<br>
|
||||||
Default: `master`
|
|
||||||
|
|
||||||
CLI arguments: `-b`, `--branch`
|
CLI arguments: `-b`, `--branch`
|
||||||
|
|
||||||
The branch on which releases should happen.
|
The branch on which releases should happen.
|
||||||
|
|
||||||
### repositoryUrl
|
### repositoryUrl
|
||||||
|
|
||||||
Type: `String`
|
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>
|
||||||
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)
|
|
||||||
|
|
||||||
CLI arguments: `-r`, `--repository-url`
|
CLI arguments: `-r`, `--repository-url`
|
||||||
|
|
||||||
The git 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
|
### tagFormat
|
||||||
|
|
||||||
Type: `String`
|
Type: `String`<br>
|
||||||
|
Default: `v${version}`<br>
|
||||||
Default: `v${version}`
|
|
||||||
|
|
||||||
CLI arguments: `-t`, `--tag-format`
|
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.
|
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).
|
**Note**: The `tagFormat` must contain the `version` variable exactly once and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
|
||||||
|
|
||||||
|
### plugins
|
||||||
|
|
||||||
|
Type: `Array`<br>
|
||||||
|
Default: `['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/npm', '@semantic-release/github']`<br>
|
||||||
|
CLI arguments: `-p`, `--plugins`
|
||||||
|
|
||||||
|
Define the list of plugins to use. Plugins will run in series, in the order defined, for each [steps](../../README.md#release-steps) if they implement it.
|
||||||
|
|
||||||
|
Plugins configuration can defined by wrapping the name and an options object in an array.
|
||||||
|
|
||||||
|
See [Plugins configuration](plugins.md#configuration) for more details.
|
||||||
|
|
||||||
### dryRun
|
### dryRun
|
||||||
|
|
||||||
Type: `Boolean`
|
Type: `Boolean`<br>
|
||||||
|
Default: `false` if running in a CI environment, `true` otherwise<br>
|
||||||
Default: `false` if running in a CI environment, `true` otherwise
|
|
||||||
|
|
||||||
CLI arguments: `-d`, `--dry-run`
|
CLI arguments: `-d`, `--dry-run`
|
||||||
|
|
||||||
Dry-run mode, skip publishing, print next version and release notes.
|
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 `fasle` to skip Continuous Integration environment verifications. This allows for making releases from a local machine.
|
||||||
|
|
||||||
CLI arguments: `--no-ci`
|
**Note**: The CLI arguments `--no-ci` is equivalent to `--ci false`.
|
||||||
|
|
||||||
Skip Continuous Integration environment verifications. This allows for making releases from a local machine.
|
|
||||||
|
|
||||||
### debug
|
### debug
|
||||||
|
|
||||||
Type: `Boolean`
|
Type: `Boolean`<br>
|
||||||
|
Default: `false`<br>
|
||||||
Default: `false`
|
|
||||||
|
|
||||||
CLI argument: `--debug`
|
CLI argument: `--debug`
|
||||||
|
|
||||||
Output debugging information. It can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
|
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`
|
| Variable | Description | Default |
|
||||||
|
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------|
|
||||||
Default: `['@semantic-release/npm', '@semantic-release/github']`
|
| `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. |
|
||||||
CLI argument: `--verify-conditions`
|
| `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. |
|
||||||
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.
|
|
||||||
|
|||||||
@@ -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
|
# 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
|
```json
|
||||||
{
|
{
|
||||||
"release": {
|
"plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm"]
|
||||||
"verifyConditions": [
|
}
|
||||||
{
|
```
|
||||||
"path": "@semantic-release/exec",
|
|
||||||
"cmd": "verify-conditions.sh"
|
## Plugin ordering
|
||||||
},
|
|
||||||
"@semantic-release/npm",
|
For each [release step](../../README.md#release-steps) the plugins that implement that step will be executed in the order in which the are defined.
|
||||||
"@semantic-release/github"
|
|
||||||
],
|
```json
|
||||||
"analyzeCommits": "custom-plugin",
|
{
|
||||||
"verifyRelease": [
|
"plugins": [
|
||||||
{
|
"@semantic-release/commit-analyzer",
|
||||||
"path": "@semantic-release/exec",
|
"@semantic-release/release-notes-generator",
|
||||||
"cmd": "verify-release.sh"
|
"@semantic-release/npm",
|
||||||
},
|
"@semantic-release/git"
|
||||||
],
|
]
|
||||||
"generateNotes": "./build/my-plugin.js",
|
}
|
||||||
"githubUrl": "https://my-ghe.com",
|
```
|
||||||
"githubApiPathPrefix": "/api-prefix"
|
|
||||||
}
|
With this configuration **semantic-release** will:
|
||||||
|
- execute the `verifyConditions` implementation of `@semantic-release/npm` then `@semantic-release/git`
|
||||||
|
- execute the `analyzeCommits` implementation of `@semantic-release/commit-analyzer`
|
||||||
|
- execute the `prepare` implementation of `@semantic-release/npm` then `@semantic-release/git`
|
||||||
|
- execute the `generateNotes` implementation of `@semantic-release/release-notes-generator`
|
||||||
|
- execute the `publish` implementation of `@semantic-release/npm`
|
||||||
|
|
||||||
|
## Plugin options
|
||||||
|
|
||||||
|
A plugin options can specified by wrapping the name and an options object in an array. Options configured this way will be passed only to that specific plugin.
|
||||||
|
|
||||||
|
Global plugin options can defined at the root of the **semantic-release** configuration object. Options configured this way will be passed to all plugins.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
["@semantic-release/github", {
|
||||||
|
"assets": ["dist/**"]
|
||||||
|
}],
|
||||||
|
"@semantic-release/git"
|
||||||
|
],
|
||||||
|
"preset": "angular"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
With this configuration:
|
With this configuration:
|
||||||
- the `custom-plugin` npm module will be used to [analyze commits](#analyzecommits-plugin)
|
- All plugins will receive the `preset` option, which will be used by both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` (and ignored by `@semantic-release/github` and `@semantic-release/git`)
|
||||||
- the `./build/my-plugin.js` script will be used to [generate release notes](#generatenotes-plugin)
|
- The `@semantic-release/github` plugin will receive the `assets` options (`@semantic-release/git` will not receive it and therefore will use it's default value for that option)
|
||||||
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec), [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/github`](https://github.com/semantic-release/github) plugins will be used to [verify conditions](#verifyconditions-plugin)
|
|
||||||
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin will be used to [verify the release](#verifyrelease-plugin)
|
|
||||||
- the `cmd` option will be set to `verify-conditions.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify conditions](#verifyconditions-plugin)
|
|
||||||
- the `cmd` option will be set to `verify-release.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify the release](#verifyrelease-plugin)
|
|
||||||
- the `githubUrl` and `githubApiPathPrefix` options will be set to respectively `https://my-ghe.com` and `/api-prefix` for all plugins
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const process = require('process');
|
const {template, pick} = require('lodash');
|
||||||
const {template} = require('lodash');
|
|
||||||
const marked = require('marked');
|
const marked = require('marked');
|
||||||
const TerminalRenderer = require('marked-terminal');
|
const TerminalRenderer = require('marked-terminal');
|
||||||
const envCi = require('env-ci');
|
const envCi = require('env-ci');
|
||||||
@@ -13,7 +12,7 @@ const getCommits = require('./lib/get-commits');
|
|||||||
const getLastRelease = require('./lib/get-last-release');
|
const getLastRelease = require('./lib/get-last-release');
|
||||||
const {extractErrors} = require('./lib/utils');
|
const {extractErrors} = require('./lib/utils');
|
||||||
const getGitAuthUrl = require('./lib/get-git-auth-url');
|
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 {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
|
||||||
const getError = require('./lib/get-error');
|
const getError = require('./lib/get-error');
|
||||||
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
|
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()});
|
marked.setOptions({renderer: new TerminalRenderer()});
|
||||||
|
|
||||||
async function run(context, plugins) {
|
async function run(context, plugins) {
|
||||||
const {isCi, branch: ciBranch, isPr} = envCi();
|
|
||||||
const {cwd, env, options, logger} = context;
|
const {cwd, env, options, logger} = context;
|
||||||
|
const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
|
||||||
|
|
||||||
if (!isCi && !options.dryRun && !options.noCi) {
|
if (!isCi && !options.dryRun && !options.noCi) {
|
||||||
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
|
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) {
|
if (isCi && isPr && !options.noCi) {
|
||||||
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
|
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) {
|
if (ciBranch !== options.branch) {
|
||||||
@@ -53,6 +52,7 @@ async function run(context, plugins) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
logger.success(`Run automated release from branch ${ciBranch}`);
|
||||||
|
|
||||||
await verify(context);
|
await verify(context);
|
||||||
|
|
||||||
@@ -60,19 +60,18 @@ async function run(context, plugins) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
|
await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
|
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
|
||||||
logger.log(
|
logger.log(
|
||||||
"The local branch %s is behind the remote one, therefore a new version won't be published.",
|
`The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.`
|
||||||
options.branch
|
|
||||||
);
|
);
|
||||||
return false;
|
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});
|
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);
|
await plugins.verifyConditions(context);
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ async function run(context, plugins) {
|
|||||||
|
|
||||||
if (!nextRelease.type) {
|
if (!nextRelease.type) {
|
||||||
logger.log('There are no relevant changes, so no new version is released.');
|
logger.log('There are no relevant changes, so no new version is released.');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
context.nextRelease = nextRelease;
|
context.nextRelease = nextRelease;
|
||||||
nextRelease.version = getNextVersion(context);
|
nextRelease.version = getNextVersion(context);
|
||||||
@@ -95,35 +94,36 @@ async function run(context, plugins) {
|
|||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
const notes = await plugins.generateNotes(context);
|
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) {
|
if (notes) {
|
||||||
logger.stdout(`${marked(notes)}\n`);
|
context.stdout.write(marked(notes));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nextRelease.notes = await plugins.generateNotes(context);
|
nextRelease.notes = await plugins.generateNotes(context);
|
||||||
await plugins.prepare(context);
|
await plugins.prepare(context);
|
||||||
|
|
||||||
// Create the tag before calling the publish plugins as some require the tag to exists
|
// 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 tag(nextRelease.gitTag, {cwd, env});
|
||||||
await push(options.repositoryUrl, options.branch, {cwd, env});
|
await push(options.repositoryUrl, options.branch, {cwd, env});
|
||||||
|
logger.success(`Created tag ${nextRelease.gitTag}`);
|
||||||
|
|
||||||
context.releases = await plugins.publish(context);
|
context.releases = await plugins.publish(context);
|
||||||
|
|
||||||
await plugins.success(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));
|
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
|
||||||
for (const error of errors) {
|
for (const error of errors) {
|
||||||
if (error.semanticRelease) {
|
if (error.semanticRelease) {
|
||||||
logger.log(`%s ${error.message}`, error.code);
|
logger.error(`${error.code} ${error.message}`);
|
||||||
if (error.details) {
|
if (error.details) {
|
||||||
logger.stderr(`${marked(error.details)}\n`);
|
stderr.write(marked(error.details));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.error('An error occurred while running semantic-release: %O', error);
|
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) {
|
async function callFail(context, plugins, err) {
|
||||||
const errors = extractErrors(error).filter(error => error.semanticRelease);
|
const errors = extractErrors(err).filter(err => err.semanticRelease);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
try {
|
try {
|
||||||
await plugins.fail({...context, errors});
|
await plugins.fail({...context, errors});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logErrors(context, err);
|
logErrors(context, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) => {
|
module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
|
||||||
const context = {cwd, env, logger};
|
const {unhook} = hookStd(
|
||||||
context.logger.log(`Running %s version %s`, pkg.name, pkg.version);
|
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
|
||||||
const {unhook} = hookStd({silent: false}, hideSensitive(context.env));
|
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 {
|
try {
|
||||||
const {plugins, options} = await getConfig(context, opts);
|
const {plugins, options} = await getConfig(context, opts);
|
||||||
context.options = options;
|
context.options = options;
|
||||||
@@ -153,15 +157,15 @@ module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) =>
|
|||||||
const result = await run(context, plugins);
|
const result = await run(context, plugins);
|
||||||
unhook();
|
unhook();
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await callFail(context, plugins, err);
|
await callFail(context, plugins, error);
|
||||||
}
|
}
|
||||||
throw err;
|
throw error;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logErrors(context, err);
|
logErrors(context, error);
|
||||||
unhook();
|
unhook();
|
||||||
throw err;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,4 +10,14 @@ const RELEASE_NOTES_SEPARATOR = '\n\n';
|
|||||||
|
|
||||||
const SECRET_REPLACEMENT = '[secure]';
|
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}`;
|
const linkify = file => `${homepage}/blob/caribou/${file}`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ENOGITREPO: () => ({
|
ENOGITREPO: ({cwd}) => ({
|
||||||
message: 'Not running from a git repository.',
|
message: 'Not running from a git repository.',
|
||||||
details: `The \`semantic-release\` command must be executed 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.`,
|
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)}\`.`,
|
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
|
||||||
}),
|
}),
|
||||||
EPLUGINCONF: ({type, pluginConf}) => ({
|
EPLUGINCONF: ({type, multiple, required, pluginConf}) => ({
|
||||||
message: `The \`${type}\` plugin configuration is invalid.`,
|
message: `The \`${type}\` plugin configuration is invalid.`,
|
||||||
details: `The [${type} plugin configuration](${linkify(
|
details: `The [${type} plugin configuration](${linkify(`docs/usage/plugins.md#${toLower(type)}-plugin`)}) ${
|
||||||
`docs/usage/plugins.md#${toLower(type)}-plugin`
|
required ? 'is required and ' : ''
|
||||||
)}) 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.
|
}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}) => ({
|
EPLUGIN: ({pluginName, type}) => ({
|
||||||
message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`,
|
message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`,
|
||||||
|
|||||||
+23
-20
@@ -1,18 +1,19 @@
|
|||||||
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
|
const {isString, isPlainObject} = require('lodash');
|
||||||
const {gitHead} = require('../git');
|
const {gitHead} = require('../git');
|
||||||
|
const hideSensitive = require('../hide-sensitive');
|
||||||
|
const {hideSensitiveValues} = require('../utils');
|
||||||
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
|
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
|
||||||
|
|
||||||
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
verifyConditions: {
|
verifyConditions: {
|
||||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
pipelineConfig: () => ({settleAll: true}),
|
pipelineConfig: () => ({settleAll: true}),
|
||||||
},
|
},
|
||||||
analyzeCommits: {
|
analyzeCommits: {
|
||||||
default: '@semantic-release/commit-analyzer',
|
default: ['@semantic-release/commit-analyzer'],
|
||||||
configValidator: conf => Boolean(conf) && validatePluginConfig(conf),
|
multiple: false,
|
||||||
|
required: true,
|
||||||
outputValidator: output => !output || RELEASE_TYPE.includes(output),
|
outputValidator: output => !output || RELEASE_TYPE.includes(output),
|
||||||
preprocess: ({commits, ...inputs}) => ({
|
preprocess: ({commits, ...inputs}) => ({
|
||||||
...inputs,
|
...inputs,
|
||||||
@@ -21,13 +22,13 @@ module.exports = {
|
|||||||
postprocess: ([result]) => result,
|
postprocess: ([result]) => result,
|
||||||
},
|
},
|
||||||
verifyRelease: {
|
verifyRelease: {
|
||||||
default: false,
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
pipelineConfig: () => ({settleAll: true}),
|
pipelineConfig: () => ({settleAll: true}),
|
||||||
},
|
},
|
||||||
generateNotes: {
|
generateNotes: {
|
||||||
default: ['@semantic-release/release-notes-generator'],
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
outputValidator: output => !output || isString(output),
|
outputValidator: output => !output || isString(output),
|
||||||
pipelineConfig: () => ({
|
pipelineConfig: () => ({
|
||||||
getNextInput: ({nextRelease, ...context}, notes) => ({
|
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: {
|
prepare: {
|
||||||
default: ['@semantic-release/npm'],
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
pipelineConfig: ({generateNotes}, logger) => ({
|
pipelineConfig: ({generateNotes}, logger) => ({
|
||||||
getNextInput: async context => {
|
getNextInput: async context => {
|
||||||
const newGitHead = await gitHead({cwd: context.cwd});
|
const newGitHead = await gitHead({cwd: context.cwd});
|
||||||
@@ -59,8 +60,8 @@ module.exports = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
publish: {
|
publish: {
|
||||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
outputValidator: output => !output || isPlainObject(output),
|
outputValidator: output => !output || isPlainObject(output),
|
||||||
pipelineConfig: () => ({
|
pipelineConfig: () => ({
|
||||||
// Add `nextRelease` and plugin properties to published release
|
// Add `nextRelease` and plugin properties to published release
|
||||||
@@ -72,13 +73,15 @@ module.exports = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
default: ['@semantic-release/github'],
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
pipelineConfig: () => ({settleAll: true}),
|
pipelineConfig: () => ({settleAll: true}),
|
||||||
|
preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}),
|
||||||
},
|
},
|
||||||
fail: {
|
fail: {
|
||||||
default: ['@semantic-release/github'],
|
multiple: true,
|
||||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
required: false,
|
||||||
pipelineConfig: () => ({settleAll: true}),
|
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();
|
commit.gitTags = commit.gitTags.trim();
|
||||||
return commit;
|
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);
|
debug('Parsed commits: %o', commits);
|
||||||
return 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 readPkgUp = require('read-pkg-up');
|
||||||
const cosmiconfig = require('cosmiconfig');
|
const cosmiconfig = require('cosmiconfig');
|
||||||
const resolveFrom = require('resolve-from');
|
const resolveFrom = require('resolve-from');
|
||||||
@@ -35,7 +35,7 @@ module.exports = async (context, opts) => {
|
|||||||
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
|
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
|
||||||
// so those plugin will be loaded relatively to the config file
|
// so those plugin will be loaded relatively to the config file
|
||||||
Object.entries(extendsOpts).reduce((pluginsPath, [option, value]) => {
|
Object.entries(extendsOpts).reduce((pluginsPath, [option, value]) => {
|
||||||
if (PLUGINS_DEFINITIONS[option]) {
|
if (PLUGINS_DEFINITIONS[option] || option === 'plugins') {
|
||||||
castArray(value)
|
castArray(value)
|
||||||
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
|
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
|
||||||
.map(plugin => (isString(plugin) ? plugin : plugin.path))
|
.map(plugin => (isString(plugin) ? plugin : plugin.path))
|
||||||
@@ -57,8 +57,14 @@ module.exports = async (context, opts) => {
|
|||||||
branch: 'master',
|
branch: 'master',
|
||||||
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
|
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: [
|
||||||
|
'@semantic-release/commit-analyzer',
|
||||||
|
'@semantic-release/release-notes-generator',
|
||||||
|
'@semantic-release/npm',
|
||||||
|
'@semantic-release/github',
|
||||||
|
],
|
||||||
// Remove `null` and `undefined` options so they can be replaced with default ones
|
// Remove `null` and `undefined` options so they can be replaced with default ones
|
||||||
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
|
...pickBy(options, option => !isNil(option)),
|
||||||
};
|
};
|
||||||
|
|
||||||
debug('options values: %O', options);
|
debug('options values: %O', options);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const {parse, format} = require('url');
|
const {parse, format} = require('url');
|
||||||
const {isUndefined} = require('lodash');
|
const {isNil} = require('lodash');
|
||||||
const gitUrlParse = require('git-url-parse');
|
const gitUrlParse = require('git-url-parse');
|
||||||
const hostedGitInfo = require('hosted-git-info');
|
const hostedGitInfo = require('hosted-git-info');
|
||||||
const {verifyAuth} = require('./git');
|
const {verifyAuth} = require('./git');
|
||||||
@@ -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)
|
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
|
||||||
try {
|
try {
|
||||||
await verifyAuth(repositoryUrl, branch, {cwd, env});
|
await verifyAuth(repositoryUrl, branch, {cwd, env});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(env[envVar]));
|
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isNil(env[envVar]));
|
||||||
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
|
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
|
||||||
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
|
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
|
||||||
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
|
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
|
||||||
|
|||||||
@@ -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});
|
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true});
|
||||||
|
|
||||||
if (tag) {
|
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};
|
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;
|
let version;
|
||||||
if (lastRelease.version) {
|
if (lastRelease.version) {
|
||||||
version = semver.inc(lastRelease.version, type);
|
version = semver.inc(lastRelease.version, type);
|
||||||
logger.log('The next release version is %s', version);
|
logger.log(`The next release version is ${version}`);
|
||||||
} else {
|
} else {
|
||||||
version = FIRST_RELEASE;
|
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;
|
return version;
|
||||||
|
|||||||
+19
-19
@@ -12,8 +12,8 @@ const debug = require('debug')('semantic-release:git');
|
|||||||
async function gitTagHead(tagName, execaOpts) {
|
async function gitTagHead(tagName, execaOpts) {
|
||||||
try {
|
try {
|
||||||
return await execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
return await execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
debug(err);
|
debug(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +44,13 @@ async function isRefInHistory(ref, execaOpts) {
|
|||||||
try {
|
try {
|
||||||
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], execaOpts);
|
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], execaOpts);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (err.code === 1) {
|
if (error.code === 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(err);
|
debug(error);
|
||||||
throw err;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ async function isRefInHistory(ref, execaOpts) {
|
|||||||
async function fetch(repositoryUrl, execaOpts) {
|
async function fetch(repositoryUrl, execaOpts) {
|
||||||
try {
|
try {
|
||||||
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], execaOpts);
|
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], execaOpts);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
await execa('git', ['fetch', '--tags', repositoryUrl], execaOpts);
|
await execa('git', ['fetch', '--tags', repositoryUrl], execaOpts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ async function fetch(repositoryUrl, execaOpts) {
|
|||||||
*
|
*
|
||||||
* @return {string} the sha of the HEAD commit.
|
* @return {string} the sha of the HEAD commit.
|
||||||
*/
|
*/
|
||||||
async function gitHead(execaOpts) {
|
function gitHead(execaOpts) {
|
||||||
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ async function gitHead(execaOpts) {
|
|||||||
async function repoUrl(execaOpts) {
|
async function repoUrl(execaOpts) {
|
||||||
try {
|
try {
|
||||||
return await execa.stdout('git', ['config', '--get', 'remote.origin.url'], execaOpts);
|
return await execa.stdout('git', ['config', '--get', 'remote.origin.url'], execaOpts);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
debug(err);
|
debug(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ async function repoUrl(execaOpts) {
|
|||||||
async function isGitRepo(execaOpts) {
|
async function isGitRepo(execaOpts) {
|
||||||
try {
|
try {
|
||||||
return (await execa('git', ['rev-parse', '--git-dir'], execaOpts)).code === 0;
|
return (await execa('git', ['rev-parse', '--git-dir'], execaOpts)).code === 0;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
debug(err);
|
debug(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +121,9 @@ async function isGitRepo(execaOpts) {
|
|||||||
async function verifyAuth(repositoryUrl, branch, execaOpts) {
|
async function verifyAuth(repositoryUrl, branch, execaOpts) {
|
||||||
try {
|
try {
|
||||||
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`], execaOpts);
|
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`], execaOpts);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
debug(err);
|
debug(error);
|
||||||
throw err;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +163,8 @@ async function push(repositoryUrl, branch, execaOpts) {
|
|||||||
async function verifyTagName(tagName, execaOpts) {
|
async function verifyTagName(tagName, execaOpts) {
|
||||||
try {
|
try {
|
||||||
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOpts)).code === 0;
|
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOpts)).code === 0;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
debug(err);
|
debug(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +182,8 @@ async function isBranchUpToDate(branch, execaOpts) {
|
|||||||
(await execa.stdout('git', ['ls-remote', '--heads', 'origin', branch], execaOpts)).match(/^(\w+)?/)[1],
|
(await execa.stdout('git', ['ls-remote', '--heads', 'origin', branch], execaOpts)).match(/^(\w+)?/)[1],
|
||||||
execaOpts
|
execaOpts
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
debug(err);
|
debug(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
const {escapeRegExp} = require('lodash');
|
const {escapeRegExp, size, isString} = require('lodash');
|
||||||
const {SECRET_REPLACEMENT} = require('./definitions/constants');
|
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
|
||||||
|
|
||||||
module.exports = env => {
|
module.exports = env => {
|
||||||
const toReplace = Object.keys(env).filter(
|
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');
|
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g');
|
||||||
return output => {
|
return output =>
|
||||||
return output && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : 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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
+61
-19
@@ -1,47 +1,89 @@
|
|||||||
const {identity, isPlainObject, omit, castArray, isUndefined} = require('lodash');
|
const {identity, isPlainObject, omit, castArray, isNil, isString} = require('lodash');
|
||||||
const AggregateError = require('aggregate-error');
|
const AggregateError = require('aggregate-error');
|
||||||
const getError = require('../get-error');
|
const getError = require('../get-error');
|
||||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||||
|
const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('./utils');
|
||||||
const pipeline = require('./pipeline');
|
const pipeline = require('./pipeline');
|
||||||
const normalize = require('./normalize');
|
const normalize = require('./normalize');
|
||||||
|
|
||||||
module.exports = ({cwd, options, logger}, pluginsPath) => {
|
module.exports = (context, pluginsPath) => {
|
||||||
|
let {options, logger} = context;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
|
|
||||||
|
const plugins = options.plugins
|
||||||
|
? castArray(options.plugins).reduce((plugins, plugin) => {
|
||||||
|
if (validatePlugin(plugin)) {
|
||||||
|
const [name, config] = parseConfig(plugin);
|
||||||
|
plugin = isString(name) ? loadPlugin(context, name, pluginsPath) : name;
|
||||||
|
|
||||||
|
if (isPlainObject(plugin)) {
|
||||||
|
Object.entries(plugin).forEach(([type, func]) => {
|
||||||
|
if (PLUGINS_DEFINITIONS[type]) {
|
||||||
|
plugins[type] = [...(PLUGINS_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,
|
pluginsConf,
|
||||||
[type, {configValidator, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
|
[type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
|
||||||
) => {
|
) => {
|
||||||
let pluginOpts;
|
let pluginOpts;
|
||||||
|
|
||||||
if (isUndefined(options[type])) {
|
if (isNil(options[type]) && def) {
|
||||||
pluginOpts = def;
|
pluginOpts = def;
|
||||||
} else {
|
} else {
|
||||||
const defaultPaths = castArray(def);
|
// If an object is passed and the path is missing, merge it with step options
|
||||||
// If an object is passed and the path is missing, set the default one for single plugins
|
if (isPlainObject(options[type]) && !options[type].path) {
|
||||||
if (isPlainObject(options[type]) && !options[type].path && defaultPaths.length === 1) {
|
options[type] = castArray(plugins[type]).map(
|
||||||
[options[type].path] = defaultPaths;
|
plugin => (plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (configValidator && !configValidator(options[type])) {
|
if (!validateStep({multiple, required}, options[type])) {
|
||||||
errors.push(getError('EPLUGINCONF', {type, pluginConf: options[type]}));
|
errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]}));
|
||||||
return plugins;
|
return pluginsConf;
|
||||||
}
|
}
|
||||||
pluginOpts = options[type];
|
pluginOpts = options[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = castArray(pluginOpts).map(pluginOpt =>
|
const steps = castArray(pluginOpts).map(pluginOpt =>
|
||||||
normalize({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 =>
|
pluginsConf[type] = async input =>
|
||||||
postprocess(await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)));
|
postprocess(
|
||||||
|
await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)),
|
||||||
|
input
|
||||||
|
);
|
||||||
|
|
||||||
return plugins;
|
return pluginsConf;
|
||||||
},
|
},
|
||||||
{}
|
plugins
|
||||||
);
|
);
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw new AggregateError(errors);
|
throw new AggregateError(errors);
|
||||||
}
|
}
|
||||||
return plugins;
|
|
||||||
|
return pluginsConf;
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-23
@@ -1,30 +1,18 @@
|
|||||||
const {dirname} = require('path');
|
const {isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash');
|
||||||
const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash');
|
|
||||||
const resolveFrom = require('resolve-from');
|
|
||||||
const getError = require('../get-error');
|
const getError = require('../get-error');
|
||||||
const {extractErrors} = require('../utils');
|
const {extractErrors} = require('../utils');
|
||||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||||
|
const {loadPlugin, parseConfig} = require('./utils');
|
||||||
|
|
||||||
module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
module.exports = (context, type, pluginOpt, pluginsPath) => {
|
||||||
|
const {stdout, stderr, options, logger} = context;
|
||||||
if (!pluginOpt) {
|
if (!pluginOpt) {
|
||||||
return noop;
|
return noop;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt;
|
const [path, config] = parseConfig(pluginOpt);
|
||||||
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
|
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
|
||||||
|
const plugin = loadPlugin(context, path, pluginsPath);
|
||||||
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));
|
|
||||||
|
|
||||||
let func;
|
let func;
|
||||||
if (isFunction(plugin)) {
|
if (isFunction(plugin)) {
|
||||||
@@ -38,18 +26,34 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
|||||||
const validator = async input => {
|
const validator = async input => {
|
||||||
const {outputValidator} = PLUGINS_DEFINITIONS[type] || {};
|
const {outputValidator} = PLUGINS_DEFINITIONS[type] || {};
|
||||||
try {
|
try {
|
||||||
logger.log('Call plugin "%s"', type);
|
logger.log(`Start step "${type}" of plugin "${pluginName}"`);
|
||||||
const result = await func(cloneDeep(input));
|
const result = await func({
|
||||||
|
...cloneDeep(omit(input, ['stdout', 'stderr', 'logger'])),
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
logger: logger.scope(logger.scopeName, pluginName),
|
||||||
|
});
|
||||||
if (outputValidator && !outputValidator(result)) {
|
if (outputValidator && !outputValidator(result)) {
|
||||||
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
|
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
|
||||||
}
|
}
|
||||||
|
logger.success(`Completed step "${type}" of plugin "${pluginName}"`);
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
|
logger.error(`Failed step "${type}" of plugin "${pluginName}"`);
|
||||||
throw err;
|
extractErrors(error).forEach(err => Object.assign(err, {pluginName}));
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
|
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
|
||||||
|
|
||||||
|
if (!isFunction(pluginOpt)) {
|
||||||
|
if (pluginsPath[path]) {
|
||||||
|
logger.success(`Loaded plugin "${type}" from "${path}" in shareable config "${pluginsPath[path]}"`);
|
||||||
|
} else {
|
||||||
|
logger.success(`Loaded plugin "${type}" from "${path}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return validator;
|
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
|
// 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);
|
result = await transform(await step(lastInput), step, lastInput);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (settleAll) {
|
if (settleAll) {
|
||||||
errors.push(...extractErrors(err));
|
errors.push(...extractErrors(error));
|
||||||
result = err;
|
result = error;
|
||||||
} else {
|
} 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
|
// 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 [path, config] = parseConfig(conf[0]);
|
||||||
|
return (isString(path) || isFunction(path)) && 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}, path, pluginsPath) {
|
||||||
|
const basePath = pluginsPath[path]
|
||||||
|
? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path]))
|
||||||
|
: __dirname;
|
||||||
|
return isFunction(path) ? path : require(resolveFrom.silent(basePath, path) || resolveFrom(cwd, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfig(plugin) {
|
||||||
|
let path;
|
||||||
|
let config;
|
||||||
|
if (isArray(plugin)) {
|
||||||
|
[path, config] = plugin;
|
||||||
|
} else if (isPlainObject(plugin) && !isNil(plugin.path)) {
|
||||||
|
({path, ...config} = plugin);
|
||||||
|
} else {
|
||||||
|
path = plugin;
|
||||||
|
}
|
||||||
|
return [path, config || {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {validatePlugin, validateStep, loadPlugin, parseConfig};
|
||||||
+14
-1
@@ -1,7 +1,20 @@
|
|||||||
const {isFunction} = require('lodash');
|
const {isFunction} = require('lodash');
|
||||||
|
const hideSensitive = require('./hide-sensitive');
|
||||||
|
|
||||||
function extractErrors(err) {
|
function extractErrors(err) {
|
||||||
return err && isFunction(err[Symbol.iterator]) ? [...err] : [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 = [];
|
const errors = [];
|
||||||
|
|
||||||
if (!(await isGitRepo({cwd, env}))) {
|
if (!(await isGitRepo({cwd, env}))) {
|
||||||
errors.push(getError('ENOGITREPO'));
|
errors.push(getError('ENOGITREPO', {cwd}));
|
||||||
} else if (!repositoryUrl) {
|
} else if (!repositoryUrl) {
|
||||||
errors.push(getError('ENOREPOURL'));
|
errors.push(getError('ENOREPOURL'));
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-16
@@ -19,53 +19,55 @@
|
|||||||
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
|
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@semantic-release/commit-analyzer": "^6.0.0",
|
"@semantic-release/commit-analyzer": "^6.1.0",
|
||||||
"@semantic-release/error": "^2.2.0",
|
"@semantic-release/error": "^2.2.0",
|
||||||
"@semantic-release/github": "^5.0.0",
|
"@semantic-release/github": "^5.1.0",
|
||||||
"@semantic-release/npm": "^4.0.0",
|
"@semantic-release/npm": "^5.0.5",
|
||||||
"@semantic-release/release-notes-generator": "^7.0.0",
|
"@semantic-release/release-notes-generator": "^7.1.0",
|
||||||
"aggregate-error": "^1.0.0",
|
"aggregate-error": "^1.0.0",
|
||||||
"chalk": "^2.3.0",
|
|
||||||
"cosmiconfig": "^5.0.1",
|
"cosmiconfig": "^5.0.1",
|
||||||
"debug": "^3.1.0",
|
"debug": "^4.0.0",
|
||||||
"env-ci": "^2.0.0",
|
"env-ci": "^3.0.0",
|
||||||
"execa": "^0.10.0",
|
"execa": "^1.0.0",
|
||||||
|
"figures": "^2.0.0",
|
||||||
"find-versions": "^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-log-parser": "^1.2.0",
|
||||||
"git-url-parse": "^10.0.1",
|
"git-url-parse": "^10.0.1",
|
||||||
"hook-std": "^1.0.1",
|
"hook-std": "^1.1.0",
|
||||||
"hosted-git-info": "^2.7.1",
|
"hosted-git-info": "^2.7.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"marked": "^0.4.0",
|
"marked": "^0.5.0",
|
||||||
"marked-terminal": "^3.0.0",
|
"marked-terminal": "^3.0.0",
|
||||||
"p-locate": "^3.0.0",
|
"p-locate": "^3.0.0",
|
||||||
"p-reduce": "^1.0.0",
|
"p-reduce": "^1.0.0",
|
||||||
"read-pkg-up": "^4.0.0",
|
"read-pkg-up": "^4.0.0",
|
||||||
"resolve-from": "^4.0.0",
|
"resolve-from": "^4.0.0",
|
||||||
"semver": "^5.4.1",
|
"semver": "^5.4.1",
|
||||||
|
"signale": "^1.2.1",
|
||||||
"yargs": "^12.0.0"
|
"yargs": "^12.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "^0.25.0",
|
"ava": "^0.25.0",
|
||||||
"clear-module": "^3.0.0",
|
"clear-module": "^3.0.0",
|
||||||
"codecov": "^3.0.0",
|
"codecov": "^3.0.0",
|
||||||
"commitizen": "^2.9.6",
|
"commitizen": "^3.0.0",
|
||||||
"cz-conventional-changelog": "^2.0.0",
|
"cz-conventional-changelog": "^2.0.0",
|
||||||
"delay": "^3.0.0",
|
"delay": "^4.0.0",
|
||||||
"dockerode": "^2.5.2",
|
"dockerode": "^2.5.2",
|
||||||
"file-url": "^2.0.2",
|
"file-url": "^2.0.2",
|
||||||
"fs-extra": "^7.0.0",
|
"fs-extra": "^7.0.0",
|
||||||
"got": "^8.0.0",
|
"got": "^9.0.0",
|
||||||
"js-yaml": "^3.10.0",
|
"js-yaml": "^3.10.0",
|
||||||
"mockserver-client": "^5.1.1",
|
"mockserver-client": "^5.1.1",
|
||||||
"nock": "^9.0.2",
|
"nock": "^10.0.0",
|
||||||
"nyc": "^12.0.1",
|
"nyc": "^12.0.1",
|
||||||
"p-retry": "^2.0.0",
|
"p-retry": "^2.0.0",
|
||||||
"proxyquire": "^2.0.0",
|
"proxyquire": "^2.0.0",
|
||||||
"sinon": "^6.0.0",
|
"sinon": "^6.0.0",
|
||||||
|
"stream-buffers": "^3.0.2",
|
||||||
"tempy": "^0.2.1",
|
"tempy": "^0.2.1",
|
||||||
"xo": "^0.21.0"
|
"xo": "^0.23.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.3"
|
"node": ">=8.3"
|
||||||
|
|||||||
+25
-1
@@ -1,6 +1,8 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
|
import {escapeRegExp} from 'lodash';
|
||||||
import proxyquire from 'proxyquire';
|
import proxyquire from 'proxyquire';
|
||||||
import {stub} from 'sinon';
|
import {stub} from 'sinon';
|
||||||
|
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||||
|
|
||||||
const requireNoCache = proxyquire.noPreserveCache();
|
const requireNoCache = proxyquire.noPreserveCache();
|
||||||
|
|
||||||
@@ -31,6 +33,9 @@ test.serial('Pass options to semantic-release API', async t => {
|
|||||||
'https://github/com/owner/repo.git',
|
'https://github/com/owner/repo.git',
|
||||||
'-t',
|
'-t',
|
||||||
`v\${version}`,
|
`v\${version}`,
|
||||||
|
'-p',
|
||||||
|
'plugin1',
|
||||||
|
'plugin2',
|
||||||
'-e',
|
'-e',
|
||||||
'config1',
|
'config1',
|
||||||
'config2',
|
'config2',
|
||||||
@@ -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].branch, 'master');
|
||||||
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
|
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
|
||||||
t.is(run.args[0][0].tagFormat, `v\${version}`);
|
t.is(run.args[0][0].tagFormat, `v\${version}`);
|
||||||
|
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
|
||||||
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
|
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
|
||||||
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
|
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
|
||||||
t.is(run.args[0][0].analyzeCommits, 'analyze');
|
t.is(run.args[0][0].analyzeCommits, 'analyze');
|
||||||
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']);
|
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].prepare, ['prepare1', 'prepare2']);
|
||||||
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
|
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
|
||||||
t.deepEqual(run.args[0][0].success, ['success1', 'success2']);
|
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',
|
'https://github/com/owner/repo.git',
|
||||||
'--tag-format',
|
'--tag-format',
|
||||||
`v\${version}`,
|
`v\${version}`,
|
||||||
|
'--plugins',
|
||||||
|
'plugin1',
|
||||||
|
'plugin2',
|
||||||
'--extends',
|
'--extends',
|
||||||
'config1',
|
'config1',
|
||||||
'config2',
|
'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].branch, 'master');
|
||||||
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
|
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
|
||||||
t.is(run.args[0][0].tagFormat, `v\${version}`);
|
t.is(run.args[0][0].tagFormat, `v\${version}`);
|
||||||
|
t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
|
||||||
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
|
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
|
||||||
t.is(run.args[0][0].dryRun, true);
|
t.is(run.args[0][0].dryRun, true);
|
||||||
|
|
||||||
@@ -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('debug' in run.args[0][0]);
|
||||||
t.false('r' in run.args[0][0]);
|
t.false('r' in run.args[0][0]);
|
||||||
t.false('t' in run.args[0][0]);
|
t.false('t' in run.args[0][0]);
|
||||||
|
t.false('p' in run.args[0][0]);
|
||||||
|
t.false('e' in run.args[0][0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
|
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
|
||||||
@@ -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.regex(t.context.errors, /semantic-release error/);
|
||||||
t.is(exitCode, 1);
|
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 test from 'ava';
|
||||||
import plugins from '../../lib/definitions/plugins';
|
import plugins from '../../lib/definitions/plugins';
|
||||||
import {RELEASE_NOTES_SEPARATOR} from '../../lib/definitions/constants';
|
import {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} 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', () => {}]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||||
t.false(plugins.analyzeCommits.outputValidator('invalid'));
|
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(''));
|
t.true(plugins.publish.outputValidator(''));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('The "generateNotes" plugins output are concatenated with separator', t => {
|
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
|
||||||
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
const env = {MY_TOKEN: 'secret token'};
|
||||||
t.is(plugins.generateNotes.postprocess(['', 'note']), 'note');
|
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||||
t.is(plugins.generateNotes.postprocess([undefined, 'note']), 'note');
|
t.is(plugins.generateNotes.postprocess(['', 'note'], {env}), 'note');
|
||||||
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
t.is(plugins.generateNotes.postprocess([undefined, 'note'], {env}), 'note');
|
||||||
t.is(plugins.generateNotes.postprocess(['note 1', undefined, 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
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 = () => {};
|
module.exports = () => {};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
const SemanticReleaseError = require('@semantic-release/error');
|
const SemanticReleaseError = require('@semantic-release/error');
|
||||||
|
|
||||||
class InheritedError extends SemanticReleaseError {
|
class InheritedError extends SemanticReleaseError {
|
||||||
constructor(message, code, newProperty) {
|
constructor(message, code) {
|
||||||
super(message);
|
super(message);
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
|
|||||||
+30
-3
@@ -8,6 +8,13 @@ import {stub} from 'sinon';
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
|
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
|
||||||
|
|
||||||
|
const DEFAULT_PLUGINS = [
|
||||||
|
'@semantic-release/commit-analyzer',
|
||||||
|
'@semantic-release/release-notes-generator',
|
||||||
|
'@semantic-release/npm',
|
||||||
|
'@semantic-release/github',
|
||||||
|
];
|
||||||
|
|
||||||
test.beforeEach(t => {
|
test.beforeEach(t => {
|
||||||
t.context.plugins = stub().returns({});
|
t.context.plugins = stub().returns({});
|
||||||
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
|
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
|
||||||
@@ -69,6 +76,7 @@ test('Read options from package.json', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json in repository root
|
// Create package.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: options});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: options});
|
||||||
@@ -89,6 +97,7 @@ test('Read options from .releaserc.yml', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json in repository root
|
// Create package.json in repository root
|
||||||
await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options));
|
await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options));
|
||||||
@@ -109,6 +118,7 @@ test('Read options from .releaserc.json', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json in repository root
|
// Create package.json in repository root
|
||||||
await outputJson(path.resolve(cwd, '.releaserc.json'), options);
|
await outputJson(path.resolve(cwd, '.releaserc.json'), options);
|
||||||
@@ -129,6 +139,7 @@ test('Read options from .releaserc.js', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json in repository root
|
// Create package.json in repository root
|
||||||
await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`);
|
await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`);
|
||||||
@@ -149,6 +160,7 @@ test('Read options from release.config.js', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json in repository root
|
// Create package.json in repository root
|
||||||
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`);
|
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`);
|
||||||
@@ -176,6 +188,7 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async
|
|||||||
branch: 'branch_cli',
|
branch: 'branch_cli',
|
||||||
repositoryUrl: 'http://cli-url.com/owner/package',
|
repositoryUrl: 'http://cli-url.com/owner/package',
|
||||||
tagFormat: `cli\${version}`,
|
tagFormat: `cli\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'};
|
const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'};
|
||||||
// Create package.json in repository root
|
// Create package.json in repository root
|
||||||
@@ -199,6 +212,7 @@ test('Read configuration from file path in "extends"', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json and shareable.json in repository root
|
// Create package.json and shareable.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||||
@@ -226,6 +240,7 @@ test('Read configuration from module path in "extends"', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json and shareable.json in repository root
|
// Create package.json and shareable.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||||
@@ -259,6 +274,7 @@ test('Read configuration from an array of paths in "extends"', async t => {
|
|||||||
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
|
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
|
||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json and shareable.json in repository root
|
// Create package.json and shareable.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||||
@@ -296,6 +312,7 @@ test('Prioritize configuration from config file over "extends"', async t => {
|
|||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json and shareable.json in repository root
|
// Create package.json and shareable.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||||
@@ -341,6 +358,7 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
|
|||||||
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
|
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
|
||||||
branch: 'test_branch2',
|
branch: 'test_branch2',
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json, shareable1.json and shareable2.json in repository root
|
// Create package.json, shareable1.json and shareable2.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||||
@@ -366,11 +384,13 @@ test('Allow to unset properties defined in shareable config with "null"', async
|
|||||||
analyzeCommits: null,
|
analyzeCommits: null,
|
||||||
branch: 'test_branch',
|
branch: 'test_branch',
|
||||||
repositoryUrl: 'https://host.null/owner/module.git',
|
repositoryUrl: 'https://host.null/owner/module.git',
|
||||||
|
plugins: null,
|
||||||
};
|
};
|
||||||
const options1 = {
|
const options1 = {
|
||||||
generateNotes: 'generateNotes',
|
generateNotes: 'generateNotes',
|
||||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: ['test-plugin'],
|
||||||
};
|
};
|
||||||
// Create package.json and shareable.json in repository root
|
// Create package.json and shareable.json in repository root
|
||||||
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
|
||||||
@@ -378,19 +398,25 @@ test('Allow to unset properties defined in shareable config with "null"', async
|
|||||||
|
|
||||||
const {options} = await t.context.getConfig({cwd});
|
const {options} = await t.context.getConfig({cwd});
|
||||||
|
|
||||||
// Verify the options contains the plugin config from shareable.json
|
// Verify the options contains the plugin config from shareable.json and the default `plugins`
|
||||||
t.deepEqual(options, {...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits'])});
|
t.deepEqual(options, {
|
||||||
// Verify the plugins module is called with the plugin options from shareable.json
|
...omit(options1, ['analyzeCommits']),
|
||||||
|
...omit(pkgOptions, ['extends', 'analyzeCommits']),
|
||||||
|
plugins: DEFAULT_PLUGINS,
|
||||||
|
});
|
||||||
|
// Verify the plugins module is called with the plugin options from shareable.json and the default `plugins`
|
||||||
t.deepEqual(t.context.plugins.args[0][0], {
|
t.deepEqual(t.context.plugins.args[0][0], {
|
||||||
options: {
|
options: {
|
||||||
...omit(options1, 'analyzeCommits'),
|
...omit(options1, 'analyzeCommits'),
|
||||||
...omit(pkgOptions, ['extends', 'analyzeCommits']),
|
...omit(pkgOptions, ['extends', 'analyzeCommits']),
|
||||||
|
plugins: DEFAULT_PLUGINS,
|
||||||
},
|
},
|
||||||
cwd,
|
cwd,
|
||||||
});
|
});
|
||||||
t.deepEqual(t.context.plugins.args[0][1], {
|
t.deepEqual(t.context.plugins.args[0][1], {
|
||||||
generateNotes: './shareable.json',
|
generateNotes: './shareable.json',
|
||||||
analyzeCommits: './shareable.json',
|
analyzeCommits: './shareable.json',
|
||||||
|
'test-plugin': './shareable.json',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -407,6 +433,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a
|
|||||||
generateNotes: 'generateNotes',
|
generateNotes: 'generateNotes',
|
||||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||||
tagFormat: `v\${version}`,
|
tagFormat: `v\${version}`,
|
||||||
|
plugins: false,
|
||||||
};
|
};
|
||||||
// Create package.json and release.config.js in repository root
|
// Create package.json and release.config.js in repository root
|
||||||
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`);
|
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`);
|
||||||
|
|||||||
@@ -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});
|
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(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 => {
|
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).
|
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
|
||||||
*/
|
*/
|
||||||
export async function gitCommits(messages, execaOpts) {
|
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)
|
execa.stdout('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)
|
||||||
);
|
);
|
||||||
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
|
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.
|
* @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);
|
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.
|
* @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);
|
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`.
|
* @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);
|
return execa.stdout('git', ['describe', '--tags', '--exact-match', gitHead], execaOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ async function start() {
|
|||||||
minTimeout: 1000,
|
minTimeout: 1000,
|
||||||
factor: 2,
|
factor: 2,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
throw new Error(`Couldn't start mock-server after 2 min`);
|
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.
|
* @param {Object} expectation The expectation created with `mock` function.
|
||||||
* @return {Promise} A Promise that resolves if the expectation is met or reject otherwise.
|
* @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);
|
return client.verify(expectation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import got from 'got';
|
|||||||
import delay from 'delay';
|
import delay from 'delay';
|
||||||
import pRetry from 'p-retry';
|
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 SERVER_PORT = 15986;
|
||||||
const COUCHDB_PORT = 5984;
|
const COUCHDB_PORT = 5984;
|
||||||
const SERVER_HOST = 'localhost';
|
const SERVER_HOST = 'localhost';
|
||||||
|
const COUCHDB_USER = 'admin';
|
||||||
|
const COUCHDB_PASSWORD = 'password';
|
||||||
const NPM_USERNAME = 'integration';
|
const NPM_USERNAME = 'integration';
|
||||||
const NPM_PASSWORD = 'suchsecure';
|
const NPM_PASSWORD = 'suchsecure';
|
||||||
const NPM_EMAIL = 'integration@test.com';
|
const NPM_EMAIL = 'integration@test.com';
|
||||||
@@ -15,7 +17,7 @@ const docker = new Docker();
|
|||||||
let container;
|
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() {
|
async function start() {
|
||||||
await getStream(await docker.pull(IMAGE));
|
await getStream(await docker.pull(IMAGE));
|
||||||
@@ -24,10 +26,11 @@ async function start() {
|
|||||||
Tty: true,
|
Tty: true,
|
||||||
Image: IMAGE,
|
Image: IMAGE,
|
||||||
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
|
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
|
||||||
|
Env: [`COUCHDB_USER=${COUCHDB_USER}`, `COUCHDB_PASSWORD=${COUCHDB_PASSWORD}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await container.start();
|
await container.start();
|
||||||
await delay(3000);
|
await delay(4000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for the registry to be ready
|
// Wait for the registry to be ready
|
||||||
@@ -36,14 +39,14 @@ async function start() {
|
|||||||
minTimeout: 1000,
|
minTimeout: 1000,
|
||||||
factor: 2,
|
factor: 2,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
|
throw new Error(`Couldn't start npm-registry-docker after 2 min`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
|
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
|
||||||
json: true,
|
json: true,
|
||||||
auth: 'admin:admin',
|
auth: `${COUCHDB_USER}:${COUCHDB_PASSWORD}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: {
|
body: {
|
||||||
_id: `org.couchdb.user:${NPM_USERNAME}`,
|
_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() {
|
async function stop() {
|
||||||
await container.stop();
|
await container.stop();
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
|
import {repeat} from 'lodash';
|
||||||
import hideSensitive from '../lib/hide-sensitive';
|
import hideSensitive from '../lib/hide-sensitive';
|
||||||
|
import {SECRET_REPLACEMENT, SECRET_MIN_SIZE} from '../lib/definitions/constants';
|
||||||
|
|
||||||
test('Replace multiple sensitive environment variable values', t => {
|
test('Replace multiple sensitive environment variable values', t => {
|
||||||
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: 'secret'};
|
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: 'secret'};
|
||||||
t.is(
|
t.is(
|
||||||
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=${env.SOME_TOKEN}`),
|
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'};
|
const env = {secretKey: 'secret'};
|
||||||
t.is(
|
t.is(
|
||||||
hideSensitive(env)(`https://user:${env.secretKey}@host.com?token=${env.secretKey}`),
|
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 => {
|
test('Escape regexp special characters', t => {
|
||||||
const env = {SOME_CREDENTIALS: 'p$^{.+}\\w[a-z]o.*rd'};
|
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 => {
|
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: ''};
|
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: ''};
|
||||||
t.is(
|
t.is(
|
||||||
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=`),
|
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 => {
|
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=');
|
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 test from 'ava';
|
||||||
|
import {escapeRegExp, isString} from 'lodash';
|
||||||
import proxyquire from 'proxyquire';
|
import proxyquire from 'proxyquire';
|
||||||
import {spy, stub} from 'sinon';
|
import {spy, stub} from 'sinon';
|
||||||
|
import {WritableStreamBuffer} from 'stream-buffers';
|
||||||
import AggregateError from 'aggregate-error';
|
import AggregateError from 'aggregate-error';
|
||||||
import SemanticReleaseError from '@semantic-release/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 {
|
import {
|
||||||
gitHead as getGitHead,
|
gitHead as getGitHead,
|
||||||
gitTagHead,
|
gitTagHead,
|
||||||
@@ -22,9 +24,13 @@ test.beforeEach(t => {
|
|||||||
// Stub the logger functions
|
// Stub the logger functions
|
||||||
t.context.log = spy();
|
t.context.log = spy();
|
||||||
t.context.error = spy();
|
t.context.error = spy();
|
||||||
t.context.stdout = spy();
|
t.context.success = spy();
|
||||||
t.context.stderr = spy();
|
t.context.logger = {
|
||||||
t.context.logger = {log: t.context.log, error: t.context.error, stdout: t.context.stdout, stderr: t.context.stderr};
|
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 => {
|
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 config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||||
const options = {
|
const options = {
|
||||||
...config,
|
...config,
|
||||||
|
plugins: false,
|
||||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||||
analyzeCommits,
|
analyzeCommits,
|
||||||
verifyRelease,
|
verifyRelease,
|
||||||
@@ -68,10 +75,15 @@ test('Plugins are called with expected values', async t => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.is(verifyConditions1.callCount, 1);
|
||||||
t.deepEqual(verifyConditions1.args[0][0], config);
|
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},
|
{...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
|
// 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 gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||||
t.is(await gitRemoteTagHead(repositoryUrl, 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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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
|
// 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 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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.is(generateNotes.callCount, 2);
|
||||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.is(success1.callCount, 1);
|
||||||
t.deepEqual(success1.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]);
|
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 config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
|
||||||
const options = {
|
const options = {
|
||||||
...config,
|
...config,
|
||||||
|
plugins: false,
|
||||||
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
|
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
|
||||||
fail,
|
fail,
|
||||||
};
|
};
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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(errors, [error1, error2, error3]);
|
||||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']);
|
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR2 error 2']);
|
||||||
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], ['ERR3 error 3']);
|
||||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [
|
t.deepEqual(t.context.error.args[t.context.error.args.length - 3], [
|
||||||
'An error occurred while running semantic-release: %O',
|
'An error occurred while running semantic-release: %O',
|
||||||
error1,
|
error1,
|
||||||
]);
|
]);
|
||||||
@@ -371,14 +414,18 @@ test('Log all "verifyRelease" errors', async t => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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(errors, [error1, error2]);
|
||||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
|
||||||
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 - 1], ['ERR2 error 2']);
|
||||||
t.is(fail.callCount, 1);
|
t.is(fail.callCount, 1);
|
||||||
t.deepEqual(fail.args[0][0], config);
|
t.deepEqual(fail.args[0][0], config);
|
||||||
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
|
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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.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);
|
t.is(verifyConditions.callCount, 1);
|
||||||
@@ -457,14 +511,18 @@ test('Dry-run skips fail', async t => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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(errors, [error1, error2]);
|
||||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
|
||||||
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 - 1], ['ERR2 error 2']);
|
||||||
t.is(fail.callCount, 0);
|
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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: false, branch: 'master'}),
|
'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(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||||
t.is(verifyConditions.callCount, 1);
|
t.is(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);
|
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
|
// Create a git repository, set the current working directory at the root of the repo
|
||||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
// Add commits to the master branch
|
// 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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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 => {
|
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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: false, branch: 'master', isPr: true}),
|
'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(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||||
t.not(
|
t.not(
|
||||||
@@ -643,10 +722,17 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
|
|||||||
};
|
};
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.is(analyzeCommits.callCount, 1);
|
||||||
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
|
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);
|
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
|
// Create a git repository, set the current working directory at the root of the repo
|
||||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
|
'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.is(
|
||||||
t.context.log.args[t.context.log.args.length - 1][0],
|
t.context.log.args[t.context.log.args.length - 1][0],
|
||||||
"This run was triggered by a pull request and therefore a new version won't be published."
|
"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
|
// Create a git repository, set the current working directory at the root of the repo
|
||||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
const repoDir = cwd;
|
const repoDir = cwd;
|
||||||
@@ -694,18 +785,22 @@ test('Returns falsy value if triggered on an outdated clone', async t => {
|
|||||||
await gitPush(repositoryUrl, 'master', {cwd});
|
await gitPush(repositoryUrl, 'master', {cwd});
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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], [
|
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.",
|
"The local branch master is behind the remote one, therefore a new version won't be published.",
|
||||||
'master',
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
// Create a git repository, set the current working directory at the root of the repo
|
||||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
const options = {
|
const options = {
|
||||||
@@ -722,18 +817,25 @@ test('Returns falsy value if not running from the configured branch', async t =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}),
|
'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.is(
|
||||||
t.context.log.args[1][0],
|
t.context.log.args[1][0],
|
||||||
'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published.'
|
'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
|
// Create a git repository, set the current working directory at the root of the repo
|
||||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
// Add commits to the master branch
|
// 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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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(analyzeCommits.callCount, 1);
|
||||||
t.is(verifyRelease.callCount, 0);
|
t.is(verifyRelease.callCount, 0);
|
||||||
t.is(generateNotes.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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.callCount, 1);
|
||||||
t.is(analyzeCommits.args[0][1].commits.length, 2);
|
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)],
|
fail: [stub().rejects(failError1), stub().rejects(failError2)],
|
||||||
};
|
};
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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][0], 'ERR Plugin error');
|
||||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2);
|
t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1);
|
||||||
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 - 2][1], failError2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
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,
|
fail,
|
||||||
};
|
};
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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.true(fail.notCalled);
|
||||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
|
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 {cwd} = await gitRepo();
|
||||||
|
|
||||||
const semanticRelease = requireNoCache('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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
|
// Verify error code and type
|
||||||
t.is(errors[0].code, 'ENOREPOURL');
|
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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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/);
|
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 => {
|
test('Get all commits including the ones not in the shallow clone', async t => {
|
||||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||||
await gitTagVersion('v1.0.0', undefined, {cwd});
|
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('..', {
|
const semanticRelease = requireNoCache('..', {
|
||||||
'./lib/logger': t.context.logger,
|
'./lib/get-logger': () => t.context.logger,
|
||||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
'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);
|
t.is(analyzeCommits.args[0][1].commits.length, 3);
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-10
@@ -4,6 +4,7 @@ import test from 'ava';
|
|||||||
import {escapeRegExp} from 'lodash';
|
import {escapeRegExp} from 'lodash';
|
||||||
import {writeJson, readJson} from 'fs-extra';
|
import {writeJson, readJson} from 'fs-extra';
|
||||||
import execa from 'execa';
|
import execa from 'execa';
|
||||||
|
import {WritableStreamBuffer} from 'stream-buffers';
|
||||||
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||||
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
|
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
|
||||||
import gitbox from './helpers/gitbox';
|
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)
|
// Environment variables used with semantic-release cli (similar to what a user would setup)
|
||||||
const env = {
|
const env = {
|
||||||
|
...npmRegistry.authEnv,
|
||||||
GH_TOKEN: gitbox.gitCredential,
|
GH_TOKEN: gitbox.gitCredential,
|
||||||
GITHUB_URL: mockServer.url,
|
GITHUB_URL: mockServer.url,
|
||||||
NPM_EMAIL: 'integration@test.com',
|
|
||||||
NPM_USERNAME: 'integration',
|
|
||||||
NPM_PASSWORD: 'suchsecure',
|
|
||||||
TRAVIS: 'true',
|
TRAVIS: 'true',
|
||||||
CI: 'true',
|
CI: 'true',
|
||||||
TRAVIS_BRANCH: 'master',
|
TRAVIS_BRANCH: 'master',
|
||||||
@@ -29,9 +28,9 @@ const env = {
|
|||||||
// Environment variables used only for the local npm command used to do verification
|
// Environment variables used only for the local npm command used to do verification
|
||||||
const testEnv = {
|
const testEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
...npmRegistry.authEnv,
|
||||||
npm_config_registry: npmRegistry.url,
|
npm_config_registry: npmRegistry.url,
|
||||||
NPM_EMAIL: 'integration@test.com',
|
LEGACY_TOKEN: Buffer.from(`${env.NPM_USERNAME}:${env.NPM_PASSWORD}`, 'utf8').toString('base64'),
|
||||||
LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cli = require.resolve('../bin/semantic-release');
|
const cli = require.resolve('../bin/semantic-release');
|
||||||
@@ -467,6 +466,10 @@ test('Run via JS API', async t => {
|
|||||||
version: '0.0.0-dev',
|
version: '0.0.0-dev',
|
||||||
repository: {url: repositoryUrl},
|
repository: {url: repositoryUrl},
|
||||||
publishConfig: {registry: npmRegistry.url},
|
publishConfig: {registry: npmRegistry.url},
|
||||||
|
release: {
|
||||||
|
fail: false,
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Initial release */
|
/* Initial release */
|
||||||
@@ -488,7 +491,7 @@ test('Run via JS API', async t => {
|
|||||||
t.log('Commit a feature');
|
t.log('Commit a feature');
|
||||||
await gitCommits(['feat: Initial commit'], {cwd});
|
await gitCommits(['feat: Initial commit'], {cwd});
|
||||||
t.log('$ Call semantic-release via API');
|
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
|
// Verify package.json and has been updated
|
||||||
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
|
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');
|
t.log('Commit a feature');
|
||||||
await gitCommits(['feat: Initial commit'], {cwd});
|
await gitCommits(['feat: Initial commit'], {cwd});
|
||||||
t.log('$ semantic-release');
|
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
|
// Verify the type and message are logged
|
||||||
t.regex(stdout, /EINHERITED Inherited error/);
|
t.regex(stderr, /EINHERITED Inherited error/);
|
||||||
t.is(code, 1);
|
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 gitCommits(['feat: Initial commit'], {cwd});
|
||||||
await gitPush('origin', 'master', {cwd});
|
await gitPush('origin', 'master', {cwd});
|
||||||
t.log('$ semantic-release');
|
t.log('$ semantic-release');
|
||||||
const {stdout, code} = await execa(
|
const {stderr, code} = await execa(
|
||||||
cli,
|
cli,
|
||||||
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
|
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
|
||||||
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false}
|
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false}
|
||||||
);
|
);
|
||||||
// Verify the type and message are logged
|
// Verify the type and message are logged
|
||||||
t.regex(stdout, /EGITNOPERMISSION/);
|
t.regex(stderr, /EGITNOPERMISSION/);
|
||||||
t.is(code, 1);
|
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 => {
|
test.beforeEach(t => {
|
||||||
// Stub the logger functions
|
// Stub the logger functions
|
||||||
t.context.log = stub();
|
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 => {
|
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(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||||
t.is(typeof plugin, 'function');
|
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 => {
|
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(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||||
t.is(typeof plugin, 'function');
|
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 => {
|
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(plugin.pluginName, './plugin-noop');
|
||||||
t.is(typeof plugin, 'function');
|
t.is(typeof plugin, 'function');
|
||||||
t.deepEqual(t.context.log.args[0], [
|
t.deepEqual(t.context.success.args[0], [
|
||||||
'Load plugin "%s" from %s in shareable config %s',
|
'Loaded plugin "verifyConditions" from "./plugin-noop" in shareable config "./test/fixtures"',
|
||||||
'verifyConditions',
|
|
||||||
'./plugin-noop',
|
|
||||||
'./test/fixtures',
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,12 +95,17 @@ test('Normalize and load plugin that retuns multiple functions', t => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
t.is(typeof plugin, 'function');
|
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 => {
|
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
|
||||||
const analyzeCommits = stub().resolves(2);
|
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());
|
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 => {
|
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
|
||||||
const generateNotes = stub().resolves(2);
|
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());
|
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 => {
|
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', {
|
const publish = stub().resolves(2);
|
||||||
'./plugin-identity': './test/fixtures',
|
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.code, 'EPUBLISHOUTPUT');
|
||||||
t.is(error.name, 'SemanticReleaseError');
|
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/);
|
t.regex(error.details, /2/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
|
test('Plugin is called with "pluginConfig" (with object definition) and input', async t => {
|
||||||
const pluginFunction = stub().resolves();
|
const pluginFunction = stub().resolves();
|
||||||
const pluginConf = {path: pluginFunction, conf: 'confValue'};
|
const pluginConf = {path: pluginFunction, conf: 'confValue'};
|
||||||
const options = {global: 'globalValue'};
|
const options = {global: 'globalValue'};
|
||||||
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
|
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 => {
|
test('Prevent plugins to modify "pluginConfig"', async t => {
|
||||||
|
|||||||
+171
-12
@@ -11,7 +11,8 @@ const cwd = process.cwd();
|
|||||||
test.beforeEach(t => {
|
test.beforeEach(t => {
|
||||||
// Stub the logger functions
|
// Stub the logger functions
|
||||||
t.context.log = stub();
|
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 => {
|
test('Export default plugins', t => {
|
||||||
@@ -28,7 +29,7 @@ test('Export default plugins', t => {
|
|||||||
t.is(typeof plugins.fail, 'function');
|
t.is(typeof plugins.fail, 'function');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Export plugins based on config', t => {
|
test('Export plugins based on steps config', t => {
|
||||||
const plugins = getPlugins(
|
const plugins = getPlugins(
|
||||||
{
|
{
|
||||||
cwd,
|
cwd,
|
||||||
@@ -54,6 +55,108 @@ test('Export plugins based on config', t => {
|
|||||||
t.is(typeof plugins.fail, 'function');
|
t.is(typeof plugins.fail, 'function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Export plugins based on "plugins" config (array)', async t => {
|
||||||
|
const plugin1 = {verifyConditions: stub(), publish: stub()};
|
||||||
|
const plugin2 = {verifyConditions: stub(), verifyRelease: stub()};
|
||||||
|
const plugins = getPlugins(
|
||||||
|
{cwd, logger: t.context.logger, options: {plugins: [plugin1, plugin2], verifyRelease: () => {}}},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
await plugins.verifyConditions({});
|
||||||
|
t.true(plugin1.verifyConditions.calledOnce);
|
||||||
|
t.true(plugin2.verifyConditions.calledOnce);
|
||||||
|
|
||||||
|
await plugins.publish({});
|
||||||
|
t.true(plugin1.publish.calledOnce);
|
||||||
|
|
||||||
|
await plugins.verifyRelease({});
|
||||||
|
t.true(plugin2.verifyRelease.notCalled);
|
||||||
|
|
||||||
|
// Verify the module returns a function for each plugin
|
||||||
|
t.is(typeof plugins.verifyConditions, 'function');
|
||||||
|
t.is(typeof plugins.analyzeCommits, 'function');
|
||||||
|
t.is(typeof plugins.verifyRelease, 'function');
|
||||||
|
t.is(typeof plugins.generateNotes, 'function');
|
||||||
|
t.is(typeof plugins.prepare, 'function');
|
||||||
|
t.is(typeof plugins.publish, 'function');
|
||||||
|
t.is(typeof plugins.success, 'function');
|
||||||
|
t.is(typeof plugins.fail, 'function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Export plugins based on "plugins" config (single definition)', async t => {
|
||||||
|
const plugin1 = {verifyConditions: stub(), publish: stub()};
|
||||||
|
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {});
|
||||||
|
|
||||||
|
await plugins.verifyConditions({});
|
||||||
|
t.true(plugin1.verifyConditions.calledOnce);
|
||||||
|
|
||||||
|
await plugins.publish({});
|
||||||
|
t.true(plugin1.publish.calledOnce);
|
||||||
|
|
||||||
|
// Verify the module returns a function for each plugin
|
||||||
|
t.is(typeof plugins.verifyConditions, 'function');
|
||||||
|
t.is(typeof plugins.analyzeCommits, 'function');
|
||||||
|
t.is(typeof plugins.verifyRelease, 'function');
|
||||||
|
t.is(typeof plugins.generateNotes, 'function');
|
||||||
|
t.is(typeof plugins.prepare, 'function');
|
||||||
|
t.is(typeof plugins.publish, 'function');
|
||||||
|
t.is(typeof plugins.success, 'function');
|
||||||
|
t.is(typeof plugins.fail, 'function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('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 sptep options', async t => {
|
||||||
|
const plugin1 = [{verifyConditions: stub(), publish: stub()}, {pluginOpt1: 'plugin1'}];
|
||||||
|
const plugin2 = [{verifyConditions: stub()}, {pluginOpt2: 'plugin2'}];
|
||||||
|
const plugin3 = [stub(), {pluginOpt3: 'plugin3'}];
|
||||||
|
const plugins = getPlugins(
|
||||||
|
{
|
||||||
|
cwd,
|
||||||
|
logger: t.context.logger,
|
||||||
|
options: {globalOpt: 'global', plugins: [plugin1, plugin2], verifyRelease: [plugin3]},
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
await plugins.verifyConditions({});
|
||||||
|
t.deepEqual(plugin1[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
|
||||||
|
t.deepEqual(plugin2[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt2: 'plugin2'});
|
||||||
|
|
||||||
|
await plugins.publish({});
|
||||||
|
t.deepEqual(plugin1[0].publish.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
|
||||||
|
|
||||||
|
await plugins.verifyRelease({});
|
||||||
|
t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unknown steps of plugins configured in "plugins" are ignored', t => {
|
||||||
|
const plugin1 = {verifyConditions: () => {}, unknown: () => {}};
|
||||||
|
const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {});
|
||||||
|
|
||||||
|
t.is(typeof plugins.verifyConditions, 'function');
|
||||||
|
t.is(plugins.unknown, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
test('Export plugins loaded from the dependency of a shareable config module', async t => {
|
test('Export plugins loaded from the dependency of a shareable config module', async t => {
|
||||||
const cwd = tempy.directory();
|
const cwd = tempy.directory();
|
||||||
await copy(
|
await copy(
|
||||||
@@ -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 => {
|
test('Use default when only options are passed for a single plugin', t => {
|
||||||
const analyzeCommits = {};
|
const analyzeCommits = {};
|
||||||
const generateNotes = {};
|
const generateNotes = {};
|
||||||
|
const publish = {};
|
||||||
const success = () => {};
|
const success = () => {};
|
||||||
const fail = [() => {}];
|
const fail = [() => {}];
|
||||||
|
|
||||||
const plugins = getPlugins(
|
const plugins = getPlugins(
|
||||||
{cwd, logger: t.context.logger, options: {analyzeCommits, generateNotes, success, fail}},
|
{
|
||||||
|
cwd,
|
||||||
|
logger: t.context.logger,
|
||||||
|
options: {
|
||||||
|
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
|
||||||
|
analyzeCommits,
|
||||||
|
generateNotes,
|
||||||
|
publish,
|
||||||
|
success,
|
||||||
|
fail,
|
||||||
|
},
|
||||||
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -158,18 +273,21 @@ test('Merge global options with plugin options', async t => {
|
|||||||
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
|
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Throw an error if plugins configuration are 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: {}}}, {}))];
|
|
||||||
|
|
||||||
t.is(errors[0].name, 'SemanticReleaseError');
|
|
||||||
t.is(errors[0].code, 'EPLUGINCONF');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
|
|
||||||
const errors = [
|
const errors = [
|
||||||
...t.throws(() =>
|
...t.throws(() =>
|
||||||
getPlugins(
|
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].name, 'SemanticReleaseError');
|
||||||
t.is(errors[0].code, 'EPLUGINCONF');
|
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