-
Notifications
You must be signed in to change notification settings - Fork 0
/
jenkins_slack_notifications.py
executable file
·299 lines (284 loc) · 10.3 KB
/
jenkins_slack_notifications.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import json
import track_notifications
import os
import requests
import time
import re
from requests.auth import HTTPBasicAuth
from get_slack_id import GetSlackID
# global constants:
HOSTNAME = 'http://qa.tptpm.info:8090'
JENKINS = requests.Session()
# 1000000 milliseconds is about 15 minutes, long enough to catch builds that
# took a while.
MAX_AGE = 1000000
# global variables:
current_time = 0
passed_notifications = []
# Generate list of builds from dict returned by Jenkins API:
def parse_build_data(build_data):
output = []
for job in build_data['jobs']:
for build in job['builds']:
output.append(make_build(build))
return output
# Check if build contains necessary data and return it:
def make_build(build):
if (build['changeSet']['items'] and
# Open paren in build name indicates branch information exists.
'(' in build['fullDisplayName'] and
# CodeSize already notifies via GitHub comment.
'CodeSize' not in build['fullDisplayName']):
user_url = build['changeSet']['items'][0]['author']['absoluteUrl']
build_name = build['fullDisplayName']
return {
'build': build_name,
'number': build['number'],
'result': build['result'],
'timestamp': build['timestamp'],
'url': build['url'],
'author': build['changeSet']['items'][0]['author']['fullName'],
# Parse Jenkins username from url:
'user': user_url.split('/')[4],
# Regex to parse branch name from build:
'branch': re.split('[()]', build_name)[1],
'started_by_naginator': started_by_naginator(build['actions']),
'upstream': upstream(build['actions']),
# The title of the GitHub issue associated with this Jenkins build
# is available in the following field in the Jenkins API.
'title': build['changeSet']['items'][0]['msg']
}
def started_by_naginator(actions):
for action in actions:
if ('causes' in action and
'Naginator' in action['causes'][0]['shortDescription']):
return True
return False
# Return the number of the upstream build of Core-SyntaxCheck-PHP-DevCloud that
# started this build. If the build was started by some other action (some other
# job, CLI, etc.), return 0.
def upstream(actions):
for action in actions:
if ('causes' in action and
'upstream' in action['causes'][0]['shortDescription'] and
'Syntax' in action['causes'][0]['shortDescription']):
# Regex to parse number of upstream build from description of cause:
return int(
re.sub('[^0-9]',
'',
action['causes'][0]['shortDescription'])
)
return 0
# Return True if this is a build of Acceptance that will be re-run by Naginator.
def naginator_check(started_by_naginator, number, name):
global HOSTNAME, JENKINS
if 'Acceptance' not in name:
return False
if not started_by_naginator:
return True
job = '/job/Core-Acceptance-PHP-DevCloud/'
url = '{}{}{}{}'.format(
HOSTNAME,
job,
str(number),
'/artifact/naginator_count'
)
naginator_count = JENKINS.get(url)
url = '{}{}{}{}'.format(
HOSTNAME,
job,
str(number),
'/artifact/naginator_maxcount'
)
naginator_maxcount = JENKINS.get(url)
return naginator_count.json() < naginator_maxcount.json()
# Core-SyntaxCheck-PHP-DevCloud triggers 6 builds, listed here in final_builds.
# Check if each of these builds, triggered by the same upstream build, have
# passed:
def all_tests_passed(upstream, builds):
global passed_notifications
if upstream == 0 or upstream in passed_notifications:
return False
passed_notifications.append(upstream)
final_builds = [
'Acceptance',
'CodeSniff',
'Unit-JS',
'Unit-PHP',
'API-Functional',
'API-Unit'
]
for final_build in final_builds:
result = upstream_match(final_build, upstream, builds)
if not result == 'SUCCESS':
return False
return True
# Return the result of a certain build that was triggered by a certain upstream
# build:
def upstream_match(final_build, upstream, builds):
for build in builds:
if (build and
upstream == build['upstream'] and
final_build in build['build']):
return build['result']
def append_assignee(output, user):
if not output:
return ' cc: @' + user
return ' @' + user
# If a user has failure notifications and finish notifications turned on, they
# will also be notified about the results of tests for pull requests that they
# are assigned to.
def assignees(title):
# Lists of assignees are in the GitHub issues API.
github_api = 'https://api.github.com/repos/TeachersPayTeachers/tpt/issues'
token_header = 'token ' + os.environ['GITHUB_REPO_TOKEN']
header = {'Authorization': token_header}
issues = requests.get(github_api, headers=header).json()
assignees_list = []
for issue in issues:
if not assignees_list and issue['title'] == title:
assignees_list = issue['assignees']
output = ''
for assignee in assignees_list:
user_url = 'https://api.github.com/users/' + assignee['login']
user_data = requests.get(user_url, headers=header).json()
user = GetSlackID(
user_data['name'],
user_data['email'],
assignee['login']
)
if user.exists and user.notify_on_failure and user.notify_on_finish:
output += append_assignee(output, user.slack_name)
return output
def notify(build, builds):
email = build['user'] + '@teacherspayteachers.com'
user = GetSlackID(build['author'], email, build['user'])
if user.exists:
# If the user has failure notifications turned on and the build failed
# and the build is not being re-run by Naginator, then send a failure
# message:
if (user.notify_on_failure and
build['result'] == 'FAILURE' and not
naginator_check(
build['started_by_naginator'],
build['number'],
build['build']
)):
message = (
build['build'] +
' failed: ' +
build['url'] +
assignees(build['title'])
)
slack(user.slack_name, message)
# If the user has finish notifications turned on and the build passed,
# check if the tests that were triggered by the build of SyntaxCheck that
# triggered this build have also passed, and if so, then send a finish
# notification:
if (user.notify_on_finish and
build['result'] == 'SUCCESS' and
all_tests_passed(build['upstream'], builds)):
syntax_job = '/job/Core-SyntaxCheck-PHP-DevCloud/'
syntax_url = '{}{}{}{}'.format(
HOSTNAME,
syntax_job,
str(build['upstream']),
'/changes'
)
message = (
'All tests triggered by upstream build #' +
str(build['upstream']) +
' passed: ' +
syntax_url +
assignees(build['title'])
)
slack(user.slack_name, message)
# Success notification for builds not in the core pipeline:
if ('Core' not in build['build'] and
user.notify_on_failure and
user.notify_on_finish and
build['result'] == 'SUCCESS'):
message = (
build['build'] +
' passed: ' +
build['url'] +
assignees(build['title'])
)
slack(user.slack_name, message)
# Notifications for unusual build statuses (ABORTED, UNSTABLE, etc.):
if (user.notify_on_failure and
user.notify_on_finish and
build['result'] and
build['result'] != 'SUCCESS' and
build['result'] != 'FAILURE'):
message = (
build['build'] +
' was ' +
build['result'] +
': ' +
build['url'] +
assignees(build['title'])
)
slack(user.slack_name, message)
# Add this build to the list of builds already handled:
if build['result']:
track_notifications.track(
build['number'],
build['timestamp'],
'build_numbers'
)
def slack(user, message):
params = {
'token': os.environ['BUILD_BOT_API_TOKEN'],
'channel': '#build-notifications',
'text': '@{}: {}'.format(user, message),
'link_names': 1,
'as_user': True
}
requests.get('https://slack.com/api/chat.postMessage', params=params)
def main():
global HOSTNAME, JENKINS, MAX_AGE
global current_time, passed_notifications
passed_notifications = []
# Convert time to milliseconds for comparison to Jenkins timestamp:
current_time = time.time() * 1000
JENKINS.auth = ('sneagle', os.environ['JENKINS_API_TOKEN'])
JENKINS.timeout = 3
JENKINS.proxies = {'http': os.environ['PROXIMO_URL']}
jenkins_api = '/api/json?tree=jobs['\
'builds['\
'actions['\
'causes['\
'shortDescription'\
']'\
'],'\
'number,'\
'timestamp,'\
'result,'\
'url,'\
'changeSet['\
'items['\
'msg,'\
'author['\
'fullName,'\
'absoluteUrl'\
']'\
']{0}'\
'],'\
'fullDisplayName'\
']'\
']'
url = '{}{}'.format(HOSTNAME, jenkins_api)
builds = parse_build_data(JENKINS.get(url).json())
for build in builds:
if (build and
current_time - build['timestamp'] < MAX_AGE and
build['branch'] != 'master' and not
track_notifications.already_notified(
build['number'],
'build_numbers'
)):
notify(build, builds)
track_notifications.clean(current_time, MAX_AGE, 'build_numbers')
if __name__ == '__main__':
main()