Skip to content

Commit fb8b84d

Browse files
authored
feat(2719): Scheduled job stop with notification if there's no admin (#44)
1 parent cf4d465 commit fb8b84d

File tree

3 files changed

+331
-161
lines changed

3 files changed

+331
-161
lines changed

index.js

Lines changed: 203 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,201 @@ const SCHEMA_BUILD_DATA = Joi.object().keys({
6161
...schema.plugins.notifications.schemaBuildData,
6262
settings: SCHEMA_SLACK_SETTINGS.required()
6363
});
64+
const SCHEMA_JOB_DATA = Joi.object().keys({
65+
...schema.plugins.notifications.schemaJobData,
66+
settings: SCHEMA_SLACK_SETTINGS.required()
67+
});
6468
const SCHEMA_SLACK_CONFIG = Joi.object().keys({
6569
token: Joi.string().required()
6670
});
6771

72+
/**
73+
* Handle slack messaging for build status
74+
* @method buildStatus
75+
* @param {Object} buildData
76+
* @param {String} buildData.status Build status
77+
* @param {Object} buildData.pipeline Pipeline
78+
* @param {String} buildData.jobName Job name
79+
* @param {Object} buildData.build Build
80+
* @param {Object} buildData.event Event
81+
* @param {String} buildData.buildLink Build link
82+
* @param {Object} buildData.settings Notification setting
83+
* @param {Object} config Slack notifications config
84+
*/
85+
function buildStatus(buildData, config) {
86+
try {
87+
Joi.attempt(buildData, SCHEMA_BUILD_DATA, 'Invalid build data format');
88+
} catch (e) {
89+
return;
90+
}
91+
92+
// Slack channel overwrite from meta data. Job specific only.
93+
const metaReplaceVar = `build.meta.notification.slack.${buildData.jobName}.channels`;
94+
95+
const metaDataSlackChannel = hoek.reach(buildData, metaReplaceVar, { default: false });
96+
97+
let channelReplacement;
98+
99+
if (metaDataSlackChannel) {
100+
channelReplacement = metaDataSlackChannel.split(',');
101+
// Remove empty/blank items.
102+
channelReplacement = channelReplacement.filter(x => x.trim() !== '');
103+
}
104+
// Slack channels from configuration
105+
if (typeof buildData.settings.slack === 'string' || Array.isArray(buildData.settings.slack)) {
106+
buildData.settings.slack =
107+
typeof buildData.settings.slack === 'string' ? [buildData.settings.slack] : buildData.settings.slack;
108+
buildData.settings.slack = {
109+
channels: buildData.settings.slack,
110+
statuses: DEFAULT_STATUSES,
111+
minimized: false
112+
};
113+
}
114+
if (channelReplacement) {
115+
buildData.settings.slack.channels = channelReplacement;
116+
}
117+
118+
if (buildData.settings.slack.statuses === undefined) {
119+
buildData.settings.slack.statuses = DEFAULT_STATUSES;
120+
}
121+
122+
// Add for fixed notification
123+
if (buildData.isFixed) {
124+
buildData.settings.slack.statuses.push('FIXED');
125+
}
126+
127+
// Do not change the `buildData.status` directly
128+
// It affects the behavior of other notification plugins
129+
let notificationStatus = buildData.status;
130+
131+
if (buildData.settings.slack.statuses.includes('FAILURE')) {
132+
if (notificationStatus === 'SUCCESS' && buildData.isFixed) {
133+
notificationStatus = 'FIXED';
134+
}
135+
}
136+
137+
if (!buildData.settings.slack.statuses.includes(notificationStatus)) {
138+
return;
139+
}
140+
141+
const pipelineLink = buildData.buildLink.split('/builds')[0];
142+
const truncatedSha = buildData.event.sha.slice(0, 6);
143+
const cutOff = 150;
144+
const commitMessage =
145+
buildData.event.commit.message.length > cutOff
146+
? `${buildData.event.commit.message.substring(0, cutOff)}...`
147+
: buildData.event.commit.message;
148+
149+
// Slack channel overwrite from meta data. Job specific only.
150+
const metaMinimizedReplaceVar = `build.meta.notification.slack.${buildData.jobName}.minimized`;
151+
const isMinimized = hoek.reach(buildData, metaMinimizedReplaceVar, {
152+
default: buildData.settings.slack.minimized
153+
});
154+
155+
let message = isMinimized
156+
? // eslint-disable-next-line max-len
157+
`<${pipelineLink}|${buildData.pipeline.scmRepo.name}#${buildData.jobName}> *${notificationStatus}*`
158+
: // eslint-disable-next-line max-len
159+
`*${notificationStatus}* ${STATUSES_MAP[notificationStatus]} <${pipelineLink}|${buildData.pipeline.scmRepo.name} ${buildData.jobName}>`;
160+
161+
const rootDir = hoek.reach(buildData, 'pipeline.scmRepo.rootDir', { default: false });
162+
163+
if (rootDir) {
164+
message = `${message}\n*Source Directory:* ${rootDir}`;
165+
}
166+
167+
const metaMessage = hoek.reach(buildData, 'build.meta.notification.slack.message', { default: false });
168+
const metaVar = `build.meta.notification.slack.${buildData.jobName}.message`;
169+
const buildMessage = hoek.reach(buildData, metaVar, { default: false });
170+
171+
// Use job specific message over generic message.
172+
if (buildMessage) {
173+
message = `${message}\n${buildMessage}`;
174+
} else if (metaMessage) {
175+
message = `${message}\n${metaMessage}`;
176+
}
177+
178+
const attachments = isMinimized
179+
? [
180+
{
181+
fallback: '',
182+
color: COLOR_MAP[notificationStatus],
183+
fields: [
184+
{
185+
title: 'Build',
186+
value: `<${buildData.buildLink}|#${buildData.build.id}>`,
187+
short: true
188+
}
189+
]
190+
}
191+
]
192+
: [
193+
{
194+
fallback: '',
195+
color: COLOR_MAP[notificationStatus],
196+
title: `#${buildData.build.id}`,
197+
title_link: `${buildData.buildLink}`,
198+
// eslint-disable-next-line max-len
199+
text:
200+
`${commitMessage} (<${buildData.event.commit.url}|${truncatedSha}>)` +
201+
`\n${buildData.event.causeMessage}`
202+
}
203+
];
204+
205+
const slackMessage = {
206+
message,
207+
attachments
208+
};
209+
210+
slacker(config.token, buildData.settings.slack.channels, slackMessage);
211+
}
212+
213+
/**
214+
* Handle slack messaging for job status
215+
* @method jobStatus
216+
* @param {Object} jobData
217+
* @param {String} jobData.status Status
218+
* @param {Object} jobData.pipeline Pipeline
219+
* @param {String} jobData.jobName Job name
220+
* @param {String} jobData.pipelineLink Pipeline link
221+
* @param {String} jobData.message Message
222+
* @param {Object} jobData.settings Notification setting
223+
* @param {Object} config Slack notifications config
224+
*/
225+
function jobStatus(jobData, config) {
226+
try {
227+
Joi.attempt(jobData, SCHEMA_JOB_DATA, 'Invalid job data format');
228+
} catch (e) {
229+
return;
230+
}
231+
232+
// Slack channels from configuration
233+
if (typeof jobData.settings.slack === 'string' || Array.isArray(jobData.settings.slack)) {
234+
jobData.settings.slack =
235+
typeof jobData.settings.slack === 'string' ? [jobData.settings.slack] : jobData.settings.slack;
236+
jobData.settings.slack = {
237+
channels: jobData.settings.slack,
238+
statuses: DEFAULT_STATUSES,
239+
minimized: false
240+
};
241+
}
242+
243+
const isMinimized = jobData.settings.slack.minimized;
244+
const message = isMinimized
245+
? // eslint-disable-next-line max-len
246+
`<${jobData.pipelineLink}|${jobData.pipeline.scmRepo.name}#${jobData.jobName}> *${jobData.status}*\n${jobData.message}`
247+
: // eslint-disable-next-line max-len
248+
`*${jobData.status}* ${STATUSES_MAP[jobData.status]} <${jobData.pipelineLink}|${
249+
jobData.pipeline.scmRepo.name
250+
} ${jobData.jobName}>\n${jobData.message}`;
251+
252+
const slackMessage = {
253+
message
254+
};
255+
256+
slacker(config.token, jobData.settings.slack.channels, slackMessage);
257+
}
258+
68259
class SlackNotifier extends NotificationBase {
69260
/**
70261
* Constructs an SlackNotifier
@@ -78,139 +269,24 @@ class SlackNotifier extends NotificationBase {
78269

79270
/**
80271
* Sets listener on server event of name 'eventName' in Screwdriver
81-
* Currently, event is triggered with a build status is updated
82272
* @method _notify
83-
* @param {Object} buildData - Build data emitted with some event from Screwdriver
273+
* @param {String} event - Event emitted from Screwdriver
274+
* @param {Object} payload - Data emitted with some event from Screwdriver
84275
*/
85-
_notify(buildData) {
86-
// Check buildData format against SCHEMA_BUILD_DATA
87-
try {
88-
Joi.attempt(buildData, SCHEMA_BUILD_DATA, 'Invalid build data format');
89-
} catch (e) {
90-
return;
91-
}
92-
if (Object.keys(buildData.settings).length === 0) {
93-
return;
94-
}
95-
96-
// Slack channel overwrite from meta data. Job specific only.
97-
const metaReplaceVar = `build.meta.notification.slack.${buildData.jobName}.channels`;
98-
99-
const metaDataSlackChannel = hoek.reach(buildData, metaReplaceVar, { default: false });
100-
101-
let channelReplacement;
102-
103-
if (metaDataSlackChannel) {
104-
channelReplacement = metaDataSlackChannel.split(',');
105-
// Remove empty/blank items.
106-
channelReplacement = channelReplacement.filter(x => x.trim() !== '');
107-
}
108-
// Slack channels from configuration
109-
if (typeof buildData.settings.slack === 'string' || Array.isArray(buildData.settings.slack)) {
110-
buildData.settings.slack =
111-
typeof buildData.settings.slack === 'string' ? [buildData.settings.slack] : buildData.settings.slack;
112-
buildData.settings.slack = {
113-
channels: buildData.settings.slack,
114-
statuses: DEFAULT_STATUSES,
115-
minimized: false
116-
};
117-
}
118-
if (channelReplacement) {
119-
buildData.settings.slack.channels = channelReplacement;
120-
}
121-
122-
if (buildData.settings.slack.statuses === undefined) {
123-
buildData.settings.slack.statuses = DEFAULT_STATUSES;
124-
}
125-
126-
// Add for fixed notification
127-
if (buildData.isFixed) {
128-
buildData.settings.slack.statuses.push('FIXED');
129-
}
130-
131-
// Do not change the `buildData.status` directly
132-
// It affects the behavior of other notification plugins
133-
let notificationStatus = buildData.status;
134-
135-
if (buildData.settings.slack.statuses.includes('FAILURE')) {
136-
if (notificationStatus === 'SUCCESS' && buildData.isFixed) {
137-
notificationStatus = 'FIXED';
138-
}
139-
}
140-
141-
if (!buildData.settings.slack.statuses.includes(notificationStatus)) {
276+
_notify(event, payload) {
277+
if (!payload || !payload.settings || Object.keys(payload.settings).length === 0) {
142278
return;
143279
}
144-
const pipelineLink = buildData.buildLink.split('/builds')[0];
145-
const truncatedSha = buildData.event.sha.slice(0, 6);
146-
const cutOff = 150;
147-
const commitMessage =
148-
buildData.event.commit.message.length > cutOff
149-
? `${buildData.event.commit.message.substring(0, cutOff)}...`
150-
: buildData.event.commit.message;
151-
152-
// Slack channel overwrite from meta data. Job specific only.
153-
const metaMinimizedReplaceVar = `build.meta.notification.slack.${buildData.jobName}.minimized`;
154-
const isMinimized = hoek.reach(buildData, metaMinimizedReplaceVar, {
155-
default: buildData.settings.slack.minimized
156-
});
157-
158-
let message = isMinimized
159-
? // eslint-disable-next-line max-len
160-
`<${pipelineLink}|${buildData.pipeline.scmRepo.name}#${buildData.jobName}> *${notificationStatus}*`
161-
: // eslint-disable-next-line max-len
162-
`*${notificationStatus}* ${STATUSES_MAP[notificationStatus]} <${pipelineLink}|${buildData.pipeline.scmRepo.name} ${buildData.jobName}>`;
163-
164-
const rootDir = hoek.reach(buildData, 'pipeline.scmRepo.rootDir', { default: false });
165-
166-
if (rootDir) {
167-
message = `${message}\n*Source Directory:* ${rootDir}`;
168-
}
169-
170-
const metaMessage = hoek.reach(buildData, 'build.meta.notification.slack.message', { default: false });
171280

172-
const metaVar = `build.meta.notification.slack.${buildData.jobName}.message`;
173-
174-
const buildMessage = hoek.reach(buildData, metaVar, { default: false });
175-
176-
// Use job specific message over generic message.
177-
if (buildMessage) {
178-
message = `${message}\n${buildMessage}`;
179-
} else if (metaMessage) {
180-
message = `${message}\n${metaMessage}`;
281+
switch (event) {
282+
case 'build_status':
283+
buildStatus(payload, this.config);
284+
break;
285+
case 'job_status':
286+
jobStatus(payload, this.config);
287+
break;
288+
default:
181289
}
182-
const attachments = isMinimized
183-
? [
184-
{
185-
fallback: '',
186-
color: COLOR_MAP[notificationStatus],
187-
fields: [
188-
{
189-
title: 'Build',
190-
value: `<${buildData.buildLink}|#${buildData.build.id}>`,
191-
short: true
192-
}
193-
]
194-
}
195-
]
196-
: [
197-
{
198-
fallback: '',
199-
color: COLOR_MAP[notificationStatus],
200-
title: `#${buildData.build.id}`,
201-
title_link: `${buildData.buildLink}`,
202-
// eslint-disable-next-line max-len
203-
text:
204-
`${commitMessage} (<${buildData.event.commit.url}|${truncatedSha}>)` +
205-
`\n${buildData.event.causeMessage}`
206-
}
207-
];
208-
const slackMessage = {
209-
message,
210-
attachments
211-
};
212-
213-
slacker(this.config.token, buildData.settings.slack.channels, slackMessage);
214290
}
215291

216292
static validateConfig(config) {

package.json

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
"main": "index.js",
66
"scripts": {
77
"pretest": "eslint .",
8-
"test": "nyc --report-dir ./artifacts/coverage --reporter=lcov mocha --reporter mocha-multi-reporters --reporter-options configFile=./mocha.config.json --recursive --timeout 4000 --retries 1 --exit --allow-uncaught true --color true",
9-
"semantic-release": "semantic-release pre && npm publish && semantic-release post"
8+
"test": "nyc --report-dir ./artifacts/coverage --reporter=lcov mocha --reporter mocha-multi-reporters --reporter-options configFile=./mocha.config.json --recursive --timeout 4000 --retries 1 --exit --allow-uncaught true --color true"
109
},
1110
"repository": {
1211
"type": "git",
13-
"url": "git@github.com:screwdriver-cd/notifications-slack.git"
12+
"url": "git+https://github.com/screwdriver-cd/notifications-slack.git"
1413
},
1514
"homepage": "https://github.com/screwdriver-cd/notifications-slack",
1615
"bugs": "https://github.com/screwdriver-cd/screwdriver/issues",
@@ -47,14 +46,11 @@
4746
"@slack/web-api": "^6.4.0",
4847
"joi": "^17.4.0",
4948
"samsam": "^1.3.0",
50-
"screwdriver-data-schema": "^21.0.0",
49+
"screwdriver-data-schema": "^21.28.1",
5150
"screwdriver-logger": "^1.0.2",
52-
"screwdriver-notifications-base": "^3.0.3"
51+
"screwdriver-notifications-base": "^3.2.0"
5352
},
5453
"release": {
55-
"debug": false,
56-
"verifyConditions": {
57-
"path": "./node_modules/semantic-release/src/lib/plugin-noop.js"
58-
}
54+
"debug": false
5955
}
6056
}

0 commit comments

Comments
 (0)