diff --git a/lib/fail.js b/lib/fail.js index 182f683..67b3d33 100644 --- a/lib/fail.js +++ b/lib/fail.js @@ -3,12 +3,15 @@ const postMessage = require('./postMessage') const template = require('./template') module.exports = async (pluginConfig, context) => { - const { logger, options, errors } = context - const { npm_package_name } = context.env + const { + logger, + options, + errors, + env: { SEMANTIC_RELEASE_PACKAGE, npm_package_name } + } = context const { slackWebhook = process.env.SLACK_WEBHOOK } = pluginConfig - let package_name = context.env.SEMANTIC_RELEASE_PACKAGE - if (!package_name) package_name = context.env.npm_package_name + const package_name = SEMANTIC_RELEASE_PACKAGE || npm_package_name if (!pluginConfig.notifyOnFail) { logger.log('Notifying on fail skipped') @@ -66,8 +69,6 @@ module.exports = async (pluginConfig, context) => { messageBlocks.push(metadata) } - // messageBlocks.push(divider) - const attachments = [ { type: 'section', diff --git a/lib/postMessage.js b/lib/postMessage.js index 380698c..82c23b9 100644 --- a/lib/postMessage.js +++ b/lib/postMessage.js @@ -1,30 +1,24 @@ const fetch = require('node-fetch') const SemanticReleaseError = require('@semantic-release/error') -async function postMessage(message, logger, slackWebhook) { - await fetch(slackWebhook, { - method: 'post', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(message) - }) - .then(res => res.text()) - .then(text => { - if (text !== 'ok') { - logger.log('JSON message format invalid: ' + text) - throw new SemanticReleaseError( - new Error().stdout, - 'INVALID SLACK COMMAND: ' + text - ) - } +module.exports = async (message, logger, slackWebhook) => { + let response + let bodyText + try { + response = await fetch(slackWebhook, { + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(message) }) - .catch(e => { - throw new SemanticReleaseError( - e.stdout, - 'SLACK CONNECTION FAILED: ' - ) - }) -} + bodyText = await response.text() + } catch (e) { + throw new SemanticReleaseError(e.message, 'SLACK CONNECTION FAILED') + } -module.exports = postMessage + if (!response.ok || bodyText !== 'ok') { + logger.log('JSON message format invalid: ' + bodyText) + throw new SemanticReleaseError(bodyText, 'INVALID SLACK COMMAND') + } +} diff --git a/lib/success.js b/lib/success.js index 01a44c8..e24e458 100644 --- a/lib/success.js +++ b/lib/success.js @@ -8,11 +8,15 @@ const truncate = require('./truncate') const MAX_LENGTH = 2900 module.exports = async (pluginConfig, context) => { - const { logger, nextRelease, options } = context + const { + logger, + nextRelease, + options, + env: { SEMANTIC_RELEASE_PACKAGE, npm_package_name } + } = context const { slackWebhook = process.env.SLACK_WEBHOOK } = pluginConfig - let package_name = context.env.SEMANTIC_RELEASE_PACKAGE - if (!package_name) package_name = context.env.npm_package_name + const package_name = SEMANTIC_RELEASE_PACKAGE || npm_package_name if (!pluginConfig.notifyOnSuccess) { logger.log('Notifying on success skipped') diff --git a/lib/template.js b/lib/template.js index fc06f3f..2cb3a17 100644 --- a/lib/template.js +++ b/lib/template.js @@ -1,30 +1,27 @@ function template(input, variables) { - const type = typeof input - if (type === 'string') { - return Object.keys(variables).reduce( - (output, variable) => - variables[variable] - ? output.replace(`$${variable}`, variables[variable]) - : output, - input - ) - } else if (type === 'object') { - if (Array.isArray(input)) { - const out = [] - for (const value of input) { - out.push(template(value, variables)) + switch (typeof input) { + case 'string': + return Object.keys(variables).reduce( + (output, variable) => + variables[variable] + ? output.replace(`$${variable}`, variables[variable]) + : output, + input + ) + case 'object': + if (Array.isArray(input)) { + return input.map(value => template(value, variables)) + } else { + return Object.entries(input).reduce( + (out, [key, value]) => ({ + ...out, + [key]: template(value, variables) + }), + {} + ) } - return out - } else { - const out = {} - for (let key of Object.keys(input)) { - const value = input[key] - out[key] = template(value, variables) - } - return out - } - } else { - return input + default: + return input } } diff --git a/lib/truncate.js b/lib/truncate.js index 0089830..f501a48 100644 --- a/lib/truncate.js +++ b/lib/truncate.js @@ -1,14 +1,14 @@ module.exports = (messageText, maxLength) => { + if (messageText.length <= maxLength) return messageText + const delimiter = '\n' - if (messageText.length > maxLength) { - messageText = messageText.substring(0, maxLength).split(delimiter) - // if no newlines, we don't remove anything, we keep the truncated message as is - if (messageText.length > 1) { - // remove all text after the last newline in the truncated message - // this avoids truncating in the middle of markdown - messageText.splice(-1, 1) - } - messageText = messageText.join(delimiter) + '*[...]*' - } - return messageText + // split the truncated message into the + // first element and an array with the rest + const [firstLine, ...restLines] = messageText + .substring(0, maxLength) + .split(delimiter) + // if the array restLines is not empty, remove the last element + const truncatedLines = [firstLine, ...restLines.slice(0, -1)] + + return `${truncatedLines.join(delimiter)}*[...]*` } diff --git a/package-lock.json b/package-lock.json index 497d452..b066ed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1108,6 +1108,12 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -1391,6 +1397,20 @@ "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.4.tgz", "integrity": "sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w==" }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1428,6 +1448,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -2191,6 +2217,15 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3107,6 +3142,12 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-own-enumerable-property-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", @@ -5056,6 +5097,37 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nock": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-11.7.0.tgz", + "integrity": "sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.13", + "mkdirp": "^0.5.0", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "node-emoji": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", @@ -8982,6 +9054,12 @@ "pify": "^2.0.0" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -9100,6 +9178,12 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "property-expr": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-1.5.1.tgz", @@ -10303,6 +10387,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", diff --git a/package.json b/package.json index 8c104c1..e5517ee 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,10 @@ "eslint-plugin-standard": "^4.0.0", "husky": "^1.3.1", "lint-staged": "^8.1.5", + "mocha": "^6.2.2", + "nock": "^11.7.0", "prettier": "^1.16.4", - "semantic-release": "^15.13.3", - "mocha": "^6.2.2" + "semantic-release": "^15.13.3" }, "peerDependencies": { "semantic-release": ">=11.0.0 <16.0.0" diff --git a/test/test_postMessage.js b/test/test_postMessage.js new file mode 100644 index 0000000..988a06d --- /dev/null +++ b/test/test_postMessage.js @@ -0,0 +1,53 @@ +const assert = require('assert') +const nock = require('nock') +const postMessage = require('../lib/postMessage') +const SemanticReleaseError = require('@semantic-release/error') + +const slackWebhook = 'https://www.webhook.com' + +async function post(url) { + url = url || slackWebhook + await postMessage('message', { log: () => undefined }, url) +} + +describe('test postMessage', () => { + it('should pass if response is 200 "ok"', async () => { + nock(slackWebhook) + .post('/') + .reply(200, 'ok') + assert.ifError(await post()) + }) + + it('should fail if response text is not "ok"', async () => { + const response = 'not ok' + nock(slackWebhook) + .post('/') + .reply(200, response) + await assert.rejects( + post(), + new SemanticReleaseError(response, 'INVALID SLACK COMMAND') + ) + }) + + it('should fail if response status code is not 200', async () => { + const response = 'error message' + nock(slackWebhook) + .post('/') + .reply(500, response) + await assert.rejects( + post(), + new SemanticReleaseError(response, 'INVALID SLACK COMMAND') + ) + }) + + it('should fail if incorrect url', async () => { + const incorrectUrl = 'https://sekhfskdfdjksfkjdhfsd.com' + await assert.rejects(post(incorrectUrl), { + name: 'SemanticReleaseError', + code: 'SLACK CONNECTION FAILED', + details: undefined, + message: /ENOTFOUND/, + semanticRelease: true + }) + }) +}) diff --git a/test/test_template.js b/test/test_template.js new file mode 100644 index 0000000..26190be --- /dev/null +++ b/test/test_template.js @@ -0,0 +1,107 @@ +const assert = require('assert') +const template = require('../lib/template') + +describe('test template', () => { + it('should replace variable in string if present', () => { + const world = 'underworld' + const expected = `hello ${world}` + const actual = template('hello $world', { world }) + assert.equal(expected, actual) + }) + + it('should not replace variable in string if not present', () => { + const expected = 'hello $world' + const actual = template('hello $world', { other: 'underworld' }) + assert.equal(expected, actual) + }) + + it('should return string as is with no matching variables', () => { + const expected = 'hello world' + const actual = template(expected, { other: 'underworld' }) + assert.equal(expected, actual) + }) + + it('should return string as is without any variables', () => { + const expected = 'hello world' + const actual = template(expected, {}) + assert.equal(expected, actual) + }) + + it('should replace multiple variables in string if present', () => { + const world = 'underworld' + const home = 'back' + const expected = `hello ${world}, welcome ${home}` + const actual = template('hello $world, welcome $home', { + world, + home + }) + assert.equal(expected, actual) + }) + + it('should replace variable in list', () => { + const expected = 'underworld' + const actual = template(['$world'], { world: expected }) + assert.equal(expected, actual) + }) + + it('should replace multiple variables in list', () => { + const expected = ['underworld', 'goodbye'] + const actual = template(['$world', '$hello'], { + world: expected[0], + hello: expected[1] + }) + assert.deepEqual(expected, actual) + }) + + it('should replace variable in object', () => { + const expected = { hello: 'underworld' } + const actual = template({ hello: '$world' }, { world: expected.hello }) + assert.deepEqual(expected, actual) + }) + + it('should replace multiple variables in object', () => { + const world = 'underworld' + const home = 'back' + const expected = { hello: `${world}, welcome ${home}` } + const actual = template( + { hello: '$world, welcome $home' }, + { world, home } + ) + assert.deepEqual(expected, actual) + }) + + it('should replace variables only if present', () => { + const world = 'underworld' + const home = 'back' + const noVariables = 'there are no variables here' + const expected = { + hello: `${world}, welcome ${home}`, + dimension: `${noVariables}` + } + const actual = template( + { hello: '$world, welcome $home', dimension: `${noVariables}` }, + { world, home } + ) + assert.deepEqual(expected, actual) + }) + + it('should replace variables in all entries', () => { + const world = 'underworld' + const nothing = 'everything' + const expected = { + hello: `${world}`, + own: `${nothing}` + } + const actual = template( + { hello: '$world', own: '$nothing' }, + { world, nothing } + ) + assert.deepEqual(expected, actual) + }) + + it('should return default if neither string or object', () => { + const expected = 123 + const actual = template(expected) + assert.equal(expected, actual) + }) +})