Skip to content

Commit

Permalink
feat: add support for token/channel option (#79)
Browse files Browse the repository at this point in the history
Supporting alternative to the webhook url that makes use of a channel name and oauth token

fix #73
  • Loading branch information
SimeonC committed Dec 16, 2021
1 parent 526e3ef commit aa1eef6
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 39 deletions.
75 changes: 65 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ Add the plugin to your npm-project:
$ npm install semantic-release-slack-bot -D
```

The corresponding slack app has to be installed in your slack workspace as well. Follow the instructions under [configuration](#configuration) for more information.
## Slack App/Webhook Usage

## Usage
The corresponding slack app has to be installed in your slack workspace as well. Follow the instructions under [configuration](#configuration) for more information.

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

Expand Down Expand Up @@ -62,6 +62,48 @@ With this example:
- Slack notifications are sent on a failure or successful release from branch "master"
- Slack notifications are skipped on all other branches

## Slack Access Token/Channel Usage

This configuration can be used with a [**bot**](https://api.slack.com/authentication/token-types#bot) Slack Access token with minimum permissions of `chat:write`.

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

```json
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"semantic-release-slack-bot",
{
"notifyOnSuccess": false,
"notifyOnFail": false,
"slackToken": "token",
"slackChannel": "my-channel-name",
"branchesConfig": [
{
"pattern": "lts/*",
"notifyOnFail": true
},
{
"pattern": "master1",
"notifyOnSuccess": true,
"notifyOnFail": true
}
]
}
]
]
}
```

With this example:

- Slack notification will always be sent to the channel "#my-channel-name"
- Slack notifications are sent on a failure release from branches matching "lts/\*"
- Slack notifications are sent on a failure or successful release from branch "master"
- Slack notifications are skipped on all other branches

## Screenshots

![Screenshot of success](images/screenshot-success.png)
Expand All @@ -77,20 +119,29 @@ The plugin uses a slack webhook which you get by adding the slack app to your sl

For the security concerned, feel free to [create your own slack app](https://api.slack.com/apps) and create a webhook or inspect the server code that does this creation for you at [create-webhook.js](lambda/create-webhook.js). The only required permission for the webhook is to publish to a single channel.

### Slack app authentication
### Slack Webhook

Installing the app will yield you with a webhook that the app uses to publish updates to your selected channel. The Slack webhook authentication link is **required and needs to be kept a secret**. It should be defined in the [environment variables](#environment-variables).

### Slack Access tokens

If you are creating your own slack app you can choose to use a bot access token and channel instead of the webhook with at least one of the following permission scopes.

1. `chat:write` with this permission scope the app/bot must be added to any channels before it can post to them
2. `chat:write.public` with this permission scope we can automatically post to any public channel for more information see [here](https://api.slack.com/authentication/basics#public)

### Environment variables

The `SLACK_WEBHOOK` variable can be defined in the environment where you will run semantic release. This can be done by exporting it in bash or in the user interface of your CI provider. Obtain this token by installing the slack app according to [slack app installation](#slack-app-installation).
Options can be defined in the environment where you will run semantic release. This can be done by exporting it in bash or in the user interface of your CI provider.

Alternatively, you could pass the webhook as a configuration option.
Alternatively, you can pass the webhook as a configuration option or use an Access Token.

| Variable | Description |
| -------------------------- | -------------------------------------------------------- |
| `SLACK_WEBHOOK` | Slack webhook created when adding app to workspace. |
| `SEMANTIC_RELEASE_PACKAGE` | Override or add package name instead of npm package name |
| Variable | Description |
| -------------------------- | ------------------------------------------------------------------------------------ |
| `SLACK_WEBHOOK` | Slack webhook created when adding app to workspace. |
| `SLACK_TOKEN` | Slack bot Access token. |
| `SLACK_CHANNEL` | Slack channel name or id to send notifications to (must be used with `SLACK_TOKEN`). |
| `SEMANTIC_RELEASE_PACKAGE` | Override or add package name instead of npm package name |

### Options

Expand All @@ -103,8 +154,12 @@ Alternatively, you could pass the webhook as a configuration option.
| `onSuccessTemplate` | Provides a template for the slack message object on success when `notifyOnSuccess` is `true`. See [templating](#templating). | undefined |
| `onFailTemplate` | Provides a template for the slack message object on fail when `notifyOnFail` is `true`. See [templating](#templating). | undefined |
| `markdownReleaseNotes` | Pass release notes through markdown to slack formatter before rendering. | false |
| `slackWebhookEnVar` | This decides what the environment variable for exporting the slack webhook is called. | SLACK_WEBHOOK |
| `slackWebhookEnVar` | This decides what the environment variable for exporting the slack webhook value. | SLACK_WEBHOOK |
| `slackWebhook` | Slack webhook created when adding app to workspace. | value of the environment variable matching `slackWebhookEnVar` |
| `slackTokenEnVar` | This decides what the environment variable for exporting the slack token value. | SLACK_WEBHOOK |
| `slackToken` | Slack bot token. | value of the environment variable matching `slackWebhookEnVar` |
| `slackChannelEnVar` | This decides what the environment variable for exporting the slack channel value. | SLACK_WEBHOOK |
| `slackChannel` | Slack channel name or id to send notifications to. | value of the environment variable matching `slackWebhookEnVar` |
| `packageName` | Override or add package name instead of npm package name | SEMANTIC_RELEASE_PACKAGE or npm package name |
| `unsafeMaxLength` | Maximum character length for the release notes before truncation. If unsafeMaxLength is too high, messages can be dropped. [Read here](https://github.com/juliuscc/semantic-release-slack-bot/issues/26#issuecomment-569804359) for more information. Set to '0' to turn off truncation entirely. | 2900 |
| `branchesConfig` | Allow to specify a custom configuration for branches which match a given pattern. For every branches matching a branch config, the config will be merged with the one put at the root. A key "pattern" used to filter the branch using glob expression must be contained in every branchesConfig. | [] |
Expand Down
10 changes: 8 additions & 2 deletions lib/fail.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const getConfigToUse = require('./getConfigToUse')
const postMessage = require('./postMessage')
const template = require('./template')
const getSlackVars = require('./getSlackVars')

module.exports = async (pluginConfig, context) => {
const {
Expand All @@ -12,7 +13,8 @@ module.exports = async (pluginConfig, context) => {
} = context

const configToUse = getConfigToUse(pluginConfig, context)
const { slackWebhook = process.env.SLACK_WEBHOOK, packageName } = configToUse
const { packageName } = configToUse
const { slackWebhook, slackToken, slackChannel } = getSlackVars(configToUse)

const package_name =
SEMANTIC_RELEASE_PACKAGE || packageName || npm_package_name
Expand Down Expand Up @@ -112,5 +114,9 @@ module.exports = async (pluginConfig, context) => {
}
}

await postMessage(slackMessage, logger, slackWebhook)
await postMessage(slackMessage, logger, {
slackWebhook,
slackChannel,
slackToken
})
}
18 changes: 18 additions & 0 deletions lib/getSlackVars.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = config => {
const {
slackWebhookEnVar = 'SLACK_WEBHOOK',
slackWebhook = process.env[slackWebhookEnVar],
slackTokenEnVar = 'SLACK_TOKEN',
slackToken = process.env[slackTokenEnVar],
slackChannelEnVar = 'SLACK_CHANNEL',
slackChannel = process.env[slackChannelEnVar]
} = config
return {
slackWebhookEnVar,
slackWebhook,
slackChannelEnVar,
slackChannel,
slackTokenEnVar,
slackToken
}
}
32 changes: 24 additions & 8 deletions lib/postMessage.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
const fetch = require('node-fetch')
const SemanticReleaseError = require('@semantic-release/error')

module.exports = async (message, logger, slackWebhook) => {
module.exports = async (
message,
logger,
{ slackWebhook, slackToken, slackChannel }
) => {
let response
let bodyText
try {
response = await fetch(slackWebhook, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(message)
})
if (slackToken && slackChannel) {
message.channel = slackChannel
response = await fetch('https://slack.com/api/chat.postMessage', {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: slackToken
},
body: JSON.stringify(message)
})
} else {
response = await fetch(slackWebhook, {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(message)
})
}
bodyText = await response.text()
} catch (e) {
throw new SemanticReleaseError(e.message, 'SLACK CONNECTION FAILED')
Expand Down
15 changes: 8 additions & 7 deletions lib/success.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const template = require('./template')
const truncate = require('./truncate')
const getRepoInfo = require('./getRepoInfo')
const getConfigToUse = require('./getConfigToUse')
const getSlackVars = require('./getSlackVars')

// 2900 is the limit for a message block of type 'section'.
const MAX_LENGTH = 2900
Expand All @@ -18,12 +19,8 @@ module.exports = async (pluginConfig, context) => {
} = context

const configToUse = getConfigToUse(pluginConfig, context)
const {
slackWebhookEnVar = 'SLACK_WEBHOOK',
slackWebhook = process.env[slackWebhookEnVar],
unsafeMaxLength = MAX_LENGTH,
packageName
} = configToUse
const { unsafeMaxLength = MAX_LENGTH, packageName } = configToUse
const { slackWebhook, slackToken, slackChannel } = getSlackVars(configToUse)

const package_name =
SEMANTIC_RELEASE_PACKAGE || packageName || npm_package_name
Expand Down Expand Up @@ -113,5 +110,9 @@ module.exports = async (pluginConfig, context) => {
}
}

await postMessage(slackMessage, logger, slackWebhook)
await postMessage(slackMessage, logger, {
slackWebhook,
slackChannel,
slackToken
})
}
33 changes: 29 additions & 4 deletions lib/verifyConditions.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
const SemanticReleaseError = require('@semantic-release/error')
const getSlackVars = require('./getSlackVars')

module.exports = (pluginConfig, context) => {
const { logger } = context
const {
slackWebhookEnVar = 'SLACK_WEBHOOK',
slackWebhook = process.env[slackWebhookEnVar]
} = pluginConfig
slackWebhook,
slackWebhookEnVar,
slackToken,
slackTokenEnVar,
slackChannel,
slackChannelEnVar
} = getSlackVars(pluginConfig)

if (!slackWebhook) {
if (slackChannel || slackToken) {
if (!slackToken) {
logger.log(
'SLACK_CHANNEL must be used with SLACK_TOKEN which has not been defined.'
)
throw new SemanticReleaseError(
'No Slack token defined.',
'ENOSLACKTOKEN',
`A Slack Token must be created and set in the \`${slackTokenEnVar}\` environment variable on your CI environment.\n\n\nPlease make sure to create a Slack Token and to set it in the \`${slackTokenEnVar}\` environment variable on your CI environment. Alternatively, provide \`slackToken\` as a configuration option.`
)
} else if (!slackChannel) {
logger.log(
'SLACK_TOKEN must be used with SLACK_CHANNEL which has not been defined.'
)
throw new SemanticReleaseError(
'No Slack channel defined.',
'ENOSLACKCHANNEL',
`A Slack Channel must be created and set in the \`${slackChannelEnVar}\` environment variable on your CI environment.\n\n\nPlease make sure to set a Slack Channel in the \`${slackChannelEnVar}\` environment variable on your CI environment. Alternatively, provide \`slackChannel\` as a configuration option.`
)
}
} else if (!slackWebhook) {
logger.log('SLACK_WEBHOOK has not been defined.')
throw new SemanticReleaseError(
'No Slack web-hook defined.',
Expand Down
59 changes: 52 additions & 7 deletions test/postMessage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@ const postMessage = require('../lib/postMessage')
const SemanticReleaseError = require('@semantic-release/error')

const slackWebhook = 'https://www.webhook.com'
const slackPostMessageDomain = 'https://slack.com'
const slackPostMessagePath = '/api/chat.postMessage'
const slackToken = 'token'
const slackChannel = 'channel'

async function post(url) {
async function postWebhook(url) {
url = url || slackWebhook
await postMessage('message', { log: () => undefined }, url)
await postMessage('message', { log: () => undefined }, { slackWebhook: url })
}

describe('test postMessage', () => {
async function postToken(token, channel) {
token = token || slackToken
channel = channel || slackChannel
await postMessage(
{ text: 'message' },
{ log: () => undefined },
{ slackToken: token, slackChannel: channel }
)
}

describe('test postMessage with webhook', () => {
it('should pass if response is 200 "ok"', async () => {
nock(slackWebhook)
.post('/')
.reply(200, 'ok')
assert.ifError(await post())
assert.ifError(await postWebhook())
})

it('should fail if response text is not "ok"', async () => {
Expand All @@ -24,7 +38,7 @@ describe('test postMessage', () => {
.post('/')
.reply(200, response)
await assert.rejects(
post(),
postWebhook(),
new SemanticReleaseError(response, 'INVALID SLACK COMMAND')
)
})
Expand All @@ -35,14 +49,14 @@ describe('test postMessage', () => {
.post('/')
.reply(500, response)
await assert.rejects(
post(),
postWebhook(),
new SemanticReleaseError(response, 'INVALID SLACK COMMAND')
)
})

it('should fail if incorrect url', async () => {
const incorrectUrl = 'https://sekhfskdfdjksfkjdhfsd.com'
await assert.rejects(post(incorrectUrl), {
await assert.rejects(postWebhook(incorrectUrl), {
name: 'SemanticReleaseError',
code: 'SLACK CONNECTION FAILED',
details: undefined,
Expand All @@ -51,3 +65,34 @@ describe('test postMessage', () => {
})
})
})

describe('test postMessage with token/channel', () => {
it('should pass if response is 200 "ok"', async () => {
nock(slackPostMessageDomain)
.post(slackPostMessagePath)
.reply(200, 'ok')
assert.ifError(await postToken())
})

it('should fail if response text is not "ok"', async () => {
const response = 'not ok'
nock(slackPostMessageDomain)
.post(slackPostMessagePath)
.reply(200, response)
await assert.rejects(
postToken(),
new SemanticReleaseError(response, 'INVALID SLACK COMMAND')
)
})

it('should fail if response status code is not 200', async () => {
const response = 'error message'
nock(slackPostMessageDomain)
.post(slackPostMessagePath)
.reply(500, response)
await assert.rejects(
postToken(),
new SemanticReleaseError(response, 'INVALID SLACK COMMAND')
)
})
})
Loading

0 comments on commit aa1eef6

Please sign in to comment.