-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
Copy pathteamcity-post-failures.py
executable file
·175 lines (146 loc) · 6.56 KB
/
teamcity-post-failures.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
#!/usr/bin/env python3
"""Post failures from the current teamcity job as github issues.
Requires the following environment variables:
- TC_API_PASSWORD
- TC_BUILD_BRANCH
- TC_BUILD_ID
- GITHUB_API_TOKEN
"""
import json
import os
import re
import subprocess
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
from pkg_resources import parse_version
from urllib.parse import urljoin, urlencode
BASEURL = "https://teamcity.cockroachdb.com/httpAuth/app/rest/"
auth_handler = urllib.request.HTTPBasicAuthHandler()
auth_handler.add_password(realm='TeamCity',
uri='https://teamcity.cockroachdb.com',
user='robot',
passwd=os.environ['TC_API_PASSWORD'])
opener = urllib.request.build_opener(auth_handler)
def tc_url(path, **params):
return urljoin(BASEURL, path) + '?' + urlencode(params)
def collect_build_results(build_id):
"""Yield a sequence of (name, log, category) pairs for all failed tests.
Looks at the given build ID and all its dependencies.
"""
dep_data = ET.parse(opener.open(tc_url('builds/{0}'.format(build_id),
fields='snapshot-dependencies(build(id,status))')))
for b in dep_data.findall("./snapshot-dependencies/build"):
if b.attrib['status'] != 'SUCCESS':
yield from collect_build_results(b.attrib['id'])
test_data = ET.parse(opener.open(tc_url('testOccurrences',
locator='count:100,status:FAILURE,build:(id:{0})'.format(build_id),
fields='testOccurrence(details,name,duration,build(buildType(name)))')))
for o in test_data.findall('./testOccurrence'):
# Category is test, race, acceptance, ...
category = o.find('build/buildType').attrib['name']
# Extract TestLint/Foo from "TestLint: TestLint/Foo".
matches = re.split(r"[: ]+", o.attrib['name'], 1)
test_name = matches[-1]
test_log = '--- FAIL: {0}/{1} ({2:.3f}s)\n{3}\n'.format(
o.find("build/buildType").attrib["name"],
o.attrib["name"],
int(o.attrib["duration"])/1000.,
o.findtext("details"))
yield (test_name, test_log, category)
def get_probable_milestone():
try:
tag = subprocess.check_output(['git', 'describe', '--abbrev=0', '--tags'],
universal_newlines=True)
except subprocess.CalledProcessError as e:
print('warning: unable to load latest tag: {0}'.format(e))
print('issue will be posted without milestone')
return None
match = re.match(r'v(\d+\.\d+)', tag)
if not match:
print('unable to parse version {0}; issue will be posted without milestone'.format(tag))
return None
version = match.group(1)
try:
res = urllib.request.urlopen(
'https://api.github.com/repos/cockroachdb/cockroach/milestones?state=open')
milestones = json.loads(res.read().decode(res.info().get_param('charset') or 'utf-8'))
except (ValueError, urllib.error.HTTPError) as e:
print('warning: unable to load milestones: {0}'.format(e))
print('issue will be posted without milestone')
return None
for m in milestones:
if m['title'] == version:
return m['number']
print('no milestone matching {0}; issue will be posted without milestone'.format(version))
return None
def create_issue(build_id, topname, category, failed_tests):
"""Format a list of failed tests as an issue.
Returns a dict which should be encoded as json for posting to the
github API.
"""
return {
'title': 'teamcity: failed test: {0}'.format(topname),
'body': '''\
The following tests appear to have failed on {0} ({5}): {4}
You may want to check [for open issues](https://github.com/cockroachdb/cockroach/issues?q=is%3Aissue+is%3Aopen+{3}).
[#{1}](https://teamcity.cockroachdb.com/viewLog.html?buildId={1}):
```
{2}
```
Please assign, take a look and update the issue accordingly.
'''.format(
os.environ['TC_BUILD_BRANCH'],
build_id,
''.join(set(t[0]+"\n"+("..." if len(t[1]) > 4000 else "") + t[1][-4000:]+"\n\n\n" for t in failed_tests)),
topname,
', '.join(set(t[0] for t in failed_tests)),
category,
),
'labels': ['C-test-failure', 'O-robot'],
'milestone': get_probable_milestone(),
}
def post_issue(issue):
req = urllib.request.Request(
'https://api.github.com/repos/cockroachdb/cockroach/issues',
data=json.dumps(issue).encode('utf-8'),
headers={'Authorization': 'token {0}'.format(os.environ['GITHUB_API_TOKEN'])})
try:
opener.open(req).read()
except urllib.error.HTTPError as e:
# Github's error response bodies include useful information, so print them.
print('Posting to github failed with error code {0}'.format(e.code))
print('Error response body: {0}'.format(e.read()))
print('Request body: {0}'.format(issue))
raise
if __name__ == '__main__':
branch = os.environ['TC_BUILD_BRANCH']
if branch == 'master' or branch.startswith('release-'):
build_id = os.environ['TC_BUILD_ID']
failed_tests = list(collect_build_results(build_id))
issues = {}
for failed_test in failed_tests:
category = failed_test[2]
# From TestImportPGDump/Subtest, extract TestImportPGDump.
matches = re.split(r"^([^/]+)/?", failed_test[0], 1)
if len(matches) < 3:
matches.append("")
topname, subname = matches[1], matches[2]
if category == "roachtest" and topname == "acceptance":
if subname == "":
# Don't post roachtest failures for the umbrella acceptance test.
# In roachtest, parent tests contain no logic.
continue
# Special casing for roachtest so that it doesn't lump any
# acceptance failure into an issue that just says "acceptance".
topname = subname
subname = ""
if topname in issues:
issues[topname].append(failed_test)
else:
issues[topname] = [failed_test]
for topname, failures in issues.items():
# TODO(tschottdorf): this should really shell out to our Go issue
# poster in pkg/cmd/internal/issues to avoid duplicating logic.
issue = create_issue(build_id, topname, failures[0][2], failures)
post_issue(issue)