Skip to content

Commit

Permalink
feat(2505): multi workspace slack notification [1] (#47)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: slack notification config is changed
It must be changed to the new configuration format.
  • Loading branch information
y-oksaku committed Aug 11, 2023
1 parent 72d4876 commit bd73b1e
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 51 deletions.
15 changes: 12 additions & 3 deletions index.js
Expand Up @@ -62,7 +62,16 @@ const SCHEMA_JOB_DATA = Joi.object().keys({
settings: SCHEMA_SLACK_SETTINGS.required()
});
const SCHEMA_SLACK_CONFIG = Joi.object().keys({
token: Joi.string().required()
defaultWorkspace: Joi.string().required(),
workspaces: Joi.object()
.unknown()
.pattern(
Joi.string(),
Joi.object().keys({
token: Joi.string().required()
})
)
.required()
});

/**
Expand Down Expand Up @@ -205,7 +214,7 @@ function buildStatus(buildData, config) {
attachments
};

slacker(config.token, buildData.settings.slack.channels, slackMessage);
slacker(config, buildData.settings.slack.channels, slackMessage);
}

/**
Expand Down Expand Up @@ -251,7 +260,7 @@ function jobStatus(jobData, config) {
message
};

slacker(config.token, jobData.settings.slack.channels, slackMessage);
slacker(config, jobData.settings.slack.channels, slackMessage);
}

class SlackNotifier extends NotificationBase {
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "screwdriver-notifications-slack",
"version": "4.0.0",
"version": "5.0.0",
"description": "Sends slack notifications on certain build events",
"main": "index.js",
"scripts": {
Expand Down
74 changes: 58 additions & 16 deletions slack.js
Expand Up @@ -2,47 +2,89 @@

const { WebClient } = require('@slack/web-api');
const logger = require('screwdriver-logger');
const webClients = {};

let web;
/**
* Parse slack config
* @method parseSlackConfig
* @param {Object} config slack config
* @param {String} config.defaultWorkspace default slack workspace
* @param {Object} config.workspaces slack workspaces config
* @param {String} channelConfig slack channel name (ex. channel1, workspace1:channel2)
* @return {Object}
*/
function parseSlackConfig(config, channelConfig) {
let workspace = config.defaultWorkspace;
let channel = channelConfig;

if (channel.includes(':')) {
[workspace, channel] = channel.split(':');
}

const workspaceConfig = config.workspaces[workspace];

if (!workspaceConfig) {
logger.error(`Cannot find Slack token for ${workspace}.`);

return {};
}

return { workspace, channel, token: workspaceConfig.token };
}

/**
* Post message to a specific channel
* @method postMessage
* @param {String} channelName name of the channel
* @param {String} channel name of the channel
* @param {Object} web slack web client
* @param {Object} payload payload of the slack message
* @return {Promise}
*/
function postMessage(channelName, payload) {
function postMessage(channel, web, payload) {
// Can post to channel name directly https://api.slack.com/methods/chat.postMessage#channels
return web.chat
.postMessage({
channel: channelName,
channel,
text: payload.message,
as_user: true,
attachments: payload.attachments
})
.catch(err => logger.error(`postMessage: failed to notify Slack channel ${channelName}: ${err.message}`));
.catch(err => logger.error(`postMessage: failed to notify Slack channel ${channel}: ${err.message}`));
}

/**
* Sends slack message to slack channels
* @param {String} token access token for slack
* @param {Object} config slack config
* @param {String} config.defaultWorkspace default slack workspace
* @param {Object} config.workspaces slack workspaces config
* @param {String[]} channels slack channel names
* @param {Object} payload slack message payload
* @return {Promise}
*/
function slacker(token, channels, payload) {
if (!web) {
web = new WebClient(token, {
retryConfig: {
retries: 5,
factor: 3.86,
maxRetryTime: 30 * 60 * 1000 // Set maximum time to 30 min that the retried operation is allowed to run
function slacker(config, channels, payload) {
return Promise.all(
channels.map(channelConfig => {
const { workspace, channel, token } = parseSlackConfig(config, channelConfig);

if (!workspace || !token) {
return Promise.resolve();
}
});
}

return Promise.all(channels.map(channelName => postMessage(channelName, payload)));
if (!webClients[workspace]) {
webClients[workspace] = new WebClient(token, {
retryConfig: {
retries: 5,
factor: 3.86,
maxRetryTime: 30 * 60 * 1000 // Set maximum time to 30 min that the retried operation is allowed to run
}
});
}

const web = webClients[workspace];

return postMessage(channel, web, payload);
})
);
}

module.exports = slacker;
106 changes: 97 additions & 9 deletions test/index.test.js
Expand Up @@ -63,7 +63,11 @@ describe('index', () => {
beforeEach(() => {
serverMock = new Hapi.Server();
configMock = {
token: 'y353e5y45'
defaultWorkspace: 'test1',
workspaces: {
test1: { token: 'test-token1' },
test2: { token: 'test-token2' }
}
};
buildDataMock = {
settings: {
Expand Down Expand Up @@ -105,7 +109,51 @@ describe('index', () => {
serverMock.events.emit(eventMock, buildDataMock);

process.nextTick(() => {
assert.calledWith(WebClientConstructorMock.WebClient, configMock.token);
assert.calledWith(WebClientConstructorMock.WebClient, configMock.workspaces.test1.token);
assert.calledThrice(WebClientMock.chat.postMessage);
done();
});
});

it('verifies that included multi workspaces slack notifier', done => {
const buildDataMultiWOrkspaceMock = {
settings: {
slack: {
channels: ['meeseeks', 'test1:caaaandoooo', 'test2:aaa'],
statuses: ['SUCCESS']
}
},
status: 'SUCCESS',
pipeline: {
id: '123',
scmRepo: {
name: 'screwdriver-cd/notifications',
url: 'http://scmtest/master'
}
},
jobName: 'publish',
build: { id: '1234' },
event: {
id: '12345',
causeMessage: 'Merge pull request #26 from screwdriver-cd/notifications',
creator: { username: 'foo' },
commit: {
author: { name: 'foo' },
message: 'fixing a bug'
},
sha: '1234567890abcdeffedcba098765432100000000'
},
buildLink: 'http://thisisaSDtest.com/pipelines/12/builds/1234',
isFixed: false
};

serverMock.event(eventMock);
serverMock.events.on(eventMock, data => notifier.notify(eventMock, data));
serverMock.events.emit(eventMock, buildDataMultiWOrkspaceMock);

process.nextTick(() => {
assert.calledWith(WebClientConstructorMock.WebClient.firstCall, configMock.workspaces.test1.token);
assert.calledWith(WebClientConstructorMock.WebClient.secondCall, configMock.workspaces.test2.token);
assert.calledThrice(WebClientMock.chat.postMessage);
done();
});
Expand All @@ -118,7 +166,7 @@ describe('index', () => {
serverMock.events.emit(eventMock, buildDataMock);

process.nextTick(() => {
assert.calledWith(WebClientConstructorMock.WebClient, configMock.token);
assert.calledWith(WebClientConstructorMock.WebClient, configMock.workspaces.test1.token);
assert.calledThrice(WebClientMock.chat.postMessage);
done();
});
Expand Down Expand Up @@ -920,7 +968,7 @@ describe('index', () => {
serverMock.events.emit(eventMock, buildDataMock);

process.nextTick(() => {
assert.calledWith(WebClientConstructorMock.WebClient, configMock.token);
assert.calledWith(WebClientConstructorMock.WebClient, configMock.workspaces.test1.token);
done();
});
});
Expand All @@ -940,8 +988,42 @@ describe('index', () => {
});

describe('config is validated', () => {
it('validates token', () => {
configMock = {};
it('validates defaultWorkspace', () => {
configMock = {
workspaces: {
test: { token: 'y353e5y45' }
}
};
try {
notifier = new SlackNotifier(configMock);
assert.fail('should not get here');
} catch (err) {
assert.instanceOf(err, Error);
assert.equal(err.name, 'ValidationError');
}
});

it('validates workspaces', () => {
configMock = {
defaultWorkspace: 'test'
};
try {
notifier = new SlackNotifier(configMock);
assert.fail('should not get here');
} catch (err) {
assert.instanceOf(err, Error);
assert.equal(err.name, 'ValidationError');
}
});

it('validates workspace token', () => {
configMock = {
defaultWorkspace: 'test1',
workspaces: {
test1: { token: 'test-token1' },
test2: {}
}
};
try {
notifier = new SlackNotifier(configMock);
assert.fail('should not get here');
Expand All @@ -956,7 +1038,10 @@ describe('index', () => {
beforeEach(() => {
serverMock = new Hapi.Server();
configMock = {
token: 'faketoken'
defaultWorkspace: 'test',
workspaces: {
test: { token: 'y353e5y45' }
}
};
buildDataMock = {
settings: {
Expand Down Expand Up @@ -1035,7 +1120,10 @@ describe('index', () => {
beforeEach(() => {
serverMock = new Hapi.Server();
configMock = {
token: 'faketoken'
defaultWorkspace: 'test',
workspaces: {
test: { token: 'y353e5y45' }
}
};
jobDataMock = {
settings: {
Expand Down Expand Up @@ -1071,7 +1159,7 @@ describe('index', () => {
serverMock.events.emit(eventMock, jobDataMock);

process.nextTick(() => {
assert.calledWith(WebClientConstructorMock.WebClient, configMock.token);
assert.calledWith(WebClientConstructorMock.WebClient, configMock.workspaces.test.token);
done();
});
});
Expand Down

0 comments on commit bd73b1e

Please sign in to comment.