Adds github triggering from status updates
This adds support for triggering on github status updates. Config schema for the github trigger has been updated to accept a list of statuses, in the "github_user:context:status" format. Change-Id: I15aef35716ddbcd1e66f84a73d27ca2689c936e4 Co-Authored-By: Jesse Keating <omgjlk@us.ibm.com> Signed-off-by: Adam Gandelman <adamg@ubuntu.com>
This commit is contained in:
parent
d96e5887b8
commit
8c6eeb5e8b
|
@ -133,6 +133,8 @@ following options.
|
|||
|
||||
*push* - head reference updated (pushed to branch)
|
||||
|
||||
*status* - status set on commit
|
||||
|
||||
A ``pull_request_review`` event will
|
||||
have associated action(s) to trigger from. The supported actions are:
|
||||
|
||||
|
@ -165,6 +167,12 @@ following options.
|
|||
strings each of which is matched to the review state, which can be one of
|
||||
``approved``, ``comment``, or ``request_changes``.
|
||||
|
||||
**status**
|
||||
This is only used for ``status`` actions. It accepts a list of strings each of
|
||||
which matches the user setting the status, the status context, and the status
|
||||
itself in the format of ``user:context:status``. For example,
|
||||
``zuul_github_ci_bot:check_pipeline:success``.
|
||||
|
||||
**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
|
||||
|
|
|
@ -758,10 +758,9 @@ class FakeGithubPullRequest(object):
|
|||
repo = self._getRepo()
|
||||
return repo.references[self._getPRReference()].commit.hexsha
|
||||
|
||||
def setStatus(self, sha, state, url, description, context):
|
||||
def setStatus(self, sha, state, url, description, context, user='zuul'):
|
||||
# Since we're bypassing github API, which would require a user, we
|
||||
# hard set the user as 'zuul' here.
|
||||
user = 'zuul'
|
||||
# insert the status at the top of the list, to simulate that it
|
||||
# is the most recent set status
|
||||
self.statuses[sha].insert(0, ({
|
||||
|
@ -805,6 +804,21 @@ class FakeGithubPullRequest(object):
|
|||
}
|
||||
return (name, data)
|
||||
|
||||
def getCommitStatusEvent(self, context, state='success', user='zuul'):
|
||||
name = 'status'
|
||||
data = {
|
||||
'state': state,
|
||||
'sha': self.head_sha,
|
||||
'description': 'Test results for %s: %s' % (self.head_sha, state),
|
||||
'target_url': 'http://zuul/%s' % self.head_sha,
|
||||
'branches': [],
|
||||
'context': context,
|
||||
'sender': {
|
||||
'login': user
|
||||
}
|
||||
}
|
||||
return (name, data)
|
||||
|
||||
|
||||
class FakeGithubConnection(githubconnection.GithubConnection):
|
||||
log = logging.getLogger("zuul.test.FakeGithubConnection")
|
||||
|
@ -878,6 +892,13 @@ class FakeGithubConnection(githubconnection.GithubConnection):
|
|||
}
|
||||
return data
|
||||
|
||||
def getPullBySha(self, sha):
|
||||
prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
|
||||
if len(prs) > 1:
|
||||
raise Exception('Multiple pulls found with head sha: %s' % sha)
|
||||
pr = prs[0]
|
||||
return self.getPull(pr.project, pr.number)
|
||||
|
||||
def getPullFileNames(self, project, number):
|
||||
pr = self.pull_requests[number - 1]
|
||||
return pr.files
|
||||
|
|
|
@ -13,11 +13,34 @@
|
|||
github:
|
||||
comment: true
|
||||
|
||||
- pipeline:
|
||||
name: trigger
|
||||
manager: independent
|
||||
trigger:
|
||||
github:
|
||||
- event: pull_request
|
||||
action: status
|
||||
status: 'zuul:check:success'
|
||||
success:
|
||||
github:
|
||||
status: 'success'
|
||||
failure:
|
||||
github:
|
||||
status: 'failure'
|
||||
|
||||
- job:
|
||||
name: project1-pipeline
|
||||
- job:
|
||||
name: project2-trigger
|
||||
|
||||
- project:
|
||||
name: org/project1
|
||||
pipeline:
|
||||
jobs:
|
||||
- project1-pipeline
|
||||
|
||||
- project:
|
||||
name: org/project2
|
||||
trigger:
|
||||
jobs:
|
||||
- project2-trigger
|
||||
|
|
|
@ -43,3 +43,39 @@ class TestGithubRequirements(ZuulTestCase):
|
|||
self.waitUntilSettled()
|
||||
self.assertEqual(len(self.history), 1)
|
||||
self.assertEqual(self.history[0].name, 'project1-pipeline')
|
||||
|
||||
@simple_layout('layouts/requirements-github.yaml', driver='github')
|
||||
def test_trigger_require_status(self):
|
||||
"Test trigger requirement: status"
|
||||
A = self.fake_github.openFakePullRequest('org/project2', 'master', 'A')
|
||||
|
||||
# An error status should not cause it to be enqueued
|
||||
A.setStatus(A.head_sha, 'error', 'null', 'null', 'check')
|
||||
self.fake_github.emitEvent(A.getCommitStatusEvent('check',
|
||||
state='error'))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(len(self.history), 0)
|
||||
|
||||
# An success status from unknown user should not cause it to be
|
||||
# enqueued
|
||||
A.setStatus(A.head_sha, 'success', 'null', 'null', 'check', user='foo')
|
||||
self.fake_github.emitEvent(A.getCommitStatusEvent('check',
|
||||
state='success',
|
||||
user='foo'))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(len(self.history), 0)
|
||||
|
||||
# A success status goes in
|
||||
A.setStatus(A.head_sha, 'success', 'null', 'null', 'check')
|
||||
self.fake_github.emitEvent(A.getCommitStatusEvent('check'))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(len(self.history), 1)
|
||||
self.assertEqual(self.history[0].name, 'project2-trigger')
|
||||
|
||||
# An error status for a different context should not cause it to be
|
||||
# enqueued
|
||||
A.setStatus(A.head_sha, 'error', 'null', 'null', 'gate')
|
||||
self.fake_github.emitEvent(A.getCommitStatusEvent('gate',
|
||||
state='error'))
|
||||
self.waitUntilSettled()
|
||||
self.assertEqual(len(self.history), 1)
|
||||
|
|
|
@ -162,6 +162,26 @@ class GithubWebhookListener():
|
|||
event.action = body.get('action')
|
||||
return event
|
||||
|
||||
def _event_status(self, request):
|
||||
body = request.json_body
|
||||
action = body.get('action')
|
||||
if action == 'pending':
|
||||
return
|
||||
pr_body = self.connection.getPullBySha(body['sha'])
|
||||
if pr_body is None:
|
||||
return
|
||||
|
||||
event = self._pull_request_to_event(pr_body)
|
||||
event.account = self._get_sender(body)
|
||||
event.type = 'pull_request'
|
||||
event.action = 'status'
|
||||
# Github API is silly. Webhook blob sets author data in
|
||||
# 'sender', but API call to get status puts it in 'creator'.
|
||||
# Duplicate the data so our code can look in one place
|
||||
body['creator'] = body['sender']
|
||||
event.status = "%s:%s:%s" % _status_as_tuple(body)
|
||||
return event
|
||||
|
||||
def _issue_to_pull_request(self, body):
|
||||
number = body.get('issue').get('number')
|
||||
project_name = body.get('repository').get('full_name')
|
||||
|
@ -377,6 +397,30 @@ class GithubConnection(BaseConnection):
|
|||
# For now, just send back a True value.
|
||||
return True
|
||||
|
||||
def getPullBySha(self, sha):
|
||||
query = '%s type:pr is:open' % sha
|
||||
pulls = []
|
||||
for issue in self.github.search_issues(query=query):
|
||||
pr_url = issue.pull_request.get('url')
|
||||
if not pr_url:
|
||||
continue
|
||||
# the issue provides no good description of the project :\
|
||||
owner, project, _, number = pr_url.split('/')[4:]
|
||||
pr = self.github.pull_request(owner, project, number)
|
||||
if pr.head.sha != sha:
|
||||
continue
|
||||
if pr.as_dict() in pulls:
|
||||
continue
|
||||
pulls.append(pr.as_dict())
|
||||
|
||||
log_rate_limit(self.log, self.github)
|
||||
if len(pulls) > 1:
|
||||
raise Exception('Multiple pulls found with head sha %s' % sha)
|
||||
|
||||
if len(pulls) == 0:
|
||||
return None
|
||||
return pulls.pop()
|
||||
|
||||
def getPullFileNames(self, project, number):
|
||||
owner, proj = project.name.split('/')
|
||||
filenames = [f.filename for f in
|
||||
|
@ -453,20 +497,27 @@ class GithubConnection(BaseConnection):
|
|||
seen = []
|
||||
statuses = []
|
||||
for status in self.getCommitStatuses(project.name, sha):
|
||||
# creator can be None if the user has been removed.
|
||||
creator = status.get('creator')
|
||||
if not creator:
|
||||
continue
|
||||
user = creator.get('login')
|
||||
context = status.get('context')
|
||||
state = status.get('state')
|
||||
if "%s:%s" % (user, context) not in seen:
|
||||
statuses.append("%s:%s:%s" % (user, context, state))
|
||||
seen.append("%s:%s" % (user, context))
|
||||
stuple = _status_as_tuple(status)
|
||||
if "%s:%s" % (stuple[0], stuple[1]) not in seen:
|
||||
statuses.append("%s:%s:%s" % stuple)
|
||||
seen.append("%s:%s" % (stuple[0], stuple[1]))
|
||||
|
||||
return statuses
|
||||
|
||||
|
||||
def _status_as_tuple(status):
|
||||
"""Translate a status into a tuple of user, context, state"""
|
||||
|
||||
creator = status.get('creator')
|
||||
if not creator:
|
||||
user = "Unknown"
|
||||
else:
|
||||
user = creator.get('login')
|
||||
context = status.get('context')
|
||||
state = status.get('state')
|
||||
return (user, context, state)
|
||||
|
||||
|
||||
def log_rate_limit(log, github):
|
||||
try:
|
||||
rate_limit = github.rate_limit()
|
||||
|
|
|
@ -58,7 +58,7 @@ class GithubTriggerEvent(TriggerEvent):
|
|||
class GithubEventFilter(EventFilter):
|
||||
def __init__(self, trigger, types=[], branches=[], refs=[],
|
||||
comments=[], actions=[], labels=[], unlabels=[],
|
||||
states=[], ignore_deletes=True):
|
||||
states=[], statuses=[], ignore_deletes=True):
|
||||
|
||||
EventFilter.__init__(self, trigger)
|
||||
|
||||
|
@ -74,6 +74,7 @@ class GithubEventFilter(EventFilter):
|
|||
self.labels = labels
|
||||
self.unlabels = unlabels
|
||||
self.states = states
|
||||
self.statuses = statuses
|
||||
self.ignore_deletes = ignore_deletes
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -97,6 +98,8 @@ class GithubEventFilter(EventFilter):
|
|||
ret += ' unlabels: %s' % ', '.join(self.unlabels)
|
||||
if self.states:
|
||||
ret += ' states: %s' % ', '.join(self.states)
|
||||
if self.statuses:
|
||||
ret += ' statuses: %s' % ', '.join(self.statuses)
|
||||
ret += '>'
|
||||
|
||||
return ret
|
||||
|
@ -160,6 +163,10 @@ class GithubEventFilter(EventFilter):
|
|||
if self.states and event.state not in self.states:
|
||||
return False
|
||||
|
||||
# statuses are ORed
|
||||
if self.statuses and event.status not in self.statuses:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -41,7 +41,8 @@ class GithubTrigger(BaseTrigger):
|
|||
comments=toList(trigger.get('comment')),
|
||||
labels=toList(trigger.get('label')),
|
||||
unlabels=toList(trigger.get('unlabel')),
|
||||
states=toList(trigger.get('state'))
|
||||
states=toList(trigger.get('state')),
|
||||
statuses=toList(trigger.get('status'))
|
||||
)
|
||||
efilters.append(f)
|
||||
|
||||
|
@ -67,6 +68,7 @@ def getSchema():
|
|||
'label': toList(str),
|
||||
'unlabel': toList(str),
|
||||
'state': toList(str),
|
||||
'status': toList(str)
|
||||
}
|
||||
|
||||
return github_trigger
|
||||
|
|
Loading…
Reference in New Issue