Skip to content

Commit

Permalink
Add github reporter that can comment on Pull Requests
Browse files Browse the repository at this point in the history
  • Loading branch information
anish committed Feb 9, 2017
1 parent 54686bc commit d7e047a
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 19 deletions.
1 change: 1 addition & 0 deletions master/buildbot/newsfragments/githubcomment.feature
@@ -0,0 +1 @@
New reporter :py:class:`~buildbot.reporters.github.GitHubCommentPush` can comment on GitHub PRs
74 changes: 63 additions & 11 deletions master/buildbot/reporters/github.py
Expand Up @@ -16,6 +16,8 @@
from __future__ import absolute_import
from __future__ import print_function

import re

from twisted.internet import defer
from twisted.python import log

Expand Down Expand Up @@ -45,9 +47,7 @@ def reconfigService(self, token,
context=None, baseURL=None, verbose=False, **kwargs):
yield http.HttpStatusPushBase.reconfigService(self, **kwargs)

self.context = context or Interpolate('buildbot/%(prop:buildername)s')
self.startDescription = startDescription or 'Build started.'
self.endDescription = endDescription or 'Build done.'
self.setDefaults(context, startDescription, endDescription)
if baseURL is None:
baseURL = HOSTED_BASE_URL
if baseURL.endswith('/'):
Expand All @@ -60,15 +60,23 @@ def reconfigService(self, token,
})
self.verbose = verbose

def setDefaults(self, context, startDescription, endDescription):
self.context = context or Interpolate('buildbot/%(prop:buildername)s')
self.startDescription = startDescription or 'Build started.'
self.endDescription = endDescription or 'Build done.'

def createStatus(self,
repo_user, repo_name, sha, state, target_url=None,
description=None, context=None):
context=None, issue=None, description=None):
"""
:param repo_user: GitHub user or organization
:param repo_name: Name of the repository
:param sha: Full sha to create the status for.
:param state: one of the following 'pending', 'success', 'error'
or 'failure'.
:param target_url: Target url to associate with this status.
:param description: Short description of the status.
:param context: Build context
:return: A deferred with the result from GitHub.
This code comes from txgithub by @tomprince.
Expand Down Expand Up @@ -105,9 +113,11 @@ def send(self, build):
CANCELLED: 'error'
}.get(build['results'], 'error')
description = yield props.render(self.endDescription)
else:
elif self.startDescription:
state = 'pending'
description = yield props.render(self.startDescription)
else:
return

context = yield props.render(self.context)

Expand All @@ -118,6 +128,13 @@ def send(self, build):

project = sourcestamps[0]['project']

branch = props['branch']
m = re.search(r"refs/pull/([0-9]*)/merge", branch)
if m:
issue = m.group(1)
else:
issue = None

if project:
repoOwner, repoName = project.split('/')
else:
Expand All @@ -140,6 +157,8 @@ def send(self, build):
target_url = bytes2NativeString(target_url, encoding='utf-8')
context = context.encode('utf-8')
context = bytes2NativeString(context, encoding='utf-8')
issue = issue.encode('utf-8')
issue = bytes2NativeString(issue, encoding='utf-8')
description = description.encode('utf-8')
description = bytes2NativeString(description, encoding='utf-8')
yield self.createStatus(
Expand All @@ -149,16 +168,49 @@ def send(self, build):
state=state,
target_url=target_url,
context=context,
issue=issue,
description=description
)
if self.verbose:
log.msg(
'Status "{state}" sent for '
'{repoOwner}/{repoName} at {sha}.'.format(
state=state, repoOwner=repoOwner, repoName=repoName, sha=sha))
'Updated status with "{state}" for'
'{repoOwner}/{repoName} at {sha}, issue {issue}.'.format(
state=state, repoOwner=repoOwner, repoName=repoName, sha=sha, issue=issue))
except Exception as e:
log.err(
e,
'Fail to send status "{state}" for '
'{repoOwner}/{repoName} at {sha}'.format(
state=state, repoOwner=repoOwner, repoName=repoName, sha=sha))
'Failed to update "{state}" for '
'{repoOwner}/{repoName} at {sha}, issue {issue}'.format(
state=state, repoOwner=repoOwner, repoName=repoName, sha=sha, issue=issue))


class GitHubCommentPush(GitHubStatusPush):
name = "GitHubCommentPush"
neededDetails = dict(wantProperties=True)

def setDefaults(self, context, startDescription, endDescription):
self.context = ''
self.startDescription = startDescription
self.endDescription = endDescription or 'Build done.'

def createStatus(self,
repo_user, repo_name, sha, state, target_url=None,
context=None, issue=None, description=None):
"""
:param repo_user: GitHub user or organization
:param repo_name: Name of the repository
:param issue: Pull request number
:param state: one of the following 'pending', 'success', 'error'
or 'failure'.
:param description: Short description of the status.
:return: A deferred with the result from GitHub.
This code comes from txgithub by @tomprince.
txgithub is based on twisted's webclient agent, which is much less reliable and featureful
as txrequest (support for proxy, connection pool, keep alive, retry, etc)
"""
payload = {'body': description}

return self._http.post(
'/'.join(['/repos', repo_user, repo_name, 'issues', issue, 'comments']),
json=payload)
60 changes: 59 additions & 1 deletion master/buildbot/test/unit/test_reporter_github.py
Expand Up @@ -25,6 +25,7 @@
from buildbot.process.results import FAILURE
from buildbot.process.results import SUCCESS
from buildbot.reporters.github import HOSTED_BASE_URL
from buildbot.reporters.github import GitHubCommentPush
from buildbot.reporters.github import GitHubStatusPush
from buildbot.test.fake import httpclientservice as fakehttpclientservice
from buildbot.test.fake import fakemaster
Expand All @@ -49,10 +50,14 @@ def setUp(self):
'Authorization': 'token XXYYZZ',
'User-Agent': 'Buildbot'
})
self.sp = sp = GitHubStatusPush('XXYYZZ')
sp = self.setService()
sp.sessionFactory = Mock(return_value=Mock())
yield sp.setServiceParent(self.master)

def setService(self):
self.sp = GitHubStatusPush('XXYYZZ')
return self.sp

def tearDown(self):
return self.master.stopService()

Expand Down Expand Up @@ -91,3 +96,56 @@ def test_basic(self):
self.sp.buildFinished(("build", 20, "finished"), build)
build['results'] = FAILURE
self.sp.buildFinished(("build", 20, "finished"), build)

@defer.inlineCallbacks
def setupBuildResultsMin(self, buildResults):
self.insertTestData([buildResults], buildResults, insertSS=False)
build = yield self.master.data.get(("builds", 20))
defer.returnValue(build)

@defer.inlineCallbacks
def test_empty(self):
build = yield self.setupBuildResultsMin(SUCCESS)
build['complete'] = False
self.sp.buildStarted(("build", 20, "started"), build)
build['complete'] = True
self.sp.buildFinished(("build", 20, "finished"), build)
build['results'] = FAILURE
self.sp.buildFinished(("build", 20, "finished"), build)


class TestGitHubCommentPush(TestGitHubStatusPush):

def setService(self):
self.sp = GitHubCommentPush('XXYYZZ')
return self.sp

@defer.inlineCallbacks
def test_basic(self):
build = yield self.setupBuildResults(SUCCESS)
# we make sure proper calls to txrequests have been made
self._http.expect(
'post',
'/repos/buildbot/buildbot/issues/34/comments',
json={'body': 'Build done.'})
self._http.expect(
'post',
'/repos/buildbot/buildbot/issues/34/comments',
json={'body': 'Build done.'})

build['complete'] = False
self.sp.buildStarted(("build", 20, "started"), build)
build['complete'] = True
self.sp.buildFinished(("build", 20, "finished"), build)
build['results'] = FAILURE
self.sp.buildFinished(("build", 20, "finished"), build)

@defer.inlineCallbacks
def test_empty(self):
build = yield self.setupBuildResultsMin(SUCCESS)
build['complete'] = False
self.sp.buildStarted(("build", 20, "started"), build)
build['complete'] = True
self.sp.buildFinished(("build", 20, "finished"), build)
build['results'] = FAILURE
self.sp.buildFinished(("build", 20, "finished"), build)
18 changes: 12 additions & 6 deletions master/buildbot/test/util/reporter.py
Expand Up @@ -35,26 +35,32 @@ class ReporterTestMixin(object):
'revision': TEST_REVISION,
'event.change.id': TEST_CHANGE_ID,
'event.change.project': TEST_PROJECT,
'branch': 'refs/pull/34/merge',
}
THING_URL = 'http://thing.example.com'

def insertTestData(self, buildResults, finalResult):
def insertTestData(self, buildResults, finalResult, insertSS=True):
self.db = self.master.db
self.db.insertTestData([
fakedb.Master(id=92),
fakedb.Worker(id=13, name='wrk'),
fakedb.Builder(id=79, name='Builder0'),
fakedb.Builder(id=80, name='Builder1'),
fakedb.Buildset(id=98, results=finalResult, reason="testReason1"),
fakedb.BuildsetSourceStamp(buildsetid=98, sourcestampid=234),
fakedb.SourceStamp(id=234,
project=self.TEST_PROJECT,
revision=self.TEST_REVISION,
repository=self.TEST_REPO),
fakedb.Change(changeid=13, branch=u'master', revision=u'9283', author='me@foo',
repository=self.TEST_REPO, codebase=u'cbgerrit',
project=u'world-domination', sourcestampid=234),
])

if insertSS:
self.db.insertTestData([
fakedb.BuildsetSourceStamp(buildsetid=98, sourcestampid=234),
fakedb.SourceStamp(id=234,
project=self.TEST_PROJECT,
revision=self.TEST_REVISION,
repository=self.TEST_REPO)
])

for i, results in enumerate(buildResults):
self.db.insertTestData([
fakedb.BuildRequest(
Expand Down
41 changes: 41 additions & 0 deletions master/docs/manual/cfg-reporters.rst
Expand Up @@ -848,6 +848,47 @@ You can create a token from you own `GitHub - Profile - Applications - Register
:param boolean verbose: if True, logs a message for each successful status push
:param list builders: only send update for specified builders

.. bb:reporter:: GitHubCommentPush
GitHubCommentPush
~~~~~~~~~~~~~~~~~


.. @cindex GitHubCommentPush
.. py:class:: buildbot.reporters.github.GitHubCommentPush
::

from buildbot.plugins import reporters, util

gc = status.GitHubCommentPush(token='githubAPIToken',
startDescription='Build started.',
endDescription='Build done.')
factory = util.BuildFactory()
buildbot_bbtools = util.BuilderConfig(
name='builder-name',
workernames=['worker1'],
factory=factory)
c['builders'].append(buildbot_bbtools)
c['services'].append(gc)

:class:`GitHubCommentPush` publishes a comment on a PR using `GitHub Review Comments API <https://developer.github.com/v3/pulls/comments/>`_.

It requires `txrequests`_ package to allow interaction with GitHub REST API.

It is configured with at least a GitHub API token. By default, it will only comment at the end of a build unless a ``startDescription`` is provided.

You can create a token from you own `GitHub - Profile - Applications - Register new application <https://github.com/settings/applications>`_ or use an external tool to generate one.

.. py:class:: GitHubCommentPush(token, startDescription=None, endDescription=None, baseURL=None, verbose=False, builders=None)
:param string token: token used for authentication.
:param rendereable string startDescription: Custom start message (default: None)
:param rendereable string endDescription: Custom end message (default: 'Build done.')
:param string baseURL: specify the github api endpoint if you work with GitHub Enterprise
:param boolean verbose: if True, logs a message for each successful status push
:param list builders: only send update for specified builders

.. bb:reporter:: StashStatusPush
StashStatusPush
Expand Down
2 changes: 1 addition & 1 deletion master/setup.py
Expand Up @@ -314,7 +314,7 @@ def define_plugin_entries(groups):
('buildbot.reporters.gerrit_verify_status',
['GerritVerifyStatusPush']),
('buildbot.reporters.http', ['HttpStatusPush']),
('buildbot.reporters.github', ['GitHubStatusPush']),
('buildbot.reporters.github', ['GitHubStatusPush', 'GitHubCommentPush']),
('buildbot.reporters.gitlab', ['GitLabStatusPush']),
('buildbot.reporters.stash', ['StashStatusPush']),
('buildbot.reporters.bitbucket', ['BitbucketStatusPush']),
Expand Down

0 comments on commit d7e047a

Please sign in to comment.