Skip to content

Commit

Permalink
feat: add dedicated command option for each step
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg committed Oct 8, 2018
1 parent 38baf57 commit 1b66a36
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 380 deletions.
146 changes: 65 additions & 81 deletions README.md
@@ -1,6 +1,6 @@
# @semantic-release/exec

Set of [semantic-release](https://github.com/semantic-release/semantic-release) plugins to execute custom shell commands.
[**semantic-release**](https://github.com/semantic-release/semantic-release) plugin to execute custom shell commands.

[![Travis](https://img.shields.io/travis/semantic-release/exec.svg)](https://travis-ci.org/semantic-release/exec)
[![Codecov](https://img.shields.io/codecov/c/github/semantic-release/exec.svg)](https://codecov.io/gh/semantic-release/exec)
Expand All @@ -9,7 +9,63 @@ Set of [semantic-release](https://github.com/semantic-release/semantic-release)
[![npm latest version](https://img.shields.io/npm/v/@semantic-release/exec/latest.svg)](https://www.npmjs.com/package/@semantic-release/exec)
[![npm next version](https://img.shields.io/npm/v/@semantic-release/exec/next.svg)](https://www.npmjs.com/package/@semantic-release/exec)

## verifyConditions
| Step | Description |
|--------------------|---------------------------------------------------------------------------------------------------------|
| `verifyConditions` | Execute a shell command to verify if the release should happen. |
| `analyzeCommits` | Execute a shell command to determine the type of release. |
| `verifyRelease` | Execute a shell command to verifying a release that was determined before and is about to be published. |
| `generateNotes` | Execute a shell command to generate the release note. |
| `prepare` | Execute a shell command to prepare the release. |
| `publish` | Execute a shell command to publish the release. |
| `success` | Execute a shell command to notify of a new release. |
| `fail` | Execute a shell command to notify of a failed release. |

## Install

```bash
$ npm install @semantic-release/exec -D
```

## Usage

The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/caribou/docs/usage/configuration.md#configuration):

```json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/exec", {
"verifyConditionsCmd": "./verify.sh",
"publishCmd": "./publish.sh ${nextRelease.version} ${options.branch} ${commits.length} ${Date.now()}"
}],
]
}
```

With this example:
- the shell command `./verify.sh` will be executed on the [verify conditions step](https://github.com/semantic-release/semantic-release#release-steps)
- the shell command `./publish.sh 1.0.0 master 3 870668040000` (for the release of version `1.0.0` from branch `master` with `3` commits on `August 4th, 1997 at 2:14 AM`) will be executed on the [publish step](https://github.com/semantic-release/semantic-release#release-steps)

## Configuration

### Options

| Options | Description |
|-----------------------|-----------------------------------------------------------------------------------------------------------------|
| `verifyConditionsCmd` | The shell command to execute during the verify condition step. See [verifyConditionsCmd](#verifyconditionscmd). |
| `analyzeCommitsCmd` | The shell command to execute during the analyze commits step. See [analyzeCommitsCmd](#analyzecommitscmd). |
| `verifyReleaseCmd` | The shell command to execute during the verify release step. See [verifyReleaseCmd](#verifyreleasecmd). |
| `generateNotesCmd` | The shell command to execute during the generate notes step. See [generateNotesCmd](#generatenotescmd). |
| `prepareCmd` | The shell command to execute during the prepare step. See [prepareCmd](#preparecmd). |
| `publishCmd` | The shell command to execute during the publish step. See [publishCmd](#publishcmd). |
| `successCmd` | The shell command to execute during the success step. See [successCmd](#successcmd). |
| `failCmd` | The shell command to execute during the fail step. See [failCmd](#failcmd). |
| `shell` | The shell to use to run the command. See [execa#shell](https://github.com/sindresorhus/execa#shell). |

Each shell command is generated with [Lodash template](https://lodash.com/docs#template). All the objets passed to the [semantic-release plugins](https://github.com/semantic-release/semantic-release#plugins) are available as template options.

## verifyConditionsCmd

Execute a shell command to verify if the release should happen.

Expand All @@ -19,130 +75,58 @@ Execute a shell command to verify if the release should happen.
| `stdout` | Write only the reason for the verification to fail. |
| `stderr` | Can be used for logging. |

## analyzeCommits

Execute a shell command to determine the type release.
## analyzeCommitsCmd

| Command property | Description |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `exit code` | Any non `0` code is considered as an unexpected error and will stop the `semantic-release` execution with an error. |
| `stdout` | Only the release type (`major`, `minor` or `patch` etc..) can be written to `stdout`. If no release has to be done the command must not write to `stdout`. |
| `stderr` | Can be used for logging. |

## verifyRelease

Execute a shell command to verifying a release that was determined before and is about to be published.
## verifyReleaseCmd

| Command property | Description |
|------------------|--------------------------------------------------------------------------|
| `exit code` | `0` if the verification is successful, or any other exit code otherwise. |
| `stdout` | Only the reason for the verification to fail can be written to `stdout`. |
| `stderr` | Can be used for logging. |

## generateNotes

Execute a shell command to generate the release note.
## generateNotesCmd

| Command property | Description |
|------------------|---------------------------------------------------------------------------------------------------------------------|
| `exit code` | Any non `0` code is considered as an unexpected error and will stop the `semantic-release` execution with an error. |
| `stdout` | Only the release note must be written to `stdout`. |
| `stderr` | Can be used for logging. |

## prepare

Execute a shell command to prepare the release.
## prepareCmd

| Command property | Description |
|------------------|---------------------------------------------------------------------------------------------------------------------|
| `exit code` | Any non `0` code is considered as an unexpected error and will stop the `semantic-release` execution with an error. |
| `stdout` | Can be used for logging. |
| `stderr` | Can be used for logging. |

## publish

Execute a shell command to publish the release.
## publishCmd

| Command property | Description |
|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `exit code` | Any non `0` code is considered as an unexpected error and will stop the `semantic-release` execution with an error. |
| `stdout` | The `release` information can be written to `stdout` as parseable JSON (for example `{"name": "Release name", "url": "http://url/release/1.0.0"}`). If the command write non parseable JSON to `stdout` no `release` information will be returned. |
| `stderr` | Can be used for logging. |

## success

Execute a shell command to notify of a successful release.
## successCmd

| Command property | Description |
|------------------|---------------------------------------------------------------------------------------------------------------------|
| `exit code` | Any non `0` code is considered as an unexpected error and will stop the `semantic-release` execution with an error. |
| `stdout` | Can be used for logging. |
| `stderr` | Can be used for logging. |

## fail

Execute a shell command to notify of a failed release.
## failCmd

| Command property | Description |
|------------------|---------------------------------------------------------------------------------------------------------------------|
| `exit code` | Any non `0` code is considered as an unexpected error and will stop the `semantic-release` execution with an error. |
| `stdout` | Can be used for logging. |
| `stderr` | Can be used for logging. |

## Configuration

### Options

| Options | Description |
|---------|------------------------------------------------------------------------------------------------------|
| `cmd` | The shell command to execute. See [cmd](#cmd). |
| `shell` | The shell to use to run the command. See [execa#shell](https://github.com/sindresorhus/execa#shell). |

#### `cmd`

The shell command is generated with [Lodash template](https://lodash.com/docs#template). All the objets passed to the [semantic-release plugins](https://github.com/semantic-release/semantic-release#plugins) are available as template options.

##### `cmd` examples

```json
{
"release": {
"publish": [
{
"path": "@semantic-release/exec",
"cmd": "./publish.sh ${nextRelease.version} ${options.branch} ${commits.length} ${Date.now()}",
},
"@semantic-release/npm",
"@semantic-release/github"
]
}
}
```

This will execute the shell command `./publish.sh 1.0.0 master 3 870668040000` (for the release of version `1.0.0` from branch `master` with `3` commits on `August 4th, 1997 at 2:14 AM`).

### Usage

Options can be set within the plugin definition in the `semantic-release` configuration file:

```json
{
"release": {
"verifyConditions": [
"@semantic-release/npm",
{
"path": "@semantic-release/exec",
"cmd": "./verify.sh",
}
],
"publish": [
"@semantic-release/npm",
{
"path": "@semantic-release/exec",
"cmd": "./publish.sh ${nextRelease.version}",
},
"@semantic-release/github"
]
}
}
```
92 changes: 55 additions & 37 deletions index.js
@@ -1,75 +1,93 @@
const {isNil} = require('lodash');
const parseJson = require('parse-json');
const debug = require('debug')('semantic-release:exec');
const SemanticReleaseError = require('@semantic-release/error');
const execScript = require('./lib/exec-script');
const exec = require('./lib/exec');
const verifyConfig = require('./lib/verify-config');

async function verifyConditions(pluginConfig, context) {
verifyConfig(pluginConfig);

try {
await execScript(pluginConfig, context);
} catch (error) {
throw new SemanticReleaseError(error.stdout, 'EVERIFYCONDITIONS');
if (!isNil(pluginConfig.verifyConditionsCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('verifyConditionsCmd', pluginConfig);

try {
await exec('verifyConditionsCmd', pluginConfig, context);
} catch (error) {
throw new SemanticReleaseError(error.stdout, 'EVERIFYCONDITIONS');
}
}
}

async function analyzeCommits(pluginConfig, context) {
verifyConfig(pluginConfig);
if (!isNil(pluginConfig.analyzeCommitsCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('analyzeCommitsCmd', pluginConfig);

const stdout = await execScript(pluginConfig, context);
return stdout || undefined;
const stdout = await exec('analyzeCommitsCmd', pluginConfig, context);
return stdout || undefined;
}
}

async function verifyRelease(pluginConfig, context) {
verifyConfig(pluginConfig);

try {
await execScript(pluginConfig, context);
} catch (error) {
throw new SemanticReleaseError(error.stdout, 'EVERIFYRELEASE');
if (!isNil(pluginConfig.verifyReleaseCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('verifyReleaseCmd', pluginConfig);

try {
await exec('verifyReleaseCmd', pluginConfig, context);
} catch (error) {
throw new SemanticReleaseError(error.stdout, 'EVERIFYRELEASE');
}
}
}

async function generateNotes(pluginConfig, context) {
verifyConfig(pluginConfig);
if (!isNil(pluginConfig.generateNotesCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('generateNotesCmd', pluginConfig);

const stdout = await execScript(pluginConfig, context);
return stdout;
const stdout = await exec('generateNotesCmd', pluginConfig, context);
return stdout;
}
}

async function prepare(pluginConfig, context) {
verifyConfig(pluginConfig);
if (!isNil(pluginConfig.prepareCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('prepareCmd', pluginConfig);

await execScript(pluginConfig, context);
await exec('prepareCmd', pluginConfig, context);
}
}

async function publish(pluginConfig, context) {
verifyConfig(pluginConfig);

const stdout = await execScript(pluginConfig, context);

try {
return stdout ? parseJson(stdout) : undefined;
} catch (error) {
debug(stdout);
debug(error);
context.logger.log(
`The command ${pluginConfig.cmd} wrote invalid JSON to stdout. The stdout content will be ignored.`
);
if (!isNil(pluginConfig.publishCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('publishCmd', pluginConfig);

const stdout = await exec('publishCmd', pluginConfig, context);

try {
return stdout ? parseJson(stdout) : undefined;
} catch (error) {
debug(stdout);
debug(error);
context.logger.log(
`The command ${pluginConfig.publishCmd ||
pluginConfig.cmd} wrote invalid JSON to stdout. The stdout content will be ignored.`
);
}
}
}

async function success(pluginConfig, context) {
verifyConfig(pluginConfig);
if (!isNil(pluginConfig.successCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('successCmd', pluginConfig);

await execScript(pluginConfig, context);
await exec('successCmd', pluginConfig, context);
}
}

async function fail(pluginConfig, context) {
verifyConfig(pluginConfig);
if (!isNil(pluginConfig.failCmd) || !isNil(pluginConfig.cmd)) {
verifyConfig('failCmd', pluginConfig);

await execScript(pluginConfig, context);
await exec('failCmd', pluginConfig, context);
}
}

module.exports = {verifyConditions, analyzeCommits, verifyRelease, generateNotes, prepare, publish, success, fail};
27 changes: 27 additions & 0 deletions lib/definitions/errors.js
@@ -0,0 +1,27 @@
const url = require('url');
const {inspect} = require('util');
const {isString} = require('lodash');
const pkg = require('../../package.json');

const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
const linkify = file => `${homepage}/blob/master/${file}`;

module.exports = {
EINVALIDCMD: ({cmd, cmdProp}) => ({
message: `Invalid \`${cmdProp}\` option.`,
details: `The [\`${cmdProp}\` option](${linkify(
`README.md#${cmdProp}`
)}) is required and must be a non empty \`String\`.
Your configuration for the \`${cmdProp}\` option is \`${stringify(cmd)}\`.`,
}),
EINVALIDSHELL: ({shell}) => ({
message: 'Invalid `shell` option.',
details: `The [\`shell\` option](${linkify(
'README.md#options'
)}) if defined, must be a non empty \`String\` or the value \`true\`.
Your configuration for the \`shell\` option is \`${stringify(shell)}\`.`,
}),
};
5 changes: 3 additions & 2 deletions lib/exec-script.js → lib/exec.js
@@ -1,8 +1,9 @@
const {template} = require('lodash');
const execa = require('execa');

module.exports = async ({cmd, shell, ...config}, {cwd, env, stdout, stderr, logger, ...context}) => {
const script = template(cmd)({config, ...context});
module.exports = async (cmdProp, {shell, ...config}, {cwd, env, stdout, stderr, logger, ...context}) => {
const cmd = config[cmdProp] ? cmdProp : 'cmd';
const script = template(config[cmd])({config, ...context});

logger.log('Call script %s', script);

Expand Down
7 changes: 7 additions & 0 deletions lib/get-error.js
@@ -0,0 +1,7 @@
const SemanticReleaseError = require('@semantic-release/error');
const ERROR_DEFINITIONS = require('./definitions/errors');

module.exports = (code, ctx) => {
const {message, details} = ERROR_DEFINITIONS[code](ctx);
return new SemanticReleaseError(message, code, details);
};

0 comments on commit 1b66a36

Please sign in to comment.