Compare commits
@@ -105,13 +105,11 @@ After running the tests the command `semantic-release` will execute the followin
|
||||
- [CI configurations](docs/recipes/README.md)
|
||||
- [Package managers and languages](docs/recipes/README.md)
|
||||
- Developer guide
|
||||
- [JavaScript API](docs/developer-guide/js-api.md)
|
||||
- [Plugins](docs/developer-guide/plugin.md)
|
||||
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
|
||||
- Resources
|
||||
- [Videos](docs/resources.md#videos)
|
||||
- [Articles](docs/resources.md#articles)
|
||||
- [Tutorials](docs/resources.md#tutorials)
|
||||
- Support
|
||||
- [Resources](docs/support/resources.md)
|
||||
- [Frequently Asked Questions](docs/support/FAQ.md)
|
||||
- [Troubleshooting](docs/support/troubleshooting.md)
|
||||
- [Node version requirement](docs/support/node-version.md)
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- [Package managers and languages](docs/recipes/README.md)
|
||||
|
||||
## Developer guide
|
||||
- [JavaScript API](docs/developer-guide/js-api.md)
|
||||
- [Plugin](docs/developer-guide/plugin.md)
|
||||
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ execa
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(error => {
|
||||
console.error(`[semantic-release]: Git version ${MIN_GIT_VERSION} is required. No git binary found.`);
|
||||
console.error(err);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const {argv} = require('process');
|
||||
const {argv, env, stderr} = require('process');
|
||||
const util = require('util');
|
||||
const hideSensitive = require('./lib/hide-sensitive');
|
||||
|
||||
const stringList = {
|
||||
type: 'string',
|
||||
@@ -25,7 +27,7 @@ Usage:
|
||||
.option('verify-conditions', {...stringList, group: 'Plugins'})
|
||||
.option('analyze-commits', {type: 'string', group: 'Plugins'})
|
||||
.option('verify-release', {...stringList, group: 'Plugins'})
|
||||
.option('generate-notes', {type: 'string', group: 'Plugins'})
|
||||
.option('generate-notes', {...stringList, group: 'Plugins'})
|
||||
.option('prepare', {...stringList, group: 'Plugins'})
|
||||
.option('publish', {...stringList, group: 'Plugins'})
|
||||
.option('success', {...stringList, group: 'Plugins'})
|
||||
@@ -55,9 +57,9 @@ Usage:
|
||||
}
|
||||
await require('.')(opts);
|
||||
return 0;
|
||||
} catch (err) {
|
||||
if (err.name !== 'YError') {
|
||||
console.error(err);
|
||||
} catch (error) {
|
||||
if (error.name !== 'YError') {
|
||||
stderr.write(hideSensitive(env)(util.inspect(error, {colors: true})));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Developer guide
|
||||
|
||||
- [JavaScript API](js-api.md)
|
||||
- [Plugins](plugin.md)
|
||||
- [Shareable configuration](shareable-configuration.md)
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# JavaScript API
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const semanticRelease = require('semantic-release');
|
||||
const {WritableStreamBuffer} = require('stream-buffers');
|
||||
|
||||
const stdoutBuffer = WritableStreamBuffer();
|
||||
const stderrBuffer = WritableStreamBuffer();
|
||||
|
||||
try {
|
||||
const result = await semanticRelease({
|
||||
// Core options
|
||||
branch: 'master',
|
||||
repositoryUrl: 'https://github.com/me/my-package.git',
|
||||
// Shareable config
|
||||
extends: 'my-shareable-config',
|
||||
// Plugin options
|
||||
githubUrl: 'https://my-ghe.com',
|
||||
githubApiPathPrefix: '/api-prefix'
|
||||
}, {
|
||||
// Run semantic-release from `/path/to/git/repo/root` without having to change local process `cwd` with `process.chdir()`
|
||||
cwd: '/path/to/git/repo/root',
|
||||
// Pass the variable `MY_ENV_VAR` to semantic-release without having to modify the local `process.env`
|
||||
env: {...process.env, MY_ENV_VAR: 'MY_ENV_VAR_VALUE'},
|
||||
// Store stdout and stderr to use later instead of writing to `process.stdout` and `process.stderr`
|
||||
stdout: stdoutBuffer,
|
||||
stderr: stderrBuffer
|
||||
});
|
||||
|
||||
if (result) {
|
||||
const {lastRelease, commits, nextRelease, releases} = result;
|
||||
|
||||
console.log(`Published ${nextRelease.type} release version ${nextRelease.version} containing ${commits.length} commits.`);
|
||||
|
||||
if (lastRelease.version) {
|
||||
console.log(`The last release was "${lastRelease.version}".`);
|
||||
}
|
||||
|
||||
for (const release of releases) {
|
||||
console.log(`The release was published with plugin "${pluginName}".`);
|
||||
}
|
||||
} else {
|
||||
console.log('No release published.');
|
||||
}
|
||||
|
||||
// Get stdout and stderr content
|
||||
const logs = stdoutBuffer.getContentsAsString('utf8');
|
||||
const errors = stderrBuffer.getContentsAsString('utf8');
|
||||
} catch (err) {
|
||||
console.error('The automated release failed with %O', err)
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### semanticRelease([options], [config]) => Promise<Result>
|
||||
|
||||
Run **semantic-release** and returns a `Promise` that resolves to a [Result](#result) object.
|
||||
|
||||
#### options
|
||||
|
||||
Type: `Object`
|
||||
|
||||
**semantic-release** options.
|
||||
|
||||
Can be used to set any [core option](../usage/configuration.md#configuration) or [plugin options](../usage/plugins.md#configuration).
|
||||
|
||||
Each option, will take precedence over options configured in the [configuration file](../usage/configuration.md#configuration) and [shareable configurations](../usage/configuration.md#extends).
|
||||
|
||||
#### config
|
||||
|
||||
Type: `Object`
|
||||
|
||||
**semantic-release** configuration specific for API usage.
|
||||
|
||||
##### cwd
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `process.cwd()`
|
||||
|
||||
The current working directory to use. It should be configured to the root of the Git repository to release from.
|
||||
|
||||
It allows to run **semantic-release** from a specific path without having to change the local process `cwd` with `process.chdir()`.
|
||||
|
||||
##### env
|
||||
|
||||
Type: `Object`<br>
|
||||
Default: `process.env`
|
||||
|
||||
The environment variables to use.
|
||||
|
||||
It allows to run **semantic-release** with specific environment variables without having to modify the local `process.env`.
|
||||
|
||||
##### stdout
|
||||
|
||||
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
|
||||
Default: `process.stdout`
|
||||
|
||||
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log information.
|
||||
|
||||
It allows to configure **semantic-release** to write logs to a specific stream rather than the local `process.stdout`.
|
||||
|
||||
##### stderr
|
||||
|
||||
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
|
||||
Default: `process.stderr`
|
||||
|
||||
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log errors.
|
||||
|
||||
It allows to configure **semantic-release** to write errors to a specific stream rather than the local `process.stderr`.
|
||||
|
||||
### Result
|
||||
|
||||
Type: `Object` `Boolean`<br>
|
||||
|
||||
And object with [`lastRelease`](#lastrelease), [`nextRelease`](#nextrelease), [`commits`](#commits) and [`releases`](#releases) if a release is published or `false` if no release was published.
|
||||
|
||||
#### lastRelease
|
||||
|
||||
Type: `Object`
|
||||
|
||||
Information related to the last release found:
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|----------------------------------------------------------------------------------------------------|
|
||||
| version | `String` | The version of the last release. |
|
||||
| gitHead | `String` | The sha of the last commit being part of the last release. |
|
||||
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the last release. |
|
||||
|
||||
**Notes**: If no previous release is found, `lastRelease` will be an empty `Object`.
|
||||
|
||||
Example:
|
||||
```js
|
||||
{
|
||||
gitHead: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
|
||||
version: '1.0.0',
|
||||
gitTag: 'v1.0.0',
|
||||
}
|
||||
```
|
||||
|
||||
#### commits
|
||||
|
||||
Type: `Array<Object>`
|
||||
|
||||
The list of commit included in the new release.<br>
|
||||
Each commit object has the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
|-----------------|----------|-------------------------------------------------|
|
||||
| commit | `Object` | The commit abbreviated and full hash. |
|
||||
| commit.long | `String` | The commit hash. |
|
||||
| commit.short | `String` | The commit abbreviated hash. |
|
||||
| tree | `Object` | The commit abbreviated and full tree hash. |
|
||||
| tree.long | `String` | The commit tree hash. |
|
||||
| tree.short | `String` | The commit abbreviated tree hash. |
|
||||
| author | `Object` | The commit author information. |
|
||||
| author.name | `String` | The commit author name. |
|
||||
| author.email | `String` | The commit author email. |
|
||||
| author.short | `String` | The commit author date. |
|
||||
| committer | `Object` | The committer information. |
|
||||
| committer.name | `String` | The committer name. |
|
||||
| committer.email | `String` | The committer email. |
|
||||
| committer.short | `String` | The committer date. |
|
||||
| subject | `String` | The commit subject. |
|
||||
| body | `String` | The commit body. |
|
||||
| message | `String` | The commit full message (`subject` and `body`). |
|
||||
| hash | `String` | The commit hash. |
|
||||
| committerDate | `String` | The committer date. |
|
||||
|
||||
Example:
|
||||
```js
|
||||
[
|
||||
{
|
||||
commit: {
|
||||
long: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
short: '68eb2c4'
|
||||
},
|
||||
tree: {
|
||||
long: '7ab515d12bd2cf431745511ac4ee13fed15ab578',
|
||||
short: '7ab515d'
|
||||
},
|
||||
author: {
|
||||
name: 'Me',
|
||||
email: 'me@email.com',
|
||||
date: 2018-07-22T20:52:44.000Z
|
||||
},
|
||||
committer: {
|
||||
name: 'Me',
|
||||
email: 'me@email.com',
|
||||
date: 2018-07-22T20:52:44.000Z
|
||||
},
|
||||
subject: 'feat: a new feature',
|
||||
body: 'Description of the new feature',
|
||||
hash: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
message: 'feat: a new feature\n\nDescription of the new feature',
|
||||
committerDate: 2018-07-22T20:52:44.000Z
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### nextRelease
|
||||
|
||||
Type: `Object`
|
||||
|
||||
Information related to the newly published release:
|
||||
|
||||
| Name | Type | Description |
|
||||
|---------|----------|---------------------------------------------------------------------------------------------------|
|
||||
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
|
||||
| version | `String` | The version of the new release. |
|
||||
| gitHead | `String` | The sha of the last commit being part of the new release. |
|
||||
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the new release. |
|
||||
| notes | `String` | The release notes for the new release. |
|
||||
|
||||
Example:
|
||||
```js
|
||||
{
|
||||
type: 'minor',
|
||||
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
version: '1.1.0',
|
||||
gitTag: 'v1.1.0',
|
||||
notes: 'Release notes for version 1.1.0...',
|
||||
}
|
||||
```
|
||||
|
||||
#### releases
|
||||
|
||||
Type: `Array<Object>`
|
||||
|
||||
The list of releases published, one release per [publish plugin](../usage/plugins.md#publish-plugin).<br>
|
||||
Each release object has the following properties:
|
||||
|
||||
| Name | Type | Description |
|
||||
|------------|----------|-----------------------------------------------------------------------------------------------|
|
||||
| name | `String` | **Optional.** The release name, only if set by the corresponding `publish` plugin. |
|
||||
| url | `String` | **Optional.** The release URL, only if set by the corresponding `publish` plugin. |
|
||||
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
|
||||
| version | `String` | The version of the release. |
|
||||
| gitHead | `String` | The sha of the last commit being part of the release. |
|
||||
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the release. |
|
||||
| notes | `String` | The release notes for the release. |
|
||||
| pluginName | `String` | The name of the plugin that published the release. |
|
||||
|
||||
Example:
|
||||
```js
|
||||
[
|
||||
{
|
||||
name: 'GitHub release',
|
||||
url: 'https://github.com/me/my-package/releases/tag/v1.1.0',
|
||||
type: 'minor',
|
||||
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
version: '1.1.0',
|
||||
gitTag: 'v1.1.0',
|
||||
notes: 'Release notes for version 1.1.0...',
|
||||
pluginName: '@semantic-release/github'
|
||||
},
|
||||
{
|
||||
name: 'npm package (@latest dist-tag)',
|
||||
url: 'https://www.npmjs.com/package/my-package',
|
||||
type: 'minor',
|
||||
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
|
||||
version: '1.1.0',
|
||||
gitTag: 'v1.1.0',
|
||||
notes: 'Release notes for version 1.1.0...',
|
||||
pluginName: '@semantic-release/npm'
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -40,6 +40,9 @@
|
||||
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker) Set of semantic-release plugins for publishing a docker image to Docker Hub
|
||||
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
|
||||
- [publish](https://github.com/felixfbecker/semantic-release-docker#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
|
||||
- [semantic-release-gcr](https://github.com/carlos-cubas/semantic-release-gcr) Set of semantic-release plugins for publishing a docker image to Google Container Registry
|
||||
- [verifyConditions](https://github.com/carlos-cubas/semantic-release-gcr#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
|
||||
- [publish](https://github.com/carlos-cubas/semantic-release-gcr#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
|
||||
- [semantic-release-vsce](https://github.com/raix/semantic-release-vsce) Set of semantic-release plugins for publishing Visual Studio Code extensions to the marketplace
|
||||
- **verifyConditions** Verify the presence and the validity of the vsce authentication and release configuration
|
||||
- **prepare** Create a `.vsix` for distribution
|
||||
@@ -50,3 +53,7 @@
|
||||
- [verifyConditions](https://github.com/GabrielDuarteM/semantic-release-chrome#verifyconditions) Verify the presence of the authentication (set via environment variables).
|
||||
- [prepare](https://github.com/GabrielDuarteM/semantic-release-chrome#prepare) Write the correct version to the manifest.json and creates a zip file of the whole dist folder.
|
||||
- [publish](https://github.com/GabrielDuarteM/semantic-release-chrome#publish) Uploads the generated zip file to the webstore, and publish the item.
|
||||
- [semantic-release-firefox](https://github.com/felixfbecker/semantic-release-firefox) Set of semantic-release plugins for publishing a Firefox extension release.
|
||||
- [verifyConditions](https://github.com/felixfbecker/semantic-release-firefox#verifyconditions) Verify the presence of the authentication (set via environment variables).
|
||||
- [prepare](https://github.com/felixfbecker/semantic-release-firefox#prepare) Write the correct version to the manifest.json, creates a xpi file of the dist folder and a zip of the sources.
|
||||
- [publish](https://github.com/felixfbecker/semantic-release-firefox#publish) Submit the generated archives to the webstore for review, and publish the item including release notes.
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ See the [CI configuration recipes](../recipes/README.md#ci-configurations) for m
|
||||
|
||||
## Can I run semantic-release on my local machine rather than on a CI server?
|
||||
|
||||
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#options) option. You will also have to set the required [authentication](../usage/ci-configuration.md#authentication) via environment variables on your local machine, for example:
|
||||
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#ci) option. You will also have to set the required [authentication](../usage/ci-configuration.md#authentication) via environment variables on your local machine, for example:
|
||||
|
||||
```bash
|
||||
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npx semantic-release --no-ci
|
||||
|
||||
@@ -10,14 +10,12 @@ See [CI configuration recipes](../recipes/README.md#ci-configurations) for more
|
||||
|
||||
**semantic-release** requires push access to the project Git repository in order to create [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). The Git authentication can be set with one of the following environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
|
||||
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
|
||||
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
|
||||
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
|
||||
|
||||
`GIT_CREDENTIALS` must be the Git username and password in the format `<username>:<password>`.
|
||||
| Variable | Description |
|
||||
|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
|
||||
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
|
||||
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
|
||||
| `GIT_CREDENTIALS` | [URL encoded](https://en.wikipedia.org/wiki/Percent-encoding) Git username and password in the format `<username>:<password>`. The username and password must each be individually URL encoded, not the `:` separating them. |
|
||||
|
||||
Alternatively the Git authentication can be set up via [SSH keys](../recipes/git-auth-ssh-keys.md).
|
||||
|
||||
|
||||
+33
-60
@@ -57,8 +57,7 @@ $ semantic-release
|
||||
|
||||
### extends
|
||||
|
||||
Type: `Array`, `String`
|
||||
|
||||
Type: `Array`, `String`<br>
|
||||
CLI arguments: `-e`, `--extends`
|
||||
|
||||
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configurations are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
|
||||
@@ -67,20 +66,16 @@ List of modules or file paths containing a [shareable configuration](shareable-c
|
||||
|
||||
### branch
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `master`
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `master`<br>
|
||||
CLI arguments: `-b`, `--branch`
|
||||
|
||||
The branch on which releases should happen.
|
||||
|
||||
### repositoryUrl
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `repository` property in `package.json` or [git origin url](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)<br>
|
||||
CLI arguments: `-r`, `--repository-url`
|
||||
|
||||
The git repository URL.
|
||||
@@ -89,10 +84,8 @@ Any valid git url format is supported (See [Git protocols](https://git-scm.com/b
|
||||
|
||||
### tagFormat
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `v${version}`
|
||||
|
||||
Type: `String`<br>
|
||||
Default: `v${version}`<br>
|
||||
CLI arguments: `-t`, `--tag-format`
|
||||
|
||||
The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by **semantic-release** to identify releases. The tag name is generated with [Lodash template](https://lodash.com/docs#template) and will be compiled with the `version` variable.
|
||||
@@ -101,40 +94,34 @@ The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by
|
||||
|
||||
### dryRun
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
Default: `false` if running in a CI environment, `true` otherwise
|
||||
|
||||
Type: `Boolean`<br>
|
||||
Default: `false` if running in a CI environment, `true` otherwise<br>
|
||||
CLI arguments: `-d`, `--dry-run`
|
||||
|
||||
Dry-run mode, skip publishing, print next version and release notes.
|
||||
|
||||
### noCi
|
||||
### ci
|
||||
|
||||
Type: `Boolean`
|
||||
Type: `Boolean`<br>
|
||||
Default: `true`<br>
|
||||
CLI arguments: `--ci` / `--no-ci`
|
||||
|
||||
Default: `false`
|
||||
Set to `fasle` to skip Continuous Integration environment verifications. This allows for making releases from a local machine.
|
||||
|
||||
CLI arguments: `--no-ci`
|
||||
|
||||
Skip Continuous Integration environment verifications. This allows for making releases from a local machine.
|
||||
**Note**: The CLI arguments `--no-ci` is equivalent to `--ci false`.
|
||||
|
||||
### debug
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `Boolean`<br>
|
||||
Default: `false`<br>
|
||||
CLI argument: `--debug`
|
||||
|
||||
Output debugging information. It can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
|
||||
|
||||
### verifyConditions
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/npm', '@semantic-release/github']`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `['@semantic-release/npm', '@semantic-release/github']`<br>
|
||||
CLI argument: `--verify-conditions`
|
||||
|
||||
Define the list of [verify conditions plugins](plugins.md#verifyconditions-plugin). Plugins will run in series, in the order defined in the `Array`.
|
||||
@@ -143,10 +130,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### analyzeCommits
|
||||
|
||||
Type: `String`, `Object`
|
||||
|
||||
Default: `'@semantic-release/commit-analyzer'`
|
||||
|
||||
Type: `String`, `Object`<br>
|
||||
Default: `'@semantic-release/commit-analyzer'`<br>
|
||||
CLI argument: `--analyze-commits`
|
||||
|
||||
Define the [analyze commits plugin](plugins.md#analyzecommits-plugin).
|
||||
@@ -155,10 +140,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### verifyRelease
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `[]`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `[]`<br>
|
||||
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`.
|
||||
@@ -167,10 +150,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### generateNotes
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/release-notes-generator']`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `['@semantic-release/release-notes-generator']`<br>
|
||||
CLI argument: `--generate-notes`
|
||||
|
||||
Define the [generate notes plugins](plugins.md#generatenotes-plugin).
|
||||
@@ -179,10 +160,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### prepare
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/npm']`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `['@semantic-release/npm']`<br>
|
||||
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`.
|
||||
@@ -191,10 +170,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### publish
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/npm', '@semantic-release/github']`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `['@semantic-release/npm', '@semantic-release/github']`<br>
|
||||
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`.
|
||||
@@ -203,10 +180,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### success
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/github']`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `['@semantic-release/github']`<br>
|
||||
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`.
|
||||
@@ -215,10 +190,8 @@ See [Plugins configuration](plugins.md#configuration) for more details.
|
||||
|
||||
### fail
|
||||
|
||||
Type: `Array`, `String`, `Object`
|
||||
|
||||
Default: `['@semantic-release/github']`
|
||||
|
||||
Type: `Array`, `String`, `Object`<br>
|
||||
Default: `['@semantic-release/github']`<br>
|
||||
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`.
|
||||
|
||||
+24
-8
@@ -10,25 +10,33 @@ See [plugins list](../extending/plugins-list.md).
|
||||
|
||||
Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
|
||||
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
### analyzeCommits plugin
|
||||
|
||||
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).
|
||||
Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).<br>
|
||||
Required.<br>
|
||||
Accept only one plugin.
|
||||
|
||||
### 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.
|
||||
Default implementation: none.<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
### 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).
|
||||
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
### prepare plugin
|
||||
|
||||
@@ -36,25 +44,33 @@ 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).
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
### 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).
|
||||
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
### success plugin
|
||||
|
||||
Responsible for notifying of a new release.
|
||||
|
||||
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).
|
||||
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
### fail plugin
|
||||
|
||||
Responsible for notifying of a failed release.
|
||||
|
||||
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).
|
||||
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).<br>
|
||||
Optional.<br>
|
||||
Accept multiple plugins.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const process = require('process');
|
||||
const {template} = require('lodash');
|
||||
const {template, pick} = require('lodash');
|
||||
const marked = require('marked');
|
||||
const TerminalRenderer = require('marked-terminal');
|
||||
const envCi = require('env-ci');
|
||||
@@ -13,7 +12,7 @@ const getCommits = require('./lib/get-commits');
|
||||
const getLastRelease = require('./lib/get-last-release');
|
||||
const {extractErrors} = require('./lib/utils');
|
||||
const getGitAuthUrl = require('./lib/get-git-auth-url');
|
||||
const logger = require('./lib/logger');
|
||||
const getLogger = require('./lib/get-logger');
|
||||
const {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
|
||||
const getError = require('./lib/get-error');
|
||||
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
|
||||
@@ -21,8 +20,8 @@ const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
|
||||
marked.setOptions({renderer: new TerminalRenderer()});
|
||||
|
||||
async function run(context, plugins) {
|
||||
const {isCi, branch: ciBranch, isPr} = envCi();
|
||||
const {cwd, env, options, logger} = context;
|
||||
const {isCi, branch: ciBranch, isPr} = envCi({env, cwd});
|
||||
|
||||
if (!isCi && !options.dryRun && !options.noCi) {
|
||||
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
@@ -42,7 +41,7 @@ async function run(context, plugins) {
|
||||
|
||||
if (isCi && isPr && !options.noCi) {
|
||||
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ciBranch !== options.branch) {
|
||||
@@ -53,6 +52,7 @@ async function run(context, plugins) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
logger.success(`Run automated release from branch ${ciBranch}`);
|
||||
|
||||
await verify(context);
|
||||
|
||||
@@ -60,19 +60,18 @@ async function run(context, plugins) {
|
||||
|
||||
try {
|
||||
await verifyAuth(options.repositoryUrl, options.branch, {cwd, env});
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
|
||||
logger.log(
|
||||
"The local branch %s is behind the remote one, therefore a new version won't be published.",
|
||||
options.branch
|
||||
`The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr);
|
||||
logger.error(`The command "${error.cmd}" failed with the error message ${error.stderr}.`);
|
||||
throw getError('EGITNOPERMISSION', {options});
|
||||
}
|
||||
|
||||
logger.log('Run automated release from branch %s', options.branch);
|
||||
logger.success(`Allowed to push to the Git repository`);
|
||||
|
||||
await plugins.verifyConditions(context);
|
||||
|
||||
@@ -85,7 +84,7 @@ async function run(context, plugins) {
|
||||
|
||||
if (!nextRelease.type) {
|
||||
logger.log('There are no relevant changes, so no new version is released.');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
context.nextRelease = nextRelease;
|
||||
nextRelease.version = getNextVersion(context);
|
||||
@@ -95,35 +94,36 @@ async function run(context, plugins) {
|
||||
|
||||
if (options.dryRun) {
|
||||
const notes = await plugins.generateNotes(context);
|
||||
logger.log('Release note for version %s:\n', nextRelease.version);
|
||||
logger.log(`Release note for version ${nextRelease.version}:`);
|
||||
if (notes) {
|
||||
logger.stdout(`${marked(notes)}\n`);
|
||||
context.stdout.write(marked(notes));
|
||||
}
|
||||
} else {
|
||||
nextRelease.notes = await plugins.generateNotes(context);
|
||||
await plugins.prepare(context);
|
||||
|
||||
// Create the tag before calling the publish plugins as some require the tag to exists
|
||||
logger.log('Create tag %s', nextRelease.gitTag);
|
||||
await tag(nextRelease.gitTag, {cwd, env});
|
||||
await push(options.repositoryUrl, options.branch, {cwd, env});
|
||||
logger.success(`Created tag ${nextRelease.gitTag}`);
|
||||
|
||||
context.releases = await plugins.publish(context);
|
||||
|
||||
await plugins.success(context);
|
||||
|
||||
logger.log('Published release: %s', nextRelease.version);
|
||||
logger.success(`Published release ${nextRelease.version}`);
|
||||
}
|
||||
return true;
|
||||
|
||||
return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
|
||||
}
|
||||
|
||||
function logErrors({logger}, err) {
|
||||
function logErrors({logger, stderr}, err) {
|
||||
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
|
||||
for (const error of errors) {
|
||||
if (error.semanticRelease) {
|
||||
logger.log(`%s ${error.message}`, error.code);
|
||||
logger.error(`${error.code} ${error.message}`);
|
||||
if (error.details) {
|
||||
logger.stderr(`${marked(error.details)}\n`);
|
||||
stderr.write(marked(error.details));
|
||||
}
|
||||
} else {
|
||||
logger.error('An error occurred while running semantic-release: %O', error);
|
||||
@@ -131,21 +131,25 @@ function logErrors({logger}, err) {
|
||||
}
|
||||
}
|
||||
|
||||
async function callFail(context, plugins, error) {
|
||||
const errors = extractErrors(error).filter(error => error.semanticRelease);
|
||||
async function callFail(context, plugins, err) {
|
||||
const errors = extractErrors(err).filter(err => err.semanticRelease);
|
||||
if (errors.length > 0) {
|
||||
try {
|
||||
await plugins.fail({...context, errors});
|
||||
} catch (err) {
|
||||
logErrors(context, err);
|
||||
} catch (error) {
|
||||
logErrors(context, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) => {
|
||||
const context = {cwd, env, logger};
|
||||
context.logger.log(`Running %s version %s`, pkg.name, pkg.version);
|
||||
const {unhook} = hookStd({silent: false}, hideSensitive(context.env));
|
||||
module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
|
||||
const {unhook} = hookStd(
|
||||
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
|
||||
hideSensitive(env)
|
||||
);
|
||||
const context = {cwd, env, stdout: stdout || process.stdout, stderr: stderr || process.stderr};
|
||||
context.logger = getLogger(context);
|
||||
context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
|
||||
try {
|
||||
const {plugins, options} = await getConfig(context, opts);
|
||||
context.options = options;
|
||||
@@ -153,15 +157,15 @@ module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) =>
|
||||
const result = await run(context, plugins);
|
||||
unhook();
|
||||
return result;
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (!options.dryRun) {
|
||||
await callFail(context, plugins, err);
|
||||
await callFail(context, plugins, error);
|
||||
}
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
} catch (err) {
|
||||
logErrors(context, err);
|
||||
} catch (error) {
|
||||
logErrors(context, error);
|
||||
unhook();
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,4 +10,14 @@ const RELEASE_NOTES_SEPARATOR = '\n\n';
|
||||
|
||||
const SECRET_REPLACEMENT = '[secure]';
|
||||
|
||||
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT};
|
||||
const SECRET_MIN_SIZE = 5;
|
||||
|
||||
module.exports = {
|
||||
RELEASE_TYPE,
|
||||
FIRST_RELEASE,
|
||||
COMMIT_NAME,
|
||||
COMMIT_EMAIL,
|
||||
RELEASE_NOTES_SEPARATOR,
|
||||
SECRET_REPLACEMENT,
|
||||
SECRET_MIN_SIZE,
|
||||
};
|
||||
|
||||
@@ -55,11 +55,13 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`
|
||||
|
||||
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
|
||||
}),
|
||||
EPLUGINCONF: ({type, pluginConf}) => ({
|
||||
EPLUGINCONF: ({type, multiple, required, pluginConf}) => ({
|
||||
message: `The \`${type}\` plugin configuration is invalid.`,
|
||||
details: `The [${type} plugin configuration](${linkify(
|
||||
`docs/usage/plugins.md#${toLower(type)}-plugin`
|
||||
)}) if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a \`path\` property.
|
||||
details: `The [${type} plugin configuration](${linkify(`docs/usage/plugins.md#${toLower(type)}-plugin`)}) ${
|
||||
required ? 'is required and ' : ''
|
||||
}must be ${
|
||||
multiple ? 'a single or an array of plugins' : 'a single plugin'
|
||||
} definition. A plugin definition is either a string or an object with a \`path\` property.
|
||||
|
||||
Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`,
|
||||
}),
|
||||
|
||||
+22
-12
@@ -1,18 +1,20 @@
|
||||
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
|
||||
const {isString, isPlainObject} = require('lodash');
|
||||
const {gitHead} = require('../git');
|
||||
const hideSensitive = require('../hide-sensitive');
|
||||
const {hideSensitiveValues} = require('../utils');
|
||||
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
|
||||
|
||||
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
|
||||
|
||||
module.exports = {
|
||||
verifyConditions: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
},
|
||||
analyzeCommits: {
|
||||
default: '@semantic-release/commit-analyzer',
|
||||
configValidator: conf => Boolean(conf) && validatePluginConfig(conf),
|
||||
multiple: false,
|
||||
required: true,
|
||||
outputValidator: output => !output || RELEASE_TYPE.includes(output),
|
||||
preprocess: ({commits, ...inputs}) => ({
|
||||
...inputs,
|
||||
@@ -22,12 +24,14 @@ module.exports = {
|
||||
},
|
||||
verifyRelease: {
|
||||
default: false,
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
},
|
||||
generateNotes: {
|
||||
default: ['@semantic-release/release-notes-generator'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
outputValidator: output => !output || isString(output),
|
||||
pipelineConfig: () => ({
|
||||
getNextInput: ({nextRelease, ...context}, notes) => ({
|
||||
@@ -38,11 +42,12 @@ module.exports = {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
postprocess: results => results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR),
|
||||
postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)),
|
||||
},
|
||||
prepare: {
|
||||
default: ['@semantic-release/npm'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: ({generateNotes}, logger) => ({
|
||||
getNextInput: async context => {
|
||||
const newGitHead = await gitHead({cwd: context.cwd});
|
||||
@@ -60,7 +65,8 @@ module.exports = {
|
||||
},
|
||||
publish: {
|
||||
default: ['@semantic-release/npm', '@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
outputValidator: output => !output || isPlainObject(output),
|
||||
pipelineConfig: () => ({
|
||||
// Add `nextRelease` and plugin properties to published release
|
||||
@@ -73,12 +79,16 @@ module.exports = {
|
||||
},
|
||||
success: {
|
||||
default: ['@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}),
|
||||
},
|
||||
fail: {
|
||||
default: ['@semantic-release/github'],
|
||||
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
|
||||
multiple: true,
|
||||
required: false,
|
||||
pipelineConfig: () => ({settleAll: true}),
|
||||
preprocess: ({errors, env, ...inputs}) => ({...inputs, env, errors: hideSensitiveValues(env, errors)}),
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ module.exports = async ({cwd, env, lastRelease: {gitHead}, logger}) => {
|
||||
commit.gitTags = commit.gitTags.trim();
|
||||
return commit;
|
||||
});
|
||||
logger.log('Found %s commits since last release', commits.length);
|
||||
logger.log(`Found ${commits.length} commits since last release`);
|
||||
debug('Parsed commits: %o', commits);
|
||||
return commits;
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ module.exports = async ({cwd, env, options: {repositoryUrl, branch}}) => {
|
||||
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
|
||||
try {
|
||||
await verifyAuth(repositoryUrl, branch, {cwd, env});
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(env[envVar]));
|
||||
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
|
||||
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
|
||||
|
||||
@@ -42,7 +42,7 @@ module.exports = async ({cwd, env, options: {tagFormat}, logger}) => {
|
||||
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true});
|
||||
|
||||
if (tag) {
|
||||
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
|
||||
logger.log(`Found git tag ${tag.gitTag} associated with version ${tag.version}`);
|
||||
return {gitHead: await gitTagHead(tag.gitTag, {cwd, env}), ...tag};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const {Signale} = require('signale');
|
||||
const figures = require('figures');
|
||||
|
||||
module.exports = ({stdout, stderr}) =>
|
||||
new Signale({
|
||||
config: {displayTimestamp: true, underlineMessage: true, displayLabel: false},
|
||||
disabled: false,
|
||||
interactive: false,
|
||||
scope: 'semantic-release',
|
||||
stream: [stdout],
|
||||
types: {
|
||||
error: {badge: figures.cross, color: 'red', label: '', stream: [stderr]},
|
||||
log: {badge: figures.info, color: 'magenta', label: '', stream: [stdout]},
|
||||
success: {badge: figures.tick, color: 'green', label: '', stream: [stdout]},
|
||||
},
|
||||
});
|
||||
@@ -5,10 +5,10 @@ module.exports = ({nextRelease: {type}, lastRelease, logger}) => {
|
||||
let version;
|
||||
if (lastRelease.version) {
|
||||
version = semver.inc(lastRelease.version, type);
|
||||
logger.log('The next release version is %s', version);
|
||||
logger.log(`The next release version is ${version}`);
|
||||
} else {
|
||||
version = FIRST_RELEASE;
|
||||
logger.log('There is no previous release, the next release version is %s', version);
|
||||
logger.log(`There is no previous release, the next release version is ${version}`);
|
||||
}
|
||||
|
||||
return version;
|
||||
|
||||
+19
-19
@@ -12,8 +12,8 @@ const debug = require('debug')('semantic-release:git');
|
||||
async function gitTagHead(tagName, execaOpts) {
|
||||
try {
|
||||
return await execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +44,13 @@ async function isRefInHistory(ref, execaOpts) {
|
||||
try {
|
||||
await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], execaOpts);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code === 1) {
|
||||
} catch (error) {
|
||||
if (error.code === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
debug(err);
|
||||
throw err;
|
||||
debug(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ async function isRefInHistory(ref, execaOpts) {
|
||||
async function fetch(repositoryUrl, execaOpts) {
|
||||
try {
|
||||
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], execaOpts);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
await execa('git', ['fetch', '--tags', repositoryUrl], execaOpts);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ async function fetch(repositoryUrl, execaOpts) {
|
||||
*
|
||||
* @return {string} the sha of the HEAD commit.
|
||||
*/
|
||||
async function gitHead(execaOpts) {
|
||||
function gitHead(execaOpts) {
|
||||
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ async function gitHead(execaOpts) {
|
||||
async function repoUrl(execaOpts) {
|
||||
try {
|
||||
return await execa.stdout('git', ['config', '--get', 'remote.origin.url'], execaOpts);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ async function repoUrl(execaOpts) {
|
||||
async function isGitRepo(execaOpts) {
|
||||
try {
|
||||
return (await execa('git', ['rev-parse', '--git-dir'], execaOpts)).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,9 @@ async function isGitRepo(execaOpts) {
|
||||
async function verifyAuth(repositoryUrl, branch, execaOpts) {
|
||||
try {
|
||||
await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`], execaOpts);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
throw err;
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +163,8 @@ async function push(repositoryUrl, branch, execaOpts) {
|
||||
async function verifyTagName(tagName, execaOpts) {
|
||||
try {
|
||||
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`], execaOpts)).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ async function isBranchUpToDate(branch, execaOpts) {
|
||||
(await execa.stdout('git', ['ls-remote', '--heads', 'origin', branch], execaOpts)).match(/^(\w+)?/)[1],
|
||||
execaOpts
|
||||
);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
const {escapeRegExp} = require('lodash');
|
||||
const {SECRET_REPLACEMENT} = require('./definitions/constants');
|
||||
const {escapeRegExp, size, isString} = require('lodash');
|
||||
const {SECRET_REPLACEMENT, SECRET_MIN_SIZE} = require('./definitions/constants');
|
||||
|
||||
module.exports = env => {
|
||||
const toReplace = Object.keys(env).filter(
|
||||
envVar => /token|password|credential|secret|private/i.test(envVar) && env[envVar].trim()
|
||||
envVar => /token|password|credential|secret|private/i.test(envVar) && size(env[envVar].trim()) >= SECRET_MIN_SIZE
|
||||
);
|
||||
|
||||
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(env[envVar])).join('|'), 'g');
|
||||
return output => {
|
||||
return output && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output;
|
||||
};
|
||||
return output =>
|
||||
output && isString(output) && toReplace.length > 0 ? output.toString().replace(regexp, SECRET_REPLACEMENT) : output;
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* Logger with `log` and `error` function.
|
||||
*/
|
||||
module.exports = {
|
||||
log(...args) {
|
||||
const [format, ...rest] = args;
|
||||
console.log(
|
||||
`${chalk.grey('[Semantic release]:')}${
|
||||
typeof format === 'string' ? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}` : ''
|
||||
}`,
|
||||
...(typeof format === 'string' ? [] : [format]).concat(rest)
|
||||
);
|
||||
},
|
||||
error(...args) {
|
||||
const [format, ...rest] = args;
|
||||
console.error(
|
||||
`${chalk.grey('[Semantic release]:')}${typeof format === 'string' ? ` ${chalk.red(format)}` : ''}`,
|
||||
...(typeof format === 'string' ? [] : [format]).concat(rest)
|
||||
);
|
||||
},
|
||||
stdout(...args) {
|
||||
console.log(args);
|
||||
},
|
||||
stderr(...args) {
|
||||
console.error(args);
|
||||
},
|
||||
};
|
||||
+11
-6
@@ -2,15 +2,17 @@ const {identity, isPlainObject, omit, castArray, isUndefined} = require('lodash'
|
||||
const AggregateError = require('aggregate-error');
|
||||
const getError = require('../get-error');
|
||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||
const {validateConfig} = require('./utils');
|
||||
const pipeline = require('./pipeline');
|
||||
const normalize = require('./normalize');
|
||||
|
||||
module.exports = ({cwd, options, logger}, pluginsPath) => {
|
||||
module.exports = (context, pluginsPath) => {
|
||||
const {options, logger} = context;
|
||||
const errors = [];
|
||||
const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
|
||||
(
|
||||
plugins,
|
||||
[type, {configValidator, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
|
||||
[type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
|
||||
) => {
|
||||
let pluginOpts;
|
||||
|
||||
@@ -22,19 +24,22 @@ module.exports = ({cwd, options, logger}, pluginsPath) => {
|
||||
if (isPlainObject(options[type]) && !options[type].path && defaultPaths.length === 1) {
|
||||
[options[type].path] = defaultPaths;
|
||||
}
|
||||
if (configValidator && !configValidator(options[type])) {
|
||||
errors.push(getError('EPLUGINCONF', {type, pluginConf: options[type]}));
|
||||
if (!validateConfig({multiple, required}, options[type])) {
|
||||
errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]}));
|
||||
return plugins;
|
||||
}
|
||||
pluginOpts = options[type];
|
||||
}
|
||||
|
||||
const steps = castArray(pluginOpts).map(pluginOpt =>
|
||||
normalize({cwd, options: omit(options, Object.keys(PLUGINS_DEFINITIONS)), logger}, type, pluginOpt, pluginsPath)
|
||||
normalize({...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS))}, type, pluginOpt, pluginsPath)
|
||||
);
|
||||
|
||||
plugins[type] = async input =>
|
||||
postprocess(await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)));
|
||||
postprocess(
|
||||
await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)),
|
||||
input
|
||||
);
|
||||
|
||||
return plugins;
|
||||
},
|
||||
|
||||
+23
-15
@@ -1,11 +1,11 @@
|
||||
const {dirname} = require('path');
|
||||
const {isString, isPlainObject, isFunction, noop, cloneDeep} = require('lodash');
|
||||
const {isString, isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash');
|
||||
const resolveFrom = require('resolve-from');
|
||||
const getError = require('../get-error');
|
||||
const {extractErrors} = require('../utils');
|
||||
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
|
||||
|
||||
module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
||||
module.exports = ({cwd, stdout, stderr, options, logger}, type, pluginOpt, pluginsPath) => {
|
||||
if (!pluginOpt) {
|
||||
return noop;
|
||||
}
|
||||
@@ -13,14 +13,6 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
||||
const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt;
|
||||
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
|
||||
|
||||
if (!isFunction(pluginOpt)) {
|
||||
if (pluginsPath[path]) {
|
||||
logger.log('Load plugin "%s" from %s in shareable config %s', type, path, pluginsPath[path]);
|
||||
} else {
|
||||
logger.log('Load plugin "%s" from %s', type, path);
|
||||
}
|
||||
}
|
||||
|
||||
const basePath = pluginsPath[path]
|
||||
? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path]))
|
||||
: __dirname;
|
||||
@@ -38,18 +30,34 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
|
||||
const validator = async input => {
|
||||
const {outputValidator} = PLUGINS_DEFINITIONS[type] || {};
|
||||
try {
|
||||
logger.log('Call plugin "%s"', type);
|
||||
const result = await func(cloneDeep(input));
|
||||
logger.log(`Start step "${type}" of plugin "${pluginName}"`);
|
||||
const result = await func({
|
||||
...cloneDeep(omit(input, ['stdout', 'stderr', 'logger'])),
|
||||
stdout,
|
||||
stderr,
|
||||
logger: logger.scope(logger.scopeName, pluginName),
|
||||
});
|
||||
if (outputValidator && !outputValidator(result)) {
|
||||
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
|
||||
}
|
||||
logger.success(`Completed step "${type}" of plugin "${pluginName}"`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
|
||||
throw err;
|
||||
} catch (error) {
|
||||
logger.error(`Failed step "${type}" of plugin "${pluginName}"`);
|
||||
extractErrors(error).forEach(err => Object.assign(err, {pluginName}));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
|
||||
|
||||
if (!isFunction(pluginOpt)) {
|
||||
if (pluginsPath[path]) {
|
||||
logger.success(`Loaded plugin "${type}" from "${path}" in shareable config "${pluginsPath[path]}"`);
|
||||
} else {
|
||||
logger.success(`Loaded plugin "${type}" from "${path}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return validator;
|
||||
};
|
||||
|
||||
@@ -36,12 +36,12 @@ module.exports = (steps, {settleAll = false, getNextInput = identity, transform
|
||||
// Call the step with the input computed at the end of the previous iteration and save intermediary result
|
||||
result = await transform(await step(lastInput), step, lastInput);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
if (settleAll) {
|
||||
errors.push(...extractErrors(err));
|
||||
result = err;
|
||||
errors.push(...extractErrors(error));
|
||||
result = error;
|
||||
} else {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Prepare input for the next step, passing the input of the last iteration (or initial parameter for the first iteration) and the result of the current one
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const {isString, isFunction, castArray} = require('lodash');
|
||||
|
||||
const validateSingleConfig = conf => {
|
||||
conf = castArray(conf);
|
||||
return conf.length === 1 && (isString(conf[0]) || isString(conf[0].path) || isFunction(conf[0]));
|
||||
};
|
||||
|
||||
const validateMultipleConfig = conf => castArray(conf).every(conf => validateSingleConfig(conf));
|
||||
|
||||
const validateConfig = ({multiple, required}, conf) => {
|
||||
conf = castArray(conf).filter(Boolean);
|
||||
if (required) {
|
||||
return Boolean(conf) && conf.length >= 1 && (multiple ? validateMultipleConfig : validateSingleConfig)(conf);
|
||||
}
|
||||
return conf.length === 0 || (multiple ? validateMultipleConfig : validateSingleConfig)(conf);
|
||||
};
|
||||
|
||||
module.exports = {validateConfig};
|
||||
+14
-1
@@ -1,7 +1,20 @@
|
||||
const {isFunction} = require('lodash');
|
||||
const hideSensitive = require('./hide-sensitive');
|
||||
|
||||
function extractErrors(err) {
|
||||
return err && isFunction(err[Symbol.iterator]) ? [...err] : [err];
|
||||
}
|
||||
|
||||
module.exports = {extractErrors};
|
||||
function hideSensitiveValues(env, objs) {
|
||||
const hideFunction = hideSensitive(env);
|
||||
return objs.map(obj => {
|
||||
Object.getOwnPropertyNames(obj).forEach(prop => {
|
||||
if (obj[prop]) {
|
||||
obj[prop] = hideFunction(obj[prop]);
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {extractErrors, hideSensitiveValues};
|
||||
|
||||
+14
-12
@@ -22,28 +22,29 @@
|
||||
"@semantic-release/commit-analyzer": "^6.0.0",
|
||||
"@semantic-release/error": "^2.2.0",
|
||||
"@semantic-release/github": "^5.0.0",
|
||||
"@semantic-release/npm": "^4.0.0",
|
||||
"@semantic-release/npm": "^5.0.1",
|
||||
"@semantic-release/release-notes-generator": "^7.0.0",
|
||||
"aggregate-error": "^1.0.0",
|
||||
"chalk": "^2.3.0",
|
||||
"cosmiconfig": "^5.0.1",
|
||||
"debug": "^3.1.0",
|
||||
"env-ci": "^2.0.0",
|
||||
"execa": "^0.10.0",
|
||||
"debug": "^4.0.0",
|
||||
"env-ci": "^3.0.0",
|
||||
"execa": "^1.0.0",
|
||||
"figures": "^2.0.0",
|
||||
"find-versions": "^2.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"git-log-parser": "^1.2.0",
|
||||
"git-url-parse": "^10.0.1",
|
||||
"hook-std": "^1.0.1",
|
||||
"hook-std": "^1.1.0",
|
||||
"hosted-git-info": "^2.7.1",
|
||||
"lodash": "^4.17.4",
|
||||
"marked": "^0.4.0",
|
||||
"marked": "^0.5.0",
|
||||
"marked-terminal": "^3.0.0",
|
||||
"p-locate": "^3.0.0",
|
||||
"p-reduce": "^1.0.0",
|
||||
"read-pkg-up": "^4.0.0",
|
||||
"resolve-from": "^4.0.0",
|
||||
"semver": "^5.4.1",
|
||||
"signale": "^1.2.1",
|
||||
"yargs": "^12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,20 +53,21 @@
|
||||
"codecov": "^3.0.0",
|
||||
"commitizen": "^2.9.6",
|
||||
"cz-conventional-changelog": "^2.0.0",
|
||||
"delay": "^3.0.0",
|
||||
"delay": "^4.0.0",
|
||||
"dockerode": "^2.5.2",
|
||||
"file-url": "^2.0.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"got": "^8.0.0",
|
||||
"got": "^9.0.0",
|
||||
"js-yaml": "^3.10.0",
|
||||
"mockserver-client": "^5.1.1",
|
||||
"nock": "^9.0.2",
|
||||
"nock": "^10.0.0",
|
||||
"nyc": "^12.0.1",
|
||||
"p-retry": "^2.0.0",
|
||||
"proxyquire": "^2.0.0",
|
||||
"sinon": "^6.0.0",
|
||||
"stream-buffers": "^3.0.2",
|
||||
"tempy": "^0.2.1",
|
||||
"xo": "^0.21.0"
|
||||
"xo": "^0.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.3"
|
||||
|
||||
+15
-1
@@ -1,6 +1,8 @@
|
||||
import test from 'ava';
|
||||
import {escapeRegExp} from 'lodash';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||
|
||||
const requireNoCache = proxyquire.noPreserveCache();
|
||||
|
||||
@@ -70,7 +72,7 @@ test.serial('Pass options to semantic-release API', async t => {
|
||||
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
|
||||
t.is(run.args[0][0].analyzeCommits, 'analyze');
|
||||
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']);
|
||||
t.is(run.args[0][0].generateNotes, 'notes');
|
||||
t.deepEqual(run.args[0][0].generateNotes, ['notes']);
|
||||
t.deepEqual(run.args[0][0].prepare, ['prepare1', 'prepare2']);
|
||||
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
|
||||
t.deepEqual(run.args[0][0].success, ['success1', 'success2']);
|
||||
@@ -208,3 +210,15 @@ test.serial('Return error code if semantic-release throw error', async t => {
|
||||
t.regex(t.context.errors, /semantic-release error/);
|
||||
t.is(exitCode, 1);
|
||||
});
|
||||
|
||||
test.serial('Hide sensitive environment variable values from the logs', async t => {
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
const run = stub().rejects(new Error(`Throw error: Exposing token ${env.MY_TOKEN}`));
|
||||
const argv = ['', ''];
|
||||
const cli = requireNoCache('../cli', {'.': run, process: {...process, argv, env: {...process.env, ...env}}});
|
||||
|
||||
const exitCode = await cli();
|
||||
|
||||
t.regex(t.context.errors, new RegExp(`Throw error: Exposing token ${escapeRegExp(SECRET_REPLACEMENT)}`));
|
||||
t.is(exitCode, 1);
|
||||
});
|
||||
|
||||
@@ -1,94 +1,6 @@
|
||||
import test from 'ava';
|
||||
import plugins from '../../lib/definitions/plugins';
|
||||
import {RELEASE_NOTES_SEPARATOR} from '../../lib/definitions/constants';
|
||||
|
||||
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyConditions.configValidator({}));
|
||||
t.false(plugins.verifyConditions.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.verifyConditions.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyConditions.configValidator());
|
||||
t.true(plugins.verifyConditions.configValidator('plugin-path.js'));
|
||||
t.true(plugins.verifyConditions.configValidator(() => {}));
|
||||
t.true(plugins.verifyConditions.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
|
||||
t.false(plugins.analyzeCommits.configValidator({}));
|
||||
t.false(plugins.analyzeCommits.configValidator({path: null}));
|
||||
t.false(plugins.analyzeCommits.configValidator([]));
|
||||
t.false(plugins.analyzeCommits.configValidator());
|
||||
|
||||
t.true(plugins.analyzeCommits.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.analyzeCommits.configValidator('plugin-path.js'));
|
||||
t.true(plugins.analyzeCommits.configValidator(() => {}));
|
||||
});
|
||||
|
||||
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyRelease.configValidator({}));
|
||||
t.false(plugins.verifyRelease.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyRelease.configValidator());
|
||||
t.true(plugins.verifyRelease.configValidator('plugin-path.js'));
|
||||
t.true(plugins.verifyRelease.configValidator(() => {}));
|
||||
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.generateNotes.configValidator({}));
|
||||
t.false(plugins.generateNotes.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.generateNotes.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.generateNotes.configValidator());
|
||||
t.true(plugins.generateNotes.configValidator('plugin-path.js'));
|
||||
t.true(plugins.generateNotes.configValidator(() => {}));
|
||||
t.true(plugins.generateNotes.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.verifyRelease.configValidator({}));
|
||||
t.false(plugins.verifyRelease.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.verifyRelease.configValidator());
|
||||
t.true(plugins.verifyRelease.configValidator('plugin-path.js'));
|
||||
t.true(plugins.verifyRelease.configValidator(() => {}));
|
||||
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.publish.configValidator({}));
|
||||
t.false(plugins.publish.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.publish.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.publish.configValidator());
|
||||
t.true(plugins.publish.configValidator('plugin-path.js'));
|
||||
t.true(plugins.publish.configValidator(() => {}));
|
||||
t.true(plugins.publish.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.success.configValidator({}));
|
||||
t.false(plugins.success.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.success.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.success.configValidator());
|
||||
t.true(plugins.success.configValidator('plugin-path.js'));
|
||||
t.true(plugins.success.configValidator(() => {}));
|
||||
t.true(plugins.success.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => {
|
||||
t.false(plugins.fail.configValidator({}));
|
||||
t.false(plugins.fail.configValidator({path: null}));
|
||||
|
||||
t.true(plugins.fail.configValidator({path: 'plugin-path.js'}));
|
||||
t.true(plugins.fail.configValidator());
|
||||
t.true(plugins.fail.configValidator('plugin-path.js'));
|
||||
t.true(plugins.fail.configValidator(() => {}));
|
||||
t.true(plugins.fail.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
import {RELEASE_NOTES_SEPARATOR, SECRET_REPLACEMENT} from '../../lib/definitions/constants';
|
||||
|
||||
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
|
||||
t.false(plugins.analyzeCommits.outputValidator('invalid'));
|
||||
@@ -120,10 +32,22 @@ test('The "publish" plugin output, if defined, must be an object', t => {
|
||||
t.true(plugins.publish.outputValidator(''));
|
||||
});
|
||||
|
||||
test('The "generateNotes" plugins output are concatenated with separator', t => {
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(plugins.generateNotes.postprocess(['', 'note']), 'note');
|
||||
t.is(plugins.generateNotes.postprocess([undefined, 'note']), 'note');
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', undefined, 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
test('The "generateNotes" plugins output are concatenated with separator and sensitive data is hidden', t => {
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(plugins.generateNotes.postprocess(['', 'note'], {env}), 'note');
|
||||
t.is(plugins.generateNotes.postprocess([undefined, 'note'], {env}), 'note');
|
||||
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2'], {env}), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
|
||||
t.is(
|
||||
plugins.generateNotes.postprocess(['note 1', undefined, 'note 2'], {env}),
|
||||
`note 1${RELEASE_NOTES_SEPARATOR}note 2`
|
||||
);
|
||||
|
||||
t.is(
|
||||
plugins.generateNotes.postprocess(
|
||||
[`Note 1: Exposing token ${env.MY_TOKEN}`, `Note 2: Exposing token ${SECRET_REPLACEMENT}`],
|
||||
{env}
|
||||
),
|
||||
`Note 1: Exposing token ${SECRET_REPLACEMENT}${RELEASE_NOTES_SEPARATOR}Note 2: Exposing token ${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
Vendored
-1
@@ -1,2 +1 @@
|
||||
module.exports = () => {};
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
|
||||
class InheritedError extends SemanticReleaseError {
|
||||
constructor(message, code, newProperty) {
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
|
||||
@@ -27,7 +27,7 @@ test('Get the highest non-prerelease valid tag', async t => {
|
||||
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
|
||||
|
||||
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
|
||||
t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']);
|
||||
t.deepEqual(t.context.log.args[0], ['Found git tag v2.0.0 associated with version 2.0.0']);
|
||||
});
|
||||
|
||||
test('Get the highest tag in the history of the current branch', async t => {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import test from 'ava';
|
||||
import {spy} from 'sinon';
|
||||
import getLogger from '../lib/get-logger';
|
||||
|
||||
test('Expose "error", "success" and "log" functions', t => {
|
||||
const stdout = spy();
|
||||
const stderr = spy();
|
||||
const logger = getLogger({stdout: {write: stdout}, stderr: {write: stderr}});
|
||||
|
||||
logger.log('test log');
|
||||
logger.success('test success');
|
||||
logger.error('test error');
|
||||
|
||||
t.regex(stdout.args[0][0], /.*test log/);
|
||||
t.regex(stdout.args[1][0], /.*test success/);
|
||||
t.regex(stderr.args[0][0], /.*test error/);
|
||||
});
|
||||
@@ -69,7 +69,7 @@ export async function initBareRepo(repositoryUrl, branch = 'master') {
|
||||
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
|
||||
*/
|
||||
export async function gitCommits(messages, execaOpts) {
|
||||
await pReduce(messages, async (_, message) =>
|
||||
await pReduce(messages, (_, message) =>
|
||||
execa.stdout('git', ['commit', '-m', message, '--allow-empty', '--no-gpg-sign'], execaOpts)
|
||||
);
|
||||
return (await gitGetCommits(undefined, execaOpts)).slice(0, messages.length);
|
||||
@@ -112,7 +112,7 @@ export async function gitCheckout(branch, create = true, execaOpts) {
|
||||
*
|
||||
* @return {String} The sha of the head commit in the current git repository.
|
||||
*/
|
||||
export async function gitHead(execaOpts) {
|
||||
export function gitHead(execaOpts) {
|
||||
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function gitAddConfig(name, value, execaOpts) {
|
||||
*
|
||||
* @return {String} The sha of the commit associated with `tagName` on the local repository.
|
||||
*/
|
||||
export async function gitTagHead(tagName, execaOpts) {
|
||||
export function gitTagHead(tagName, execaOpts) {
|
||||
return execa.stdout('git', ['rev-list', '-1', tagName], execaOpts);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ export async function gitRemoteTagHead(repositoryUrl, tagName, execaOpts) {
|
||||
*
|
||||
* @return {String} The tag associatedwith the sha in parameter or `null`.
|
||||
*/
|
||||
export async function gitCommitTag(gitHead, execaOpts) {
|
||||
export function gitCommitTag(gitHead, execaOpts) {
|
||||
return execa.stdout('git', ['describe', '--tags', '--exact-match', gitHead], execaOpts);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ async function start() {
|
||||
minTimeout: 1000,
|
||||
factor: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
throw new Error(`Couldn't start mock-server after 2 min`);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ async function mock(
|
||||
* @param {Object} expectation The expectation created with `mock` function.
|
||||
* @return {Promise} A Promise that resolves if the expectation is met or reject otherwise.
|
||||
*/
|
||||
async function verify(expectation) {
|
||||
function verify(expectation) {
|
||||
return client.verify(expectation);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import got from 'got';
|
||||
import delay from 'delay';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
|
||||
const IMAGE = 'semanticrelease/npm-registry-docker:latest';
|
||||
const SERVER_PORT = 15986;
|
||||
const COUCHDB_PORT = 5984;
|
||||
const SERVER_HOST = 'localhost';
|
||||
const COUCHDB_USER = 'admin';
|
||||
const COUCHDB_PASSWORD = 'password';
|
||||
const NPM_USERNAME = 'integration';
|
||||
const NPM_PASSWORD = 'suchsecure';
|
||||
const NPM_EMAIL = 'integration@test.com';
|
||||
@@ -15,7 +17,7 @@ const docker = new Docker();
|
||||
let container;
|
||||
|
||||
/**
|
||||
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
|
||||
* Download the `npm-registry-docker` Docker image, create a new container and start it.
|
||||
*/
|
||||
async function start() {
|
||||
await getStream(await docker.pull(IMAGE));
|
||||
@@ -24,10 +26,11 @@ async function start() {
|
||||
Tty: true,
|
||||
Image: IMAGE,
|
||||
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
|
||||
Env: [`COUCHDB_USER=${COUCHDB_USER}`, `COUCHDB_PASSWORD=${COUCHDB_PASSWORD}`],
|
||||
});
|
||||
|
||||
await container.start();
|
||||
await delay(3000);
|
||||
await delay(4000);
|
||||
|
||||
try {
|
||||
// Wait for the registry to be ready
|
||||
@@ -36,14 +39,14 @@ async function start() {
|
||||
minTimeout: 1000,
|
||||
factor: 2,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
|
||||
} catch (error) {
|
||||
throw new Error(`Couldn't start npm-registry-docker after 2 min`);
|
||||
}
|
||||
|
||||
// Create user
|
||||
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
|
||||
json: true,
|
||||
auth: 'admin:admin',
|
||||
auth: `${COUCHDB_USER}:${COUCHDB_PASSWORD}`,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
_id: `org.couchdb.user:${NPM_USERNAME}`,
|
||||
@@ -66,7 +69,7 @@ const authEnv = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop and remote the `npm-docker-couchdb` Docker container.
|
||||
* Stop and remote the `npm-registry-docker` Docker container.
|
||||
*/
|
||||
async function stop() {
|
||||
await container.stop();
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import test from 'ava';
|
||||
import {repeat} from 'lodash';
|
||||
import hideSensitive from '../lib/hide-sensitive';
|
||||
import {SECRET_REPLACEMENT, SECRET_MIN_SIZE} from '../lib/definitions/constants';
|
||||
|
||||
test('Replace multiple sensitive environment variable values', t => {
|
||||
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: 'secret'};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=${env.SOME_TOKEN}`),
|
||||
'https://user:[secure]@host.com?token=[secure]'
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com?token=${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,13 +15,16 @@ test('Replace multiple occurences of sensitive environment variable values', t =
|
||||
const env = {secretKey: 'secret'};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.secretKey}@host.com?token=${env.secretKey}`),
|
||||
'https://user:[secure]@host.com?token=[secure]'
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com?token=${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
test('Escape regexp special characters', t => {
|
||||
const env = {SOME_CREDENTIALS: 'p$^{.+}\\w[a-z]o.*rd'};
|
||||
t.is(hideSensitive(env)(`https://user:${env.SOME_CREDENTIALS}@host.com`), 'https://user:[secure]@host.com');
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.SOME_CREDENTIALS}@host.com`),
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com`
|
||||
);
|
||||
});
|
||||
|
||||
test('Accept "undefined" input', t => {
|
||||
@@ -34,10 +39,20 @@ test('Exclude empty environment variables from the regexp', t => {
|
||||
const env = {SOME_PASSWORD: 'password', SOME_TOKEN: ''};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${env.SOME_PASSWORD}@host.com?token=`),
|
||||
'https://user:[secure]@host.com?token='
|
||||
`https://user:${SECRET_REPLACEMENT}@host.com?token=`
|
||||
);
|
||||
});
|
||||
|
||||
test('Exclude empty environment variables from the regexp if there is only empty ones', t => {
|
||||
t.is(hideSensitive({SOME_PASSWORD: '', SOME_TOKEN: ' \n '})(`https://host.com?token=`), 'https://host.com?token=');
|
||||
});
|
||||
|
||||
test('Exclude environment variables with value shorter than SECRET_MIN_SIZE from the regexp', t => {
|
||||
const SHORT_TOKEN = repeat('a', SECRET_MIN_SIZE - 1);
|
||||
const LONG_TOKEN = repeat('b', SECRET_MIN_SIZE);
|
||||
const env = {SHORT_TOKEN, LONG_TOKEN};
|
||||
t.is(
|
||||
hideSensitive(env)(`https://user:${SHORT_TOKEN}@host.com?token=${LONG_TOKEN}`),
|
||||
`https://user:${SHORT_TOKEN}@host.com?token=${SECRET_REPLACEMENT}`
|
||||
);
|
||||
});
|
||||
|
||||
+277
-66
@@ -1,9 +1,11 @@
|
||||
import test from 'ava';
|
||||
import {escapeRegExp, isString} from 'lodash';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {spy, stub} from 'sinon';
|
||||
import {WritableStreamBuffer} from 'stream-buffers';
|
||||
import AggregateError from 'aggregate-error';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
import {COMMIT_NAME, COMMIT_EMAIL} from '../lib/definitions/constants';
|
||||
import {COMMIT_NAME, COMMIT_EMAIL, SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||
import {
|
||||
gitHead as getGitHead,
|
||||
gitTagHead,
|
||||
@@ -22,9 +24,13 @@ test.beforeEach(t => {
|
||||
// Stub the logger functions
|
||||
t.context.log = spy();
|
||||
t.context.error = spy();
|
||||
t.context.stdout = spy();
|
||||
t.context.stderr = spy();
|
||||
t.context.logger = {log: t.context.log, error: t.context.error, stdout: t.context.stdout, stderr: t.context.stderr};
|
||||
t.context.success = spy();
|
||||
t.context.logger = {
|
||||
log: t.context.log,
|
||||
error: t.context.error,
|
||||
success: t.context.success,
|
||||
scope: () => t.context.logger,
|
||||
};
|
||||
});
|
||||
|
||||
test('Plugins are called with expected values', async t => {
|
||||
@@ -68,10 +74,15 @@ test('Plugins are called with expected values', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, extendEnv: false, env}));
|
||||
const result = await semanticRelease(options, {
|
||||
cwd,
|
||||
env,
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
});
|
||||
|
||||
t.is(verifyConditions1.callCount, 1);
|
||||
t.deepEqual(verifyConditions1.args[0][0], config);
|
||||
@@ -159,6 +170,16 @@ test('Plugins are called with expected values', async t => {
|
||||
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
|
||||
]);
|
||||
|
||||
t.deepEqual(result, {
|
||||
lastRelease,
|
||||
commits: [commits[0]],
|
||||
nextRelease: {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`},
|
||||
releases: [
|
||||
{...release1, ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: '[Function: proxy]'},
|
||||
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||
@@ -193,10 +214,17 @@ test('Use custom tag format', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
|
||||
@@ -237,11 +265,18 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(generateNotes.callCount, 2);
|
||||
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
|
||||
@@ -295,11 +330,13 @@ test('Call all "success" plugins even if one errors out', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options, {cwd, env: {}}));
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
);
|
||||
|
||||
t.is(success1.callCount, 1);
|
||||
t.deepEqual(success1.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]);
|
||||
@@ -327,15 +364,19 @@ test('Log all "verifyConditions" errors', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
t.deepEqual(errors, [error1, error2, error3]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 3', 'ERR3']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR2 error 2']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR3 error 3']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 3], [
|
||||
'An error occurred while running semantic-release: %O',
|
||||
error1,
|
||||
]);
|
||||
@@ -371,14 +412,18 @@ test('Log all "verifyRelease" errors', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
t.deepEqual(errors, [error1, error2]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']);
|
||||
t.is(fail.callCount, 1);
|
||||
t.deepEqual(fail.args[0][0], config);
|
||||
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
|
||||
@@ -419,10 +464,17 @@ test('Dry-run skips publish and success', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
t.is(verifyConditions.callCount, 1);
|
||||
@@ -457,14 +509,18 @@ test('Dry-run skips fail', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
t.deepEqual(errors, [error1, error2]);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
|
||||
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']);
|
||||
t.is(fail.callCount, 0);
|
||||
});
|
||||
|
||||
@@ -504,10 +560,17 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: false, branch: 'master'}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
t.is(verifyConditions.callCount, 1);
|
||||
@@ -518,7 +581,7 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
|
||||
t.is(success.callCount, 0);
|
||||
});
|
||||
|
||||
test.serial('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
|
||||
test('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
@@ -547,12 +610,19 @@ test.serial('Dry-run does not print changelog if "generateNotes" return "undefin
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version %s:\n', '2.0.0']);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version 2.0.0:']);
|
||||
});
|
||||
|
||||
test('Allow local releases with "noCi" option', async t => {
|
||||
@@ -591,10 +661,17 @@ test('Allow local releases with "noCi" option', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: false, branch: 'master', isPr: true}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
|
||||
t.not(
|
||||
@@ -643,10 +720,17 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
|
||||
@@ -665,23 +749,28 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
|
||||
t.is(publish.args[0][1].nextRelease.notes, notes2);
|
||||
});
|
||||
|
||||
test('Returns falsy value if triggered by a PR', async t => {
|
||||
test('Returns false if triggered by a PR', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease({cwd, repositoryUrl}, {cwd, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(
|
||||
{cwd, repositoryUrl},
|
||||
{cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}
|
||||
)
|
||||
);
|
||||
t.is(
|
||||
t.context.log.args[t.context.log.args.length - 1][0],
|
||||
"This run was triggered by a pull request and therefore a new version won't be published."
|
||||
);
|
||||
});
|
||||
|
||||
test('Returns falsy value if triggered on an outdated clone', async t => {
|
||||
test('Returns false if triggered on an outdated clone', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||
const repoDir = cwd;
|
||||
@@ -694,18 +783,22 @@ test('Returns falsy value if triggered on an outdated clone', async t => {
|
||||
await gitPush(repositoryUrl, 'master', {cwd});
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease({repositoryUrl}, {cwd: repoDir, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(
|
||||
{repositoryUrl},
|
||||
{cwd: repoDir, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}
|
||||
)
|
||||
);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
|
||||
"The local branch %s is behind the remote one, therefore a new version won't be published.",
|
||||
'master',
|
||||
"The local branch master is behind the remote one, therefore a new version won't be published.",
|
||||
]);
|
||||
});
|
||||
|
||||
test('Returns falsy value if not running from the configured branch', async t => {
|
||||
test('Returns false if not running from the configured branch', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
const options = {
|
||||
@@ -722,18 +815,25 @@ test('Returns falsy value if not running from the configured branch', async t =>
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
t.is(
|
||||
t.context.log.args[1][0],
|
||||
'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Returns falsy value if there is no relevant changes', async t => {
|
||||
test('Returns false if there is no relevant changes', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
// Add commits to the master branch
|
||||
@@ -759,11 +859,18 @@ test('Returns falsy value if there is no relevant changes', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
t.falsy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.false(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.is(verifyRelease.callCount, 0);
|
||||
t.is(generateNotes.callCount, 0);
|
||||
@@ -807,10 +914,15 @@ test('Exclude commits with [skip release] or [release skip] from analysis', asyn
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
await semanticRelease(options, {cwd, env: {}});
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
});
|
||||
|
||||
t.is(analyzeCommits.callCount, 1);
|
||||
t.is(analyzeCommits.args[0][1].commits.length, 2);
|
||||
@@ -830,15 +942,17 @@ test('Log both plugins errors and errors thrown by "fail" plugin', async t => {
|
||||
fail: [stub().rejects(failError1), stub().rejects(failError2)],
|
||||
};
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options, {cwd, env: {}}));
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
);
|
||||
|
||||
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError1);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2);
|
||||
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s Plugin error', 'ERR']);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][0], 'ERR Plugin error');
|
||||
t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2);
|
||||
});
|
||||
|
||||
test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
||||
@@ -853,11 +967,13 @@ test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
|
||||
fail,
|
||||
};
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
|
||||
await t.throws(semanticRelease(options, {cwd, env: {}}));
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
);
|
||||
|
||||
t.true(fail.notCalled);
|
||||
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
|
||||
@@ -868,10 +984,14 @@ test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found
|
||||
const {cwd} = await gitRepo();
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const errors = [...(await t.throws(semanticRelease({}, {cwd, env: {}})))];
|
||||
const errors = [
|
||||
...(await t.throws(
|
||||
semanticRelease({}, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()})
|
||||
)),
|
||||
];
|
||||
|
||||
// Verify error code and type
|
||||
t.is(errors[0].code, 'ENOREPOURL');
|
||||
@@ -902,13 +1022,97 @@ test('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const error = await t.throws(semanticRelease(options, {cwd, env: {}}), Error);
|
||||
const error = await t.throws(
|
||||
semanticRelease(options, {cwd, env: {}, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}),
|
||||
Error
|
||||
);
|
||||
t.regex(error.details, /string/);
|
||||
});
|
||||
|
||||
test('Hide sensitive information passed to "fail" plugin', async t => {
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
|
||||
const fail = stub().resolves();
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: stub().throws(
|
||||
new SemanticReleaseError(
|
||||
`Message: Exposing token ${env.MY_TOKEN}`,
|
||||
'ERR',
|
||||
`Details: Exposing token ${env.MY_TOKEN}`
|
||||
)
|
||||
),
|
||||
success: stub().resolves(),
|
||||
fail,
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
await t.throws(
|
||||
semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()}),
|
||||
Error
|
||||
);
|
||||
|
||||
const error = fail.args[0][1].errors[0];
|
||||
|
||||
t.is(error.message, `Message: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
t.is(error.details, `Details: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
|
||||
Object.getOwnPropertyNames(error).forEach(prop => {
|
||||
if (isString(error[prop])) {
|
||||
t.notRegex(error[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Hide sensitive information passed to "success" plugin', async t => {
|
||||
const {cwd, repositoryUrl} = await gitRepo(true);
|
||||
await gitCommits(['feat: initial release'], {cwd});
|
||||
await gitTagVersion('v1.0.0', undefined, {cwd});
|
||||
await gitCommits(['feat: new feature'], {cwd});
|
||||
await gitPush(repositoryUrl, 'master', {cwd});
|
||||
|
||||
const success = stub().resolves();
|
||||
const env = {MY_TOKEN: 'secret token'};
|
||||
const options = {
|
||||
branch: 'master',
|
||||
repositoryUrl,
|
||||
verifyConditions: false,
|
||||
verifyRelease: false,
|
||||
prepare: false,
|
||||
publish: stub().resolves({
|
||||
name: `Name: Exposing token ${env.MY_TOKEN}`,
|
||||
url: `URL: Exposing token ${env.MY_TOKEN}`,
|
||||
}),
|
||||
success,
|
||||
fail: stub().resolves(),
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
await semanticRelease(options, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()});
|
||||
|
||||
const release = success.args[0][1].releases[0];
|
||||
|
||||
t.is(release.name, `Name: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
t.is(release.url, `URL: Exposing token ${SECRET_REPLACEMENT}`);
|
||||
|
||||
Object.getOwnPropertyNames(release).forEach(prop => {
|
||||
if (isString(release[prop])) {
|
||||
t.notRegex(release[prop], new RegExp(escapeRegExp(env.MY_TOKEN)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Get all commits including the ones not in the shallow clone', async t => {
|
||||
let {cwd, repositoryUrl} = await gitRepo(true);
|
||||
await gitTagVersion('v1.0.0', undefined, {cwd});
|
||||
@@ -935,10 +1139,17 @@ test('Get all commits including the ones not in the shallow clone', async t => {
|
||||
};
|
||||
|
||||
const semanticRelease = requireNoCache('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'./lib/get-logger': () => t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options, {cwd, env: {}}));
|
||||
t.truthy(
|
||||
await semanticRelease(options, {
|
||||
cwd,
|
||||
env: {},
|
||||
stdout: new WritableStreamBuffer(),
|
||||
stderr: new WritableStreamBuffer(),
|
||||
})
|
||||
);
|
||||
|
||||
t.is(analyzeCommits.args[0][1].commits.length, 3);
|
||||
});
|
||||
|
||||
+13
-10
@@ -4,6 +4,7 @@ import test from 'ava';
|
||||
import {escapeRegExp} from 'lodash';
|
||||
import {writeJson, readJson} from 'fs-extra';
|
||||
import execa from 'execa';
|
||||
import {WritableStreamBuffer} from 'stream-buffers';
|
||||
import {SECRET_REPLACEMENT} from '../lib/definitions/constants';
|
||||
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
|
||||
import gitbox from './helpers/gitbox';
|
||||
@@ -16,11 +17,9 @@ const requireNoCache = proxyquire.noPreserveCache();
|
||||
|
||||
// Environment variables used with semantic-release cli (similar to what a user would setup)
|
||||
const env = {
|
||||
...npmRegistry.authEnv,
|
||||
GH_TOKEN: gitbox.gitCredential,
|
||||
GITHUB_URL: mockServer.url,
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
NPM_USERNAME: 'integration',
|
||||
NPM_PASSWORD: 'suchsecure',
|
||||
TRAVIS: 'true',
|
||||
CI: 'true',
|
||||
TRAVIS_BRANCH: 'master',
|
||||
@@ -29,9 +28,9 @@ const env = {
|
||||
// Environment variables used only for the local npm command used to do verification
|
||||
const testEnv = {
|
||||
...process.env,
|
||||
...npmRegistry.authEnv,
|
||||
npm_config_registry: npmRegistry.url,
|
||||
NPM_EMAIL: 'integration@test.com',
|
||||
LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
|
||||
LEGACY_TOKEN: Buffer.from(`${env.NPM_USERNAME}:${env.NPM_PASSWORD}`, 'utf8').toString('base64'),
|
||||
};
|
||||
|
||||
const cli = require.resolve('../bin/semantic-release');
|
||||
@@ -467,6 +466,10 @@ test('Run via JS API', async t => {
|
||||
version: '0.0.0-dev',
|
||||
repository: {url: repositoryUrl},
|
||||
publishConfig: {registry: npmRegistry.url},
|
||||
release: {
|
||||
fail: false,
|
||||
success: false,
|
||||
},
|
||||
});
|
||||
|
||||
/* Initial release */
|
||||
@@ -488,7 +491,7 @@ test('Run via JS API', async t => {
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit'], {cwd});
|
||||
t.log('$ Call semantic-release via API');
|
||||
await semanticRelease({fail: false, success: false}, {cwd, env});
|
||||
await semanticRelease(undefined, {cwd, env, stdout: new WritableStreamBuffer(), stderr: new WritableStreamBuffer()});
|
||||
|
||||
// Verify package.json and has been updated
|
||||
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
|
||||
@@ -552,9 +555,9 @@ test('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit'], {cwd});
|
||||
t.log('$ semantic-release');
|
||||
const {stdout, code} = await execa(cli, [], {env, cwd, reject: false});
|
||||
const {stderr, code} = await execa(cli, [], {env, cwd, reject: false});
|
||||
// Verify the type and message are logged
|
||||
t.regex(stdout, /EINHERITED Inherited error/);
|
||||
t.regex(stderr, /EINHERITED Inherited error/);
|
||||
t.is(code, 1);
|
||||
});
|
||||
|
||||
@@ -570,13 +573,13 @@ test('Exit with 1 if missing permission to push to the remote repository', async
|
||||
await gitCommits(['feat: Initial commit'], {cwd});
|
||||
await gitPush('origin', 'master', {cwd});
|
||||
t.log('$ semantic-release');
|
||||
const {stdout, code} = await execa(
|
||||
const {stderr, code} = await execa(
|
||||
cli,
|
||||
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
|
||||
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false}
|
||||
);
|
||||
// Verify the type and message are logged
|
||||
t.regex(stdout, /EGITNOPERMISSION/);
|
||||
t.regex(stderr, /EGITNOPERMISSION/);
|
||||
t.is(code, 1);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import test from 'ava';
|
||||
import {stub} from 'sinon';
|
||||
import logger from '../lib/logger';
|
||||
|
||||
test.beforeEach(t => {
|
||||
t.context.log = stub(console, 'log');
|
||||
t.context.error = stub(console, 'error');
|
||||
});
|
||||
|
||||
test.afterEach.always(t => {
|
||||
t.context.log.restore();
|
||||
t.context.error.restore();
|
||||
});
|
||||
|
||||
test.serial('Basic log', t => {
|
||||
logger.log('test log');
|
||||
logger.error('test error');
|
||||
|
||||
t.regex(t.context.log.args[0][0], /.*test log/);
|
||||
t.regex(t.context.error.args[0][0], /.*test error/);
|
||||
});
|
||||
|
||||
test.serial('Log object', t => {
|
||||
const obj = {a: 1, b: '2'};
|
||||
logger.log(obj);
|
||||
logger.error(obj);
|
||||
|
||||
t.is(t.context.log.args[0][1], obj);
|
||||
t.is(t.context.error.args[0][1], obj);
|
||||
});
|
||||
|
||||
test.serial('Log with string formatting', t => {
|
||||
logger.log('test log %s', 'log value');
|
||||
logger.error('test error %s', 'error value');
|
||||
|
||||
t.regex(t.context.log.args[0][0], /.*test log/);
|
||||
t.regex(t.context.error.args[0][0], /.*test error/);
|
||||
t.is(t.context.log.args[0][1], 'log value');
|
||||
t.is(t.context.error.args[0][1], 'error value');
|
||||
});
|
||||
|
||||
test.serial('Log with error stacktrace and properties', t => {
|
||||
const error = new Error('error message');
|
||||
logger.error(error);
|
||||
const otherError = new Error('other error message');
|
||||
logger.error('test error %O', otherError);
|
||||
|
||||
t.is(t.context.error.args[0][1], error);
|
||||
t.regex(t.context.error.args[1][0], /.*test error/);
|
||||
t.is(t.context.error.args[1][1], otherError);
|
||||
});
|
||||
@@ -8,7 +8,15 @@ const cwd = process.cwd();
|
||||
test.beforeEach(t => {
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.logger = {log: t.context.log};
|
||||
t.context.error = stub();
|
||||
t.context.success = stub();
|
||||
t.context.stderr = {write: stub()};
|
||||
t.context.logger = {
|
||||
log: t.context.log,
|
||||
error: t.context.error,
|
||||
success: t.context.success,
|
||||
scope: () => t.context.logger,
|
||||
};
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from string', t => {
|
||||
@@ -21,7 +29,7 @@ test('Normalize and load plugin from string', t => {
|
||||
|
||||
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
|
||||
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']);
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from object', t => {
|
||||
@@ -34,7 +42,7 @@ test('Normalize and load plugin from object', t => {
|
||||
|
||||
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'publish', './test/fixtures/plugin-noop']);
|
||||
t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop"']);
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from a base file path', t => {
|
||||
@@ -44,11 +52,8 @@ test('Normalize and load plugin from a base file path', t => {
|
||||
|
||||
t.is(plugin.pluginName, './plugin-noop');
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], [
|
||||
'Load plugin "%s" from %s in shareable config %s',
|
||||
'verifyConditions',
|
||||
'./plugin-noop',
|
||||
'./test/fixtures',
|
||||
t.deepEqual(t.context.success.args[0], [
|
||||
'Loaded plugin "verifyConditions" from "./plugin-noop" in shareable config "./test/fixtures"',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -90,12 +95,17 @@ test('Normalize and load plugin that retuns multiple functions', t => {
|
||||
);
|
||||
|
||||
t.is(typeof plugin, 'function');
|
||||
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
|
||||
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/multi-plugin"']);
|
||||
});
|
||||
|
||||
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
|
||||
const analyzeCommits = stub().resolves(2);
|
||||
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'analyzeCommits', analyzeCommits, {});
|
||||
const plugin = normalize(
|
||||
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
|
||||
'analyzeCommits',
|
||||
analyzeCommits,
|
||||
{}
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
@@ -108,7 +118,12 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the
|
||||
|
||||
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
|
||||
const generateNotes = stub().resolves(2);
|
||||
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'generateNotes', generateNotes, {});
|
||||
const plugin = normalize(
|
||||
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
|
||||
'generateNotes',
|
||||
generateNotes,
|
||||
{}
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
@@ -120,11 +135,15 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the
|
||||
});
|
||||
|
||||
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
|
||||
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'publish', './plugin-identity', {
|
||||
'./plugin-identity': './test/fixtures',
|
||||
});
|
||||
const publish = stub().resolves(2);
|
||||
const plugin = normalize(
|
||||
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
|
||||
'publish',
|
||||
publish,
|
||||
{}
|
||||
);
|
||||
|
||||
const error = await t.throws(plugin(2));
|
||||
const error = await t.throws(plugin());
|
||||
|
||||
t.is(error.code, 'EPUBLISHOUTPUT');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
@@ -138,9 +157,14 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi
|
||||
const pluginConf = {path: pluginFunction, conf: 'confValue'};
|
||||
const options = {global: 'globalValue'};
|
||||
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
|
||||
await plugin('param');
|
||||
await plugin({param: 'param'});
|
||||
|
||||
t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
|
||||
t.true(
|
||||
pluginFunction.calledWithMatch(
|
||||
{conf: 'confValue', global: 'globalValue'},
|
||||
{param: 'param', logger: t.context.logger}
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('Prevent plugins to modify "pluginConfig"', async t => {
|
||||
|
||||
@@ -11,7 +11,8 @@ const cwd = process.cwd();
|
||||
test.beforeEach(t => {
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.logger = {log: t.context.log};
|
||||
t.context.success = stub();
|
||||
t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger};
|
||||
});
|
||||
|
||||
test('Export default plugins', t => {
|
||||
@@ -158,18 +159,15 @@ test('Merge global options with plugin options', async t => {
|
||||
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
|
||||
});
|
||||
|
||||
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
|
||||
const errors = [...t.throws(() => getPlugins({cwd, logger: t.context.logger, options: {verifyConditions: {}}}, {}))];
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
});
|
||||
|
||||
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
|
||||
test('Throw an error if plugins configuration are invalid', t => {
|
||||
const errors = [
|
||||
...t.throws(() =>
|
||||
getPlugins(
|
||||
{cwd, logger: t.context.logger, options: {verifyConditions: [{path: '@semantic-release/npm'}, {}]}},
|
||||
{
|
||||
cwd,
|
||||
logger: t.context.logger,
|
||||
options: {verifyConditions: {}, analyzeCommits: [], verifyRelease: [{}], generateNotes: [{path: null}]},
|
||||
},
|
||||
{}
|
||||
)
|
||||
),
|
||||
@@ -177,4 +175,10 @@ test('Throw an error if an array of plugin configuration is missing a path for p
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINCONF');
|
||||
t.is(errors[2].name, 'SemanticReleaseError');
|
||||
t.is(errors[2].code, 'EPLUGINCONF');
|
||||
t.is(errors[3].name, 'SemanticReleaseError');
|
||||
t.is(errors[3].code, 'EPLUGINCONF');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import test from 'ava';
|
||||
import {validateConfig} from '../../lib/plugins/utils';
|
||||
|
||||
test('Validate multiple/optional plugin configuration', t => {
|
||||
const type = {multiple: true, required: false};
|
||||
t.false(validateConfig(type, {}));
|
||||
t.false(validateConfig(type, {path: null}));
|
||||
|
||||
t.true(validateConfig(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateConfig(type));
|
||||
t.true(validateConfig(type, 'plugin-path.js'));
|
||||
t.true(validateConfig(type, ['plugin-path.js']));
|
||||
t.true(validateConfig(type, () => {}));
|
||||
t.true(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('Validate multiple/required plugin configuration', t => {
|
||||
const type = {multiple: true, required: true};
|
||||
t.false(validateConfig(type, {}));
|
||||
t.false(validateConfig(type, {path: null}));
|
||||
t.false(validateConfig(type));
|
||||
|
||||
t.true(validateConfig(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateConfig(type, 'plugin-path.js'));
|
||||
t.true(validateConfig(type, ['plugin-path.js']));
|
||||
t.true(validateConfig(type, () => {}));
|
||||
t.true(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
});
|
||||
|
||||
test('Validate single/required plugin configuration', t => {
|
||||
const type = {multiple: false, required: true};
|
||||
|
||||
t.false(validateConfig(type, {}));
|
||||
t.false(validateConfig(type, {path: null}));
|
||||
t.false(validateConfig(type, []));
|
||||
t.false(validateConfig(type));
|
||||
t.false(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
|
||||
t.true(validateConfig(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateConfig(type, 'plugin-path.js'));
|
||||
t.true(validateConfig(type, ['plugin-path.js']));
|
||||
t.true(validateConfig(type, () => {}));
|
||||
});
|
||||
|
||||
test('Validate single/optional plugin configuration', t => {
|
||||
const type = {multiple: false, required: false};
|
||||
|
||||
t.false(validateConfig(type, {}));
|
||||
t.false(validateConfig(type, {path: null}));
|
||||
t.false(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
|
||||
|
||||
t.true(validateConfig(type));
|
||||
t.true(validateConfig(type, []));
|
||||
t.true(validateConfig(type, {path: 'plugin-path.js'}));
|
||||
t.true(validateConfig(type, 'plugin-path.js'));
|
||||
t.true(validateConfig(type, ['plugin-path.js']));
|
||||
t.true(validateConfig(type, () => {}));
|
||||
});
|
||||
Reference in New Issue
Block a user