diff --git a/contrib/teamcity/build.sh b/contrib/teamcity/build.sh new file mode 100755 --- /dev/null +++ b/contrib/teamcity/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash -euo pipefail + +ROOT_DIR=`git rev-parse --show-toplevel` + +# Report build status to phabricator +report() { + EXIT_CODE=$? + + set +e + + if [[ ${EXIT_CODE} != 0 ]]; then + echo "failure" > build.status + else + echo "success" > build.status + fi + + cd ${ROOT_DIR} + ./contrib/teamcity/teamcitybot.py build.status test_bitcoin.xml + exit $EXIT_CODE +} + +# Configure and build +mkdir -p output + +cd ${ROOT_DIR} +rm build.status test_bitcoin.xml + +# Trap exit for reporting +trap report EXIT + +# Configure and run build +./autogen.sh +./configure --prefix=`pwd`/output/ +make -j 6 + +# Run unit tests +./src/test/test_bitcoin --log_format=JUNIT > test_bitcoin.xml +echo $? > build.status + diff --git a/contrib/teamcity/requirements.txt b/contrib/teamcity/requirements.txt new file mode 100644 --- /dev/null +++ b/contrib/teamcity/requirements.txt @@ -0,0 +1,4 @@ +s# Tested against the following versions +junitparser==1.0.0 +phabricator==0.7.0 +pygit2==0.26.2 diff --git a/contrib/teamcity/teamcitybot.py b/contrib/teamcity/teamcitybot.py new file mode 100755 --- /dev/null +++ b/contrib/teamcity/teamcitybot.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# +# Description: Quick and dirty script to read build output and report it to phabricator. +# + +from phabricator import Phabricator +import pygit2 +import urlparse +import sys +import os +import os.path +from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error, Failure + + +def get_failures(junitfile): + """Return a map of failures from a given junit report""" + ts = JUnitXml.fromfile(junitfile) + failures = {} + for case in ts: + failure_texts = [] + for failure in case.iterchildren(Failure): + failure_texts.append(failure._elem.text.strip()) + + if len(failure_texts) != 0: + key = "{}.{}".format(case.name, case.classname) + failures[key] = "\n".join(failure_texts) + + return failures + + +def get_commit_message(): + """Get the current commit message""" + repo = pygit2.Repository(pygit2.discover_repository(".")) + commit_message = repo.head.peel().message + + return commit_message + + +def get_revision(phab, commit_message): + """Return a phabricator `revisionID` for the given commit body""" + diffInfo = phab.differential.parsecommitmessage(corpus=commit_message) + return diffInfo.fields['revisionID'] + + +def get_author(phab, revisionID): + diffdata = phab.differential.diff.search( + constraints={"ids": [revisionID]}).data[0] + authorPHID = diffdata['fields']['authorPHID'] + return authorPHID + + +def create_task_body(buildUrl, failures): + failure_blocks = [] + + # TODO: Fix this templating mess. + for failure, message in failures.iteritems(): + failure_blocks.append("""{failure} +``` +{message} +``` +""".format(failure=failure, message=message)) + + task_body = """A [build]({url}) has failed for the following reasons: + +{reasons} +""".format(url=buildUrl, reasons="\n".join(failure_blocks)) + + return task_body + + +def create_task(phab, guiltyPHID, revisionID, task_body): + revision_str = "D{}".format(revisionID) + + phab.maniphest.edit(transactions=[ + {"type": "owner", "value": authorPHID}, + {"type": "title", "value": "Revision {} broke tests".format( + revision_str)}, + {"type": "description", "value": task_body.format( + revision=revision_str)} + ]) + + +def create_comment(phab, revisionID, build_status, buildUrl): + status_verb = "" + if build_status == "success": + status_verb = "suceeded" + else: + status_verb = "failed" + + phab.differential.revision.edit(transactions=[ + {"type": "comment", "value": "This diff has {} [testing]({}).".format( + status_verb, buildUrl)} + ], objectIdentifier=revisionID) + + +def main(args): + if len(args) < 2: + print("Please provide a list of junit reports as command line arguments.") + sys.exit(1) + + token = os.getenv("TEAMCITY_CONDUIT_TOKEN", None) + if not token: + print("Please provide a conduit token in the environment variable ""TEAMCITY_CONDUIT_TOKEN""") + sys.exit(1) + + phabricatorUrl = os.getenv( + 'PHABRICATOR_URL', 'https://reviews.bitcoinabc.org/api/') + buildUrl = os.getenv('BUILD_URL', '') + + build_status = "success" + failures = {} + for arg in args: + # All inputs may not exist if the build fails prematurely + if not os.path.isfile(arg): + continue + + if arg.endswith(".status"): + with open(arg, "r") as f: + build_status = f.read().strip() + elif arg.endswith(".xml"): + failures.update(get_failures(arg)) + + if len(failures) != 0: + build_status = "failure" + + phab = Phabricator(host=phabricatorUrl, token=token) + phab.update_interfaces() + + revisionID = get_revision(phab, get_commit_message()) + authorPHID = get_author(phab, revisionID) + + if build_status == "success": + task_body = create_task_body(buildUrl, failures) + create_task(phab, authorPHID, revisionID, task_body) + + create_comment(phab, revisionID, build_status, buildUrl) + + +if __name__ == "__main__": + main(sys.argv)