phabricator-report
5 KB
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
#!/usr/bin/env python3
#===----------------------------------------------------------------------===##
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
#===----------------------------------------------------------------------===##
import argparse
import io
import os
import phabricator
import re
import socket
import subprocess
import sys
import time
LLVM_REVIEWS_API = "https://reviews.llvm.org/api/"
def exponentialBackoffRetry(f, exception, maxAttempts=3):
"""Tries calling a function, but retry with exponential backoff if the
function fails with the specified exception.
"""
waitTime = 1
attempts = 0
while True:
try:
f()
break
except exception as e:
attempts += 1
if attempts == maxAttempts:
raise e
else:
time.sleep(waitTime)
waitTime *= 2
def buildPassed(log):
"""
Tries to guess whether a build has passed or not based on the logs
produced by it.
This is really hacky -- it would be better to use the status of the
script that runs the tests, however that script is being piped into
this script, so we can't know its exit status. What we do here is
basically look for abnormal CMake or Lit output, but that is tightly
coupled to the specific CI we're running.
"""
# Lit reporting failures
matches = re.findall(r"^\s*Failed\s*:\s*(\d+)$", log, flags=re.MULTILINE)
if matches and any(int(match) > 0 for match in matches):
return False
# Error while running CMake
if 'CMake Error' in log or 'Configuring incomplete, errors occurred!' in log:
return False
# Ninja failed to build some target
if 'FAILED:' in log:
return False
return True
def main(argv):
parser = argparse.ArgumentParser(
description="""
This script gathers information about a Buildkite build and updates the
Phabricator review associated to the HEAD commit with those results.
The intended usage of this script is to pipe the output of a command defined
in a Buildkite pipeline into it. The script will echo everything to stdout,
like tee, but will also update the Phabricator review associated to HEAD
with the results of the build.
The script is assumed to be running inside a Buildkite agent, and as such,
it assumes the existence of several environment variables that are specific
to Buildkite.
It also assumes that it is running in a context where the HEAD commit contains
the Phabricator ID of the review to update. If the commit does not contain the
Phabricator ID, this script is basically a no-op. This allows running the CI
on commits that are not triggered by a Phabricator review.
""")
args = parser.parse_args(argv)
for var in ('BUILDKITE_LABEL', 'BUILDKITE_JOB_ID', 'BUILDKITE_BUILD_URL', 'CONDUIT_TOKEN'):
if var not in os.environ:
raise RuntimeError(
'The {} environment variable must exist -- are you running '
'this script from a Buildkite agent?'.format(var))
# First, read all the log input and write it line-by-line to stdout.
# This is important so that we can follow progress in the Buildkite
# console. Since we're being piped into in real time, it's also the
# moment to time the duration of the job.
start = time.time()
log = io.StringIO()
while True:
line = sys.stdin.readline()
if line == '':
break
sys.stdout.write(line)
sys.stdout.flush() # flush every line to avoid buffering
log.write(line)
end = time.time()
# Then, extract information from the environment and post-process the logs.
log.seek(0)
log = log.read()
result = 'pass' if buildPassed(log) else 'fail'
resultObject = {
'name': '{BUILDKITE_LABEL} ({BUILDKITE_BUILD_URL}#{BUILDKITE_JOB_ID})'.format(**os.environ),
'result': result,
'duration': end - start,
'details': log
}
commitMessage = subprocess.check_output(['git', 'log', '--format=%B' , '-n', '1']).decode()
phabricatorID = re.search(r'^Phabricator-ID:\s+(.+)$', commitMessage, flags=re.MULTILINE)
# If there's a Phabricator ID in the commit, then the build was triggered
# by a Phabricator review -- update the results back. Otherwise, don't
# do anything.
if phabricatorID:
phabricatorID = phabricatorID.group(1)
token = os.environ['CONDUIT_TOKEN']
phab = phabricator.Phabricator(token=token, host=LLVM_REVIEWS_API)
exponentialBackoffRetry(
lambda: phab.harbormaster.sendmessage(buildTargetPHID=phabricatorID, type=result, unit=[resultObject]),
exception=socket.timeout
)
else:
print('The HEAD commit does not appear to be tied to a Phabricator review -- '
'not uploading the results to any review.')
if __name__ == '__main__':
main(sys.argv[1:])