| #!/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:]) |