Support for github commit status
Github reporter sets commit status of pull request based on the result
of the pipeline.
Change-Id: Id95bf0dbaa710c555e3a1838d3430e18ac9501aa
Co-Authored-By: Jesse Keating <omgjlk@us.ibm.com>
diff --git a/doc/source/reporters.rst b/doc/source/reporters.rst
index 4972e66..cf4f6e4 100644
--- a/doc/source/reporters.rst
+++ b/doc/source/reporters.rst
@@ -32,7 +32,23 @@
------
Zuul reports back to GitHub pull requests via GitHub API.
-It will create a comment containing the job status.
+On success and failure, it creates a comment containing the build results.
+It also sets the status on start, success and failure. Status name and
+description is taken from the pipeline.
+
+A :ref:`connection` that uses the github driver must be supplied to the
+reporter. It has the following options:
+
+ **status**
+ String value (``pending``, ``success``, ``failure``) that the reporter should
+ set as the commit status on github.
+ ``status: 'success'``
+
+ **comment**
+ Boolean value (``true`` or ``false``) that determines if the reporter should
+ add a comment to the pipeline status to the github pull request. Defaults
+ to ``true``.
+ ``comment: false``
SMTP
----
diff --git a/tests/base.py b/tests/base.py
index 56b1269..0ad1ec1 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -556,6 +556,7 @@
self.branch = branch
self.upstream_root = upstream_root
self.comments = []
+ self.statuses = {}
self.updated_at = None
self.head_sha = None
self._createPRRef()
@@ -566,11 +567,13 @@
"""Adds a commit on top of the actual PR head."""
self._addCommitToRepo()
self._updateTimeStamp()
+ self._clearStatuses()
def forcePush(self):
"""Clears actual commits and add a commit on top of the base."""
self._addCommitToRepo(reset=True)
self._updateTimeStamp()
+ self._clearStatuses()
def getPullRequestOpenedEvent(self):
return self._getPullRequestEvent('opened')
@@ -644,6 +647,16 @@
repo = self._getRepo()
return repo.references[self._getPRReference()].commit.hexsha
+ def setStatus(self, state, url, description, context):
+ self.statuses[context] = {
+ 'state': state,
+ 'url': url,
+ 'description': description
+ }
+
+ def _clearStatuses(self):
+ self.statuses = {}
+
def _getPRReference(self):
return '%s/head' % self.number
@@ -745,10 +758,19 @@
# just returns master for now
return ['master']
- def report(self, project, pr_number, message, params=None):
+ def commentPull(self, project, pr_number, message):
pull_request = self.pull_requests[pr_number - 1]
pull_request.addComment(message)
+ def setCommitStatus(self, project, sha, state,
+ url='', description='', context=''):
+ owner, proj = project.split('/')
+ for pr in self.pull_requests:
+ pr_owner, pr_project = pr.project.split('/')
+ if (pr_owner == owner and pr_project == proj and
+ pr.head_sha == sha):
+ pr.setStatus(state, url, description, context)
+
class BuildHistory(object):
def __init__(self, **kw):
diff --git a/tests/fixtures/layouts/reporting-github.yaml b/tests/fixtures/layouts/reporting-github.yaml
new file mode 100644
index 0000000..bcbac1b
--- /dev/null
+++ b/tests/fixtures/layouts/reporting-github.yaml
@@ -0,0 +1,45 @@
+- pipeline:
+ name: check
+ description: Standard check
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: opened
+ start:
+ github:
+ status: 'pending'
+ comment: false
+ success:
+ github:
+ status: 'success'
+
+- pipeline:
+ name: reporting
+ description: Uncommon reporting
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request
+ action: comment
+ comment: 'reporting check'
+ start:
+ github: {}
+ success:
+ github:
+ comment: false
+ failure:
+ github:
+ comment: false
+
+- job:
+ name: project-test1
+
+- project:
+ name: org/project
+ check:
+ jobs:
+ - project-test1
+ reporting:
+ jobs:
+ - project-test1
diff --git a/tests/fixtures/zuul-github-driver.conf b/tests/fixtures/zuul-github-driver.conf
index b4a85f7..58c7cd5 100644
--- a/tests/fixtures/zuul-github-driver.conf
+++ b/tests/fixtures/zuul-github-driver.conf
@@ -3,6 +3,7 @@
[zuul]
job_name_in_report=true
+status_url=http://zuul.example.com/status
[merger]
git_dir=/tmp/zuul-test/git
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index 1e5f6a6..9017ce9 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -13,6 +13,8 @@
# under the License.
import logging
+import re
+from testtools.matchers import MatchesRegex
from tests.base import ZuulTestCase, simple_layout, random_sha1
@@ -122,3 +124,44 @@
"""Test that git_ssh option gives git url with ssh"""
url = self.fake_github_ssh.real_getGitUrl('org/project')
self.assertEqual('ssh://git@github.com/org/project.git', url)
+
+ @simple_layout('layouts/reporting-github.yaml', driver='github')
+ def test_reporting(self):
+ # pipeline reports pull status both on start and success
+ self.executor_server.hold_jobs_in_build = True
+ pr = self.fake_github.openFakePullRequest('org/project', 'master')
+ self.fake_github.emitEvent(pr.getPullRequestOpenedEvent())
+ self.waitUntilSettled()
+ self.assertIn('check', pr.statuses)
+ check_status = pr.statuses['check']
+ self.assertEqual('Standard check', check_status['description'])
+ self.assertEqual('pending', check_status['state'])
+ self.assertEqual('http://zuul.example.com/status', check_status['url'])
+ self.assertEqual(0, len(pr.comments))
+
+ self.executor_server.hold_jobs_in_build = False
+ self.executor_server.release()
+ self.waitUntilSettled()
+ check_status = pr.statuses['check']
+ self.assertEqual('Standard check', check_status['description'])
+ self.assertEqual('success', check_status['state'])
+ self.assertEqual('http://zuul.example.com/status', check_status['url'])
+ self.assertEqual(1, len(pr.comments))
+ self.assertThat(pr.comments[0],
+ MatchesRegex('.*Build succeeded.*', re.DOTALL))
+
+ # pipeline does not report any status but does comment
+ self.executor_server.hold_jobs_in_build = True
+ self.fake_github.emitEvent(
+ pr.getCommentAddedEvent('reporting check'))
+ self.waitUntilSettled()
+ self.assertNotIn('reporting', pr.statuses)
+ # comments increased by one for the start message
+ self.assertEqual(2, len(pr.comments))
+ self.assertThat(pr.comments[1],
+ MatchesRegex('.*Starting reporting jobs.*', re.DOTALL))
+ self.executor_server.hold_jobs_in_build = False
+ self.executor_server.release()
+ self.waitUntilSettled()
+ self.assertNotIn('reporting', pr.statuses)
+ self.assertEqual(2, len(pr.comments))
diff --git a/zuul/driver/github/__init__.py b/zuul/driver/github/__init__.py
index 4e54f11..2d6829d 100644
--- a/zuul/driver/github/__init__.py
+++ b/zuul/driver/github/__init__.py
@@ -34,7 +34,7 @@
return githubsource.GithubSource(self, connection)
def getReporter(self, connection, config=None):
- return githubreporter.GithubReporter(self, connection)
+ return githubreporter.GithubReporter(self, connection, config)
def getTriggerSchema(self):
return githubtrigger.getSchema()
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index 7085bf6..6604d81 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -277,12 +277,18 @@
owner, proj = project_name.split('/')
return self.github.pull_request(owner, proj, number).as_dict()
- def report(self, project, pr_number, message):
- owner, proj = project.name.split('/')
+ def commentPull(self, project, pr_number, message):
+ owner, proj = project.split('/')
repository = self.github.repository(owner, proj)
pull_request = repository.issue(pr_number)
pull_request.create_comment(message)
+ def setCommitStatus(self, project, sha, state, url='', description='',
+ context=''):
+ owner, proj = project.split('/')
+ repository = self.github.repository(owner, proj)
+ repository.create_status(sha, state, url, description, context)
+
def _ghTimestampToDate(self, timestamp):
return time.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
diff --git a/zuul/driver/github/githubreporter.py b/zuul/driver/github/githubreporter.py
index 685c60e..ecbb486 100644
--- a/zuul/driver/github/githubreporter.py
+++ b/zuul/driver/github/githubreporter.py
@@ -24,15 +24,54 @@
name = 'github'
log = logging.getLogger("zuul.GithubReporter")
+ def __init__(self, driver, connection, config=None):
+ super(GithubReporter, self).__init__(driver, connection, config)
+ self._commit_status = self.config.get('status', None)
+ self._create_comment = self.config.get('comment', True)
+
def report(self, source, pipeline, item):
- """Comment on PR with test status."""
+ """Comment on PR and set commit status."""
+ if self._create_comment:
+ self.addPullComment(pipeline, item)
+ if (self._commit_status is not None and
+ hasattr(item.change, 'patchset') and
+ item.change.patchset is not None):
+ self.setPullStatus(pipeline, item)
+
+ def addPullComment(self, pipeline, item):
message = self._formatItemReport(pipeline, item)
project = item.change.project.name
pr_number = item.change.number
+ self.log.debug(
+ 'Reporting change %s, params %s, message: %s' %
+ (item.change, self.config, message))
+ self.connection.commentPull(project, pr_number, message)
- self.connection.report(project, pr_number, message)
+ def setPullStatus(self, pipeline, item):
+ project = item.change.project.name
+ sha = item.change.patchset
+ context = pipeline.name
+ state = self._commit_status
+ url = ''
+ if self.connection.sched.config.has_option('zuul', 'status_url'):
+ url = self.connection.sched.config.get('zuul', 'status_url')
+ description = ''
+ if pipeline.description:
+ description = pipeline.description
+
+ self.log.debug(
+ 'Reporting change %s, params %s, status:\n'
+ 'context: %s, state: %s, description: %s, url: %s' %
+ (item.change, self.config, context, state,
+ description, url))
+
+ self.connection.setCommitStatus(
+ project, sha, state, url, description, context)
def getSchema():
- github_reporter = v.Any(str, v.Schema({}, extra=True))
+ github_reporter = v.Schema({
+ 'status': v.Any('pending', 'success', 'failure'),
+ 'comment': bool
+ })
return github_reporter