Merge "Add trigger capability on github pr review" into feature/zuulv3
diff --git a/doc/source/triggers.rst b/doc/source/triggers.rst
index 07b18ab..f73ad2f 100644
--- a/doc/source/triggers.rst
+++ b/doc/source/triggers.rst
@@ -109,7 +109,10 @@
following options.
**event**
- The pull request event from github. A ``pull_request`` event will
+ The event from github. Supported events are ``pull_request``,
+ ``pull_request_review``, and ``push``.
+
+ A ``pull_request`` event will
have associated action(s) to trigger from. The supported actions are:
*opened* - pull request opened
@@ -126,32 +129,46 @@
*unlabeled* - label removed from pull request
+ *review* - review added on pull request
+
*push* - head reference updated (pushed to branch)
+ A ``pull_request_review`` event will
+ have associated action(s) to trigger from. The supported actions are:
+
+ *submitted* - pull request review added
+
+ *dismissed* - pull request review removed
+
**branch**
The branch associated with the event. Example: ``master``. This
field is treated as a regular expression, and multiple branches may
- be listed. Used for ``pull-request`` events.
+ be listed. Used for ``pull_request`` and ``pull_request_review`` events.
**comment**
- This is only used for ``pull_request`` ``comment`` events. It accepts a list
- of regexes that are searched for in the comment string. If any of these
+ This is only used for ``pull_request`` ``comment`` actions. It accepts a
+ list of regexes that are searched for in the comment string. If any of these
regexes matches a portion of the comment string the trigger is matched.
``comment: retrigger`` will match when comments containing 'retrigger'
somewhere in the comment text are added to a pull request.
**label**
- This is only used for ``labeled`` and ``unlabeled`` actions. It accepts a list
- of strings each of which matches the label name in the event literally.
- ``label: recheck`` will match a ``labeled`` action when pull request is
- labeled with a ``recheck`` label. ``label: 'do not test'`` will match a
- ``unlabeled`` action when a label with name ``do not test`` is removed from
- the pull request.
+ This is only used for ``labeled`` and ``unlabeled`` ``pull_request`` actions.
+ It accepts a list of strings each of which matches the label name in the
+ event literally. ``label: recheck`` will match a ``labeled`` action when
+ pull request is labeled with a ``recheck`` label. ``label: 'do not test'``
+ will match a ``unlabeled`` action when a label with name ``do not test`` is
+ removed from the pull request.
- Additionally a ``push`` event can be configured, with an ``ref`` field. This
- field is treated as a regular expression and multiple refs may be listed.
- Github always sends full ref name, eg. ``refs/tags/bar`` and this string is
- matched against the regexp.
+ **state**
+ This is only used for ``pull_request_review`` events. It accepts a list of
+ strings each of which is matched to the review state, which can be one of
+ ``approved``, ``comment``, or ``request_changes``.
+
+ **ref**
+ This is only used for ``push`` events. This field is treated as a regular
+ expression and multiple refs may be listed. Github always sends full ref
+ name, eg. ``refs/tags/bar`` and this string is matched against the regexp.
GitHub Configuration
~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/base.py b/tests/base.py
index 937d60f..2fbdb88 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -617,6 +617,36 @@
}
return (name, data)
+ def getReviewAddedEvent(self, review):
+ name = 'pull_request_review'
+ data = {
+ 'action': 'submitted',
+ 'pull_request': {
+ 'number': self.number,
+ 'title': self.subject,
+ 'updated_at': self.updated_at,
+ 'base': {
+ 'ref': self.branch,
+ 'repo': {
+ 'full_name': self.project
+ }
+ },
+ 'head': {
+ 'sha': self.head_sha
+ }
+ },
+ 'review': {
+ 'state': review
+ },
+ 'repository': {
+ 'full_name': self.project
+ },
+ 'sender': {
+ 'login': 'ghuser'
+ }
+ }
+ return (name, data)
+
def addLabel(self, name):
if name not in self.labels:
self.labels.append(name)
diff --git a/tests/fixtures/layouts/reviews-github.yaml b/tests/fixtures/layouts/reviews-github.yaml
new file mode 100644
index 0000000..1cc887a
--- /dev/null
+++ b/tests/fixtures/layouts/reviews-github.yaml
@@ -0,0 +1,21 @@
+- pipeline:
+ name: reviews
+ manager: independent
+ trigger:
+ github:
+ - event: pull_request_review
+ action: submitted
+ state: 'approve'
+ success:
+ github:
+ label:
+ - 'tests passed'
+
+- job:
+ name: project-reviews
+
+- project:
+ name: org/project
+ reviews:
+ jobs:
+ - project-reviews
diff --git a/tests/unit/test_github_driver.py b/tests/unit/test_github_driver.py
index a1a05e0..f2a6e5b 100644
--- a/tests/unit/test_github_driver.py
+++ b/tests/unit/test_github_driver.py
@@ -177,6 +177,21 @@
self.assertEqual(2, len(self.history))
self.assertEqual(['other label'], C.labels)
+ @simple_layout('layouts/reviews-github.yaml', driver='github')
+ def test_review_event(self):
+ A = self.fake_github.openFakePullRequest('org/project', 'master', 'A')
+ self.fake_github.emitEvent(A.getReviewAddedEvent('approve'))
+ self.waitUntilSettled()
+ self.assertEqual(1, len(self.history))
+ self.assertEqual('project-reviews', self.history[0].name)
+ self.assertEqual(['tests passed'], A.labels)
+
+ # test_review_unmatched_event
+ B = self.fake_github.openFakePullRequest('org/project', 'master', 'B')
+ self.fake_github.emitEvent(B.getReviewAddedEvent('comment'))
+ self.waitUntilSettled()
+ self.assertEqual(1, len(self.history))
+
@simple_layout('layouts/dequeue-github.yaml', driver='github')
def test_dequeue_pull_synchronized(self):
self.executor_server.hold_jobs_in_build = True
diff --git a/zuul/driver/github/githubconnection.py b/zuul/driver/github/githubconnection.py
index d715222..b7fb05d 100644
--- a/zuul/driver/github/githubconnection.py
+++ b/zuul/driver/github/githubconnection.py
@@ -143,6 +143,24 @@
event.action = 'comment'
return event
+ def _event_pull_request_review(self, request):
+ """Handles pull request reviews"""
+ body = request.json_body
+ pr_body = body.get('pull_request')
+ if pr_body is None:
+ return
+
+ review = body.get('review')
+ if review is None:
+ return
+
+ event = self._pull_request_to_event(pr_body)
+ event.state = review.get('state')
+ event.account = self._get_sender(body)
+ event.type = 'pull_request_review'
+ event.action = body.get('action')
+ return event
+
def _issue_to_pull_request(self, body):
number = body.get('issue').get('number')
project_name = body.get('repository').get('full_name')
diff --git a/zuul/driver/github/githubtrigger.py b/zuul/driver/github/githubtrigger.py
index 541c783..b9c1026 100644
--- a/zuul/driver/github/githubtrigger.py
+++ b/zuul/driver/github/githubtrigger.py
@@ -40,7 +40,8 @@
refs=toList(trigger.get('ref')),
comments=toList(trigger.get('comment')),
labels=toList(trigger.get('label')),
- unlabels=toList(trigger.get('unlabel'))
+ unlabels=toList(trigger.get('unlabel')),
+ states=toList(trigger.get('state'))
)
efilters.append(f)
@@ -57,6 +58,7 @@
github_trigger = {
v.Required('event'):
toList(v.Any('pull_request',
+ 'pull_request_review',
'push')),
'action': toList(str),
'branch': toList(str),
@@ -64,6 +66,7 @@
'comment': toList(str),
'label': toList(str),
'unlabel': toList(str),
+ 'state': toList(str),
}
return github_trigger
diff --git a/zuul/model.py b/zuul/model.py
index 167706f..8002f16 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1896,6 +1896,7 @@
self.comment = None
self.label = None
self.unlabel = None
+ self.state = None
# ref-updated
self.ref = None
self.oldrev = None
@@ -2047,7 +2048,7 @@
def __init__(self, trigger, types=[], branches=[], refs=[],
event_approvals={}, comments=[], emails=[], usernames=[],
timespecs=[], required_approvals=[], reject_approvals=[],
- pipelines=[], actions=[], labels=[], unlabels=[],
+ pipelines=[], actions=[], labels=[], unlabels=[], states=[],
ignore_deletes=True):
super(EventFilter, self).__init__(
required_approvals=required_approvals,
@@ -2072,6 +2073,7 @@
self.timespecs = timespecs
self.labels = labels
self.unlabels = unlabels
+ self.states = states
self.ignore_deletes = ignore_deletes
def __repr__(self):
@@ -2110,6 +2112,8 @@
ret += ' labels: %s' % ', '.join(self.labels)
if self.unlabels:
ret += ' unlabels: %s' % ', '.join(self.unlabels)
+ if self.states:
+ ret += ' states: %s' % ', '.join(self.states)
ret += '>'
return ret
@@ -2222,6 +2226,10 @@
if self.unlabels and event.unlabel not in self.unlabels:
return False
+ # states are ORed
+ if self.states and event.state not in self.states:
+ return False
+
return True