diff --git a/.travis.yml b/.travis.yml index bab1186d..7a54295c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ version: ~> 1.0 import: - semantic-release/semantic-release:.travis/node.yml - - semantic-release/semantic-release:.travis/node-versions.yml + - semantic-release/semantic-release:.travis/node-versions.yml@beta - semantic-release/semantic-release:.travis/semantic-release.yml - semantic-release/semantic-release:.travis/greenkeeper.yml - semantic-release/semantic-release:.travis/codecov.yml diff --git a/README.md b/README.md index e9d79544..0c66424d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ [![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) +[![npm beta version](https://img.shields.io/npm/v/@semantic-release/exec/beta.svg)](https://www.npmjs.com/package/@semantic-release/exec) | Step | Description | |--------------------|---------------------------------------------------------------------------------------------------------| @@ -60,6 +61,7 @@ With this example: | `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). | +| `addChannelCmd` | The shell command to execute during the add channel step. See [addChannelCmd](#addchannelcmd). | | `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). | @@ -110,6 +112,14 @@ Execute a shell command to verify if the release should happen. | `stdout` | Can be used for logging. | | `stderr` | Can be used for logging. | +## addChannelCmd + +| 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. | + ## publishCmd | Command property | Description | diff --git a/index.js b/index.js index fbd1f505..d5a88e6b 100644 --- a/index.js +++ b/index.js @@ -79,6 +79,27 @@ async function publish(pluginConfig, context) { return false; } +async function addChannel(pluginConfig, context) { + if (!isNil(pluginConfig.addChannelCmd) || !isNil(pluginConfig.cmd)) { + verifyConfig('addChannelCmd', pluginConfig); + + const stdout = await exec('addChannelCmd', pluginConfig, context); + + try { + return stdout ? parseJson(stdout) : undefined; + } catch (error) { + debug(stdout); + debug(error); + + debug(`The command ${pluginConfig.cmd} wrote invalid JSON to stdout. The stdout content will be ignored.`); + + return undefined; + } + } + + return false; +} + async function success(pluginConfig, context) { if (!isNil(pluginConfig.successCmd) || !isNil(pluginConfig.cmd)) { verifyConfig('successCmd', pluginConfig); @@ -95,4 +116,14 @@ async function fail(pluginConfig, context) { } } -module.exports = {verifyConditions, analyzeCommits, verifyRelease, generateNotes, prepare, publish, success, fail}; +module.exports = { + verifyConditions, + analyzeCommits, + verifyRelease, + generateNotes, + prepare, + publish, + addChannel, + success, + fail, +}; diff --git a/package.json b/package.json index e442b9f1..2aae1286 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "@semantic-release/error": "^2.1.0", "aggregate-error": "^3.0.0", "debug": "^4.0.0", - "execa": "^3.2.0", + "execa": "^4.0.0", "lodash": "^4.17.4", "parse-json": "^5.0.0" }, @@ -22,13 +22,13 @@ "ava": "^2.0.0", "codecov": "^3.0.0", "nyc": "^15.0.0", - "semantic-release": "^15.0.0", + "semantic-release": "^16.0.0-beta", "sinon": "^8.0.0", "stream-buffers": "^3.0.2", "xo": "^0.25.0" }, "engines": { - "node": ">=8.16" + "node": ">=10.13" }, "files": [ "lib", @@ -59,15 +59,14 @@ "all": true }, "peerDependencies": { - "semantic-release": ">=15.9.0 <16.0.0" + "semantic-release": ">=16.0.0-beta <17.0.0" }, "prettier": { "printWidth": 120, "trailingComma": "es5" }, "publishConfig": { - "access": "public", - "tag": "next" + "access": "public" }, "repository": { "type": "git", diff --git a/test/add-channel.test.js b/test/add-channel.test.js new file mode 100644 index 00000000..a73c6d20 --- /dev/null +++ b/test/add-channel.test.js @@ -0,0 +1,86 @@ +import test from 'ava'; +import {stub} from 'sinon'; +import {WritableStreamBuffer} from 'stream-buffers'; +import {addChannel} from '..'; + +test.beforeEach(t => { + t.context.stdout = new WritableStreamBuffer(); + t.context.stderr = new WritableStreamBuffer(); + // Mock logger + t.context.log = stub(); + t.context.error = stub(); + t.context.logger = {log: t.context.log, error: t.context.error}; +}); + +test('Parse JSON returned by addChannel script', async t => { + const pluginConfig = { + addChannelCmd: + './test/fixtures/echo-args.sh {\\"name\\": \\"Release name\\", \\"url\\": \\"https://host.com/release/1.0.0\\"}', + }; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.deepEqual(result, {name: 'Release name', url: 'https://host.com/release/1.0.0'}); +}); + +test('Return "undefined" if the addChannel script wrtite invalid JSON to stdout (with "publishCmd")', async t => { + const pluginConfig = {addChannelCmd: './test/fixtures/echo-args.sh invalid_json'}; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.is(result, undefined); +}); + +test('Return "undefined" if the addChannel script wrtite invalid JSON to stdout (with "cmd")', async t => { + const pluginConfig = {cmd: './test/fixtures/echo-args.sh invalid_json'}; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.is(result, undefined); +}); + +test('Return "undefined" if the addChannel script wrtite nothing to stdout', async t => { + const pluginConfig = {addChannelCmd: './test/fixtures/echo-args.sh'}; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.is(result, undefined); +}); + +test('Throw "Error" if the addChannel script does not returns 0', async t => { + const pluginConfig = {addChannelCmd: 'exit 1'}; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger, options: {}}; + + await t.throwsAsync(addChannel(pluginConfig, context), Error); +}); + +test('Use "cmd" if defined and "addChannelCmd" is not', async t => { + const pluginConfig = { + cmd: + './test/fixtures/echo-args.sh {\\"name\\": \\"Release name\\", \\"url\\": \\"https://host.com/release/1.0.0\\"}', + }; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.deepEqual(result, {name: 'Release name', url: 'https://host.com/release/1.0.0'}); +}); + +test('Use "addChannelCmd" even if "cmd" is defined', async t => { + const pluginConfig = { + addChannelCmd: + './test/fixtures/echo-args.sh {\\"name\\": \\"Release name\\", \\"url\\": \\"https://host.com/release/1.0.0\\"}', + cmd: 'exit 1', + }; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.deepEqual(result, {name: 'Release name', url: 'https://host.com/release/1.0.0'}); +}); + +test('Return "false" if neither "addChannelCmd" nor "cmd" is defined', async t => { + const pluginConfig = {}; + const context = {stdout: t.context.stdout, stderr: t.context.stderr, logger: t.context.logger}; + + const result = await addChannel(pluginConfig, context); + t.is(result, false); +}); diff --git a/test/integration.test.js b/test/integration.test.js index f5c92b0a..436613a8 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -8,6 +8,7 @@ test('Skip step if neither "cmd" nor step cmd is defined', async t => { await t.notThrowsAsync(m.generateNotes({}, {})); await t.notThrowsAsync(m.prepare({}, {})); await t.notThrowsAsync(m.publish({}, {})); + await t.notThrowsAsync(m.addChannel({}, {})); await t.notThrowsAsync(m.success({}, {})); await t.notThrowsAsync(m.fail({}, {})); });