Merge pull requests from github reporter

Github reporter can be configured to merge pull reqeusts.

When there are multiple merges called at the same time, it leads to a
situation when github returns 405 MethodNotAllowed error becuase github
is checking the branch mergeability.
When we encounter this situation, we try to wait a bit (2 seconds for
now) and try to merge again.

Pre-release version of Github3.py has to be used, because the latest
released version 9.4 has a bug in merge method. Furthermore the newest
merge method supports specifying exact sha to be merged, which is
desirable to ensure that the exact commit that went through the pipeline
gets merged.

Both are already fixed in the stable branch, but not yet released on
PyPi. See:
90c6b7c265
6ef02cb33f

Change-Id: I0c3abbcce476774a5ba8981c171382eaa4fe0abf
This commit is contained in:
Jan Hruban 2015-11-03 11:45:46 +01:00 committed by Jesse Keating
parent e252a73a46
commit 49bff07ec8
7 changed files with 106 additions and 2 deletions

View File

@ -50,6 +50,11 @@ reporter. It has the following options:
to ``true``.
``comment: false``
**merge**
Boolean value (``true`` or ``false``) that determines if the reporter should
merge the pull reqeust. Defaults to ``false``.
``merge=true``
SMTP
----

View File

@ -1,6 +1,6 @@
pbr>=1.1.0
Github3.py
Github3.py==1.0.0a2
PyYAML>=3.1.0
Paste
WebOb>=1.2.3

View File

@ -70,6 +70,7 @@ import zuul.merger.merger
import zuul.merger.server
import zuul.nodepool
import zuul.zk
from zuul.exceptions import MergeFailure
FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
'fixtures')
@ -559,6 +560,7 @@ class FakeGithubPullRequest(object):
self.statuses = {}
self.updated_at = None
self.head_sha = None
self.is_merged = False
self._createPRRef()
self._addCommitToRepo()
self._updateTimeStamp()
@ -693,6 +695,8 @@ class FakeGithubConnection(githubconnection.GithubConnection):
self.pr_number = 0
self.pull_requests = []
self.upstream_root = upstream_root
self.merge_failure = False
self.merge_not_allowed_count = 0
def openFakePullRequest(self, project, branch):
self.pr_number += 1
@ -762,6 +766,16 @@ class FakeGithubConnection(githubconnection.GithubConnection):
pull_request = self.pull_requests[pr_number - 1]
pull_request.addComment(message)
def mergePull(self, project, pr_number, sha=None):
pull_request = self.pull_requests[pr_number - 1]
if self.merge_failure:
raise Exception('Pull request was not merged')
if self.merge_not_allowed_count > 0:
self.merge_not_allowed_count -= 1
raise MergeFailure('Merge was not successful due to mergeability'
' conflict')
pull_request.is_merged = True
def setCommitStatus(self, project, sha, state,
url='', description='', context=''):
owner, proj = project.split('/')

View File

@ -0,0 +1,19 @@
- pipeline:
name: merge
description: Pipeline for merging the pull request
manager: independent
trigger:
github:
- event: pull_request
action: comment
comment: 'merge me'
success:
github:
merge: true
comment: false
- project:
name: org/project
merge:
jobs:
- noop

View File

@ -165,3 +165,35 @@ class TestGithubDriver(ZuulTestCase):
self.waitUntilSettled()
self.assertNotIn('reporting', pr.statuses)
self.assertEqual(2, len(pr.comments))
@simple_layout('layouts/merging-github.yaml', driver='github')
def test_report_pull_merge(self):
# pipeline merges the pull request on success
A = self.fake_github.openFakePullRequest('org/project', 'master')
self.fake_github.emitEvent(A.getCommentAddedEvent('merge me'))
self.waitUntilSettled()
self.assertTrue(A.is_merged)
# pipeline merges the pull request on success after failure
self.fake_github.merge_failure = True
B = self.fake_github.openFakePullRequest('org/project', 'master')
self.fake_github.emitEvent(B.getCommentAddedEvent('merge me'))
self.waitUntilSettled()
self.assertFalse(B.is_merged)
self.fake_github.merge_failure = False
# pipeline merges the pull request on second run of merge
# first merge failed on 405 Method Not Allowed error
self.fake_github.merge_not_allowed_count = 1
C = self.fake_github.openFakePullRequest('org/project', 'master')
self.fake_github.emitEvent(C.getCommentAddedEvent('merge me'))
self.waitUntilSettled()
self.assertTrue(C.is_merged)
# pipeline does not merge the pull request
# merge failed on 405 Method Not Allowed error - twice
self.fake_github.merge_not_allowed_count = 2
D = self.fake_github.openFakePullRequest('org/project', 'master')
self.fake_github.emitEvent(D.getCommentAddedEvent('merge me'))
self.waitUntilSettled()
self.assertFalse(D.is_merged)

View File

@ -21,9 +21,11 @@ import webob
import webob.dec
import voluptuous as v
import github3
from github3.exceptions import MethodNotAllowed
from zuul.connection import BaseConnection
from zuul.model import PullRequest, Ref, TriggerEvent
from zuul.exceptions import MergeFailure
class GithubWebhookListener():
@ -283,6 +285,17 @@ class GithubConnection(BaseConnection):
pull_request = repository.issue(pr_number)
pull_request.create_comment(message)
def mergePull(self, project, pr_number, sha=None):
owner, proj = project.split('/')
pull_request = self.github.pull_request(owner, proj, pr_number)
try:
result = pull_request.merge(sha=sha)
except MethodNotAllowed as e:
raise MergeFailure('Merge was not successful due to mergeability'
' conflict, original error is %s' % e)
if not result:
raise Exception('Pull request was not merged')
def setCommitStatus(self, project, sha, state, url='', description='',
context=''):
owner, proj = project.split('/')

View File

@ -14,8 +14,10 @@
import logging
import voluptuous as v
import time
from zuul.reporter import BaseReporter
from zuul.exceptions import MergeFailure
class GithubReporter(BaseReporter):
@ -28,6 +30,7 @@ class GithubReporter(BaseReporter):
super(GithubReporter, self).__init__(driver, connection, config)
self._commit_status = self.config.get('status', None)
self._create_comment = self.config.get('comment', True)
self._merge = self.config.get('merge', False)
def report(self, source, pipeline, item):
"""Comment on PR and set commit status."""
@ -37,6 +40,9 @@ class GithubReporter(BaseReporter):
hasattr(item.change, 'patchset') and
item.change.patchset is not None):
self.setPullStatus(pipeline, item)
if (self._merge and
hasattr(item.change, 'number')):
self.mergePull(item)
def addPullComment(self, pipeline, item):
message = self._formatItemReport(pipeline, item)
@ -68,10 +74,25 @@ class GithubReporter(BaseReporter):
self.connection.setCommitStatus(
project, sha, state, url, description, context)
def mergePull(self, item):
project = item.change.project.name
pr_number = item.change.number
sha = item.change.patchset
self.log.debug('Reporting change %s, params %s, merging via API' %
(item.change, self.config))
try:
self.connection.mergePull(project, pr_number, sha)
except MergeFailure:
time.sleep(2)
self.log.debug('Trying to merge change %s again...' % item.change)
self.connection.mergePull(project, pr_number, sha)
item.change.is_merged = True
def getSchema():
github_reporter = v.Schema({
'status': v.Any('pending', 'success', 'failure'),
'comment': bool
'comment': bool,
'merge': bool
})
return github_reporter