Pagure - handle Pull Request tags (labels) metadata

This change implements event handling for pull-request.tags.added.
Tags can be used as trigger event filter or required metadata.

Change-Id: I128bbef34245932e3bbee1f848ad1c484d3ccae3
This commit is contained in:
Fabien Boucher 2019-09-09 17:32:01 +02:00
parent 3c2b5c6ed2
commit ae42bbe735
9 changed files with 185 additions and 13 deletions

View File

@ -109,6 +109,10 @@ the following options.
Status set on pull request.
.. value:: tagged
Tag metadata set on pull request.
A :value:`pipeline.trigger.<pagure
source>.event.pg_pull_request_review` event will have associated
action(s) to trigger from. The supported actions are:
@ -123,7 +127,7 @@ the following options.
.. attr:: comment
This is only used for ``pg_pull_request`` ``comment`` actions. It
This is only used for ``pg_pull_request`` and ``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
@ -137,6 +141,12 @@ the following options.
the status, the status context, and the status itself in the
format of ``status``. For example, ``success`` or ``failure``.
.. attr:: tag
This is used for ``pg_pull_request`` and ``tagged`` actions. It
accepts a list of strings and if one of them is part of the
event tags metadata then the trigger is matched.
.. attr:: ref
This is only used for ``pg_push`` events. This field is treated as
@ -199,6 +209,8 @@ configuration such as the following:
score: 1
merged: false
status: success
tags:
- gateit
This indicates that changes originating from the Pagure connection
must have a score of *1*, a CI status *success* and not being already merged.
@ -226,6 +238,11 @@ must have a score of *1*, a CI status *success* and not being already merged.
A boolean value (``true`` or ``false``) that indicates whether
the Pull Request must be open or closed in order to be enqueued.
.. attr:: tags
if present, the list of tags a Pull Request must have.
Reference pipelines configuration
---------------------------------

View File

@ -35,6 +35,7 @@
pagure.io:
score: 1
merged: False
tags: gateit
status: success
sqlreporter:
trigger:
@ -44,6 +45,9 @@
status: success
- event: pg_pull_request_review
action: thumbsup
- event: pg_pull_request
action: tagged
tag: gateit
start:
pagure.io:
status: 'pending'

View File

@ -827,6 +827,7 @@ class FakePagurePullRequest(object):
self.comments = []
self.flags = []
self.files = {}
self.tags = []
self.cached_merge_status = ''
self.threshold_reached = False
self.commit_stop = None
@ -841,16 +842,17 @@ class FakePagurePullRequest(object):
self._addCommitInPR(files=files)
self._updateTimeStamp()
def _getPullRequestEvent(self, action):
def _getPullRequestEvent(self, action, pull_data_field='pullrequest'):
name = 'pg_pull_request'
data = {
'msg': {
'pullrequest': {
pull_data_field: {
'branch': self.branch,
'comments': self.comments,
'commit_start': self.commit_start,
'commit_stop': self.commit_stop,
'date_created': '0',
'tags': self.tags,
'initial_comment': self.initial_comment,
'id': self.number,
'project': {
@ -867,6 +869,8 @@ class FakePagurePullRequest(object):
}
if action == 'pull-request.flag.added':
data['msg']['flag'] = self.flags[0]
if action == 'pull-request.tag.added':
data['msg']['tags'] = self.tags
return (name, data)
def getPullRequestOpenedEvent(self):
@ -888,6 +892,20 @@ class FakePagurePullRequest(object):
self._updateTimeStamp()
return self._getPullRequestEvent('pull-request.initial_comment.edited')
def getPullRequestTagAddedEvent(self, tags, reset=True):
if reset:
self.tags = []
_tags = set(self.tags)
_tags.update(set(tags))
self.tags = list(_tags)
self.addComment(
"**Metadata Update from @pingou**:\n- " +
"Pull-request tagged with: %s" % ', '.join(tags),
True)
self._updateTimeStamp()
return self._getPullRequestEvent(
'pull-request.tag.added', pull_data_field='pull_request')
def getPullRequestStatusSetEvent(self, status):
self.addFlag(
status, "https://url", "Build %s" % status)
@ -1014,7 +1032,8 @@ class FakePagureAPIClient(pagureconnection.PagureAPIClient):
'comments': pr.comments,
'commit_stop': pr.commit_stop,
'threshold_reached': pr.threshold_reached,
'cached_merge_status': pr.cached_merge_status
'cached_merge_status': pr.cached_merge_status,
'tags': pr.tags,
}, 200, "", "GET"
match = re.match(r'.+/api/0/(.+)/pull-request/(\d+)/flag$', url)

View File

@ -38,6 +38,34 @@
pagure:
status: 'success'
- pipeline:
name: trigger-tag
manager: independent
trigger:
pagure:
- event: pg_pull_request
action: tagged
tag:
- gateit
- mergeit
success:
pagure:
status: 'success'
- pipeline:
name: require-tag
manager: independent
require:
pagure:
tags: gateit
trigger:
pagure:
- event: pg_pull_request
action: changed
success:
pagure:
status: 'success'
- job:
name: base
parent: null
@ -64,3 +92,15 @@
trigger-flag:
jobs:
- project-test
- project:
name: org/project4
trigger-tag:
jobs:
- project-test
- project:
name: org/project5
require-tag:
jobs:
- project-test

View File

@ -360,6 +360,52 @@ class TestPagureDriver(ZuulTestCase):
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
@simple_layout('layouts/requirements-pagure.yaml', driver='pagure')
def test_tag_trigger(self):
A = self.fake_pagure.openFakePullRequest(
'org/project4', 'master', 'A')
self.fake_pagure.emitEvent(
A.getPullRequestTagAddedEvent(["lambda"]))
self.waitUntilSettled()
self.assertEqual(0, len(self.history))
self.fake_pagure.emitEvent(
A.getPullRequestTagAddedEvent(["gateit", "lambda"]))
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
self.fake_pagure.emitEvent(
A.getPullRequestTagAddedEvent(["mergeit"]))
self.waitUntilSettled()
self.assertEqual(2, len(self.history))
@simple_layout('layouts/requirements-pagure.yaml', driver='pagure')
def test_tag_require(self):
A = self.fake_pagure.openFakePullRequest(
'org/project5', 'master', 'A')
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
self.waitUntilSettled()
self.assertEqual(0, len(self.history))
A.tags = ["lambda"]
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
self.waitUntilSettled()
self.assertEqual(0, len(self.history))
A.tags = ["lambda", "gateit"]
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
A.tags = []
self.fake_pagure.emitEvent(A.getPullRequestUpdatedEvent())
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
@simple_layout('layouts/merging-pagure.yaml', driver='pagure')
def test_merge_action_in_independent(self):

View File

@ -18,6 +18,7 @@ import hashlib
import queue
import threading
import time
import re
import json
import requests
import cherrypy
@ -194,13 +195,17 @@ class PagureEventConnector(threading.Thread):
self.daemon = True
self.connection = connection
self._stopped = False
self.metadata_notif = re.compile(
r"^\*\*Metadata Update", re.MULTILINE)
self.event_handler_mapping = {
'pull-request.comment.added': self._event_issue_comment,
'pull-request.new': self._event_pull_request,
'pull-request.flag.added': self._event_flag_added,
'git.receive': self._event_ref_updated,
'pull-request.initial_comment.edited':
self._event_issue_initial_comment
self._event_issue_initial_comment,
'pull-request.tag.added':
self._event_pull_request_tags_changed
}
def stop(self):
@ -244,16 +249,19 @@ class PagureEventConnector(threading.Thread):
self.connection.logEvent(event)
self.connection.sched.addEvent(event)
def _event_base(self, body):
def _event_base(self, body, pull_data_field='pullrequest'):
event = PagureTriggerEvent()
if 'pullrequest' in body['msg']:
data = body['msg']['pullrequest']
if pull_data_field in body['msg']:
data = body['msg'][pull_data_field]
data['tags'] = body['msg'].get('tags', [])
data['flag'] = body['msg'].get('flag')
event.title = data.get('title')
event.project_name = data.get('project', {}).get('fullname')
event.change_number = data.get('id')
event.updated_at = data.get('date_created')
event.branch = data.get('branch')
event.tags = data.get('tags', [])
event.change_url = self.connection.getPullUrl(event.project_name,
event.change_number)
event.ref = "refs/pull/%s/head" % event.change_number
@ -271,12 +279,21 @@ class PagureEventConnector(threading.Thread):
event.action = 'changed'
return event
def _event_pull_request_tags_changed(self, body):
""" Handles pull request metadata change """
# pull-request.tag.added/removed use pull_request in payload body
event, _ = self._event_base(body, pull_data_field='pull_request')
event.action = 'tagged'
return event
def _event_issue_comment(self, body):
""" Handles pull request comments """
# https://fedora-fedmsg.readthedocs.io/en/latest/topics.html#pagure-pull-request-comment-added
event, data = self._event_base(body)
last_comment = data.get('comments', [])[-1]
if last_comment.get('notification') is True:
if (last_comment.get('notification') is True and
not self.metadata_notif.match(
last_comment.get('comment', ''))):
# An updated PR (new commits) triggers the comment.added
# event. A message is added by pagure on the PR but notification
# is set to true.
@ -556,7 +573,8 @@ class PagureConnection(BaseConnection):
self.connectors = {}
self.source = driver.getSource(self)
self.event_queue = queue.Queue()
self.metadata_notif = re.compile(
r"^\*\*Metadata Update", re.MULTILINE)
self.sched = None
def onLoad(self):
@ -793,6 +811,9 @@ class PagureConnection(BaseConnection):
for comment in pr.get('comments', []):
# PR updated are reported as comment but with the notification flag
if comment['notification']:
# Ignore metadata update such as assignee and tags
if self.metadata_notif.match(comment.get('comment', '')):
continue
date = int(comment['date_created'])
if date > last_pr_code_updated:
last_pr_code_updated = date
@ -822,6 +843,7 @@ class PagureConnection(BaseConnection):
change.patchset = change.pr.get('commit_stop')
change.files = change.pr.get('files')
change.title = change.pr.get('title')
change.tags = change.pr.get('tags')
change.open = change.pr.get('status') == 'Open'
change.is_merged = change.pr.get('status') == 'Merged'
change.status = self.getStatus(change.project, change.number)

View File

@ -27,6 +27,7 @@ class PullRequest(Change):
self.title = None
self.score = 0
self.files = []
self.tags = []
def __repr__(self):
r = ['<Change 0x%x' % id(self)]
@ -42,6 +43,8 @@ class PullRequest(Change):
r.append('status: %s' % self.status)
if self.score:
r.append('score: %s' % self.score)
if self.tags:
r.append('tags: %s' % ', '.join(self.tags))
if self.is_merged:
r.append('state: merged')
if self.open:
@ -64,6 +67,7 @@ class PagureTriggerEvent(TriggerEvent):
self.title = None
self.action = None
self.status = None
self.tags = []
def _repr(self):
r = [super(PagureTriggerEvent, self)._repr()]
@ -74,6 +78,8 @@ class PagureTriggerEvent(TriggerEvent):
r.append("project:%s" % self.canonical_project_name)
if self.change_number:
r.append("pr:%s" % self.change_number)
if self.tags:
r.append("tags:%s" % ', '.join(self.tags))
return ' '.join(r)
def isPatchsetCreated(self):
@ -84,7 +90,7 @@ class PagureTriggerEvent(TriggerEvent):
class PagureEventFilter(EventFilter):
def __init__(self, trigger, types=[], refs=[], statuses=[],
comments=[], actions=[], ignore_deletes=True):
comments=[], actions=[], tags=[], ignore_deletes=True):
EventFilter.__init__(self, trigger)
@ -96,6 +102,7 @@ class PagureEventFilter(EventFilter):
self.comments = [re.compile(x) for x in comments]
self.actions = actions
self.statuses = statuses
self.tags = tags
self.ignore_deletes = ignore_deletes
def __repr__(self):
@ -113,6 +120,8 @@ class PagureEventFilter(EventFilter):
ret += ' actions: %s' % ', '.join(self.actions)
if self.statuses:
ret += ' statuses: %s' % ', '.join(self.statuses)
if self.tags:
ret += ' tags: %s' % ', '.join(self.tags)
ret += '>'
return ret
@ -159,6 +168,10 @@ class PagureEventFilter(EventFilter):
if self.statuses and not matches_status:
return False
if self.tags:
if not set(event.tags).intersection(set(self.tags)):
return False
return True
@ -166,12 +179,13 @@ class PagureEventFilter(EventFilter):
# pipeline requires definition)
class PagureRefFilter(RefFilter):
def __init__(self, connection_name, score=None,
open=None, merged=None, status=None):
open=None, merged=None, status=None, tags=[]):
RefFilter.__init__(self, connection_name)
self.score = score
self.open = open
self.merged = merged
self.status = status
self.tags = tags
def __repr__(self):
ret = '<PagureRefFilter connection_name: %s ' % self.connection_name
@ -183,6 +197,8 @@ class PagureRefFilter(RefFilter):
ret += ' merged: %s' % self.merged
if self.status is not None:
ret += ' status: %s' % self.status
if self.tags:
ret += ' tags: %s' % ', '.join(self.tags)
ret += '>'
return ret
@ -203,4 +219,8 @@ class PagureRefFilter(RefFilter):
if change.status != self.status:
return False
if self.tags:
if not set(change.tags).intersection(set(self.tags)):
return False
return True

View File

@ -20,6 +20,7 @@ from zuul.source import BaseSource
from zuul.model import Project
from zuul.driver.pagure.paguremodel import PagureRefFilter
from zuul.driver.util import scalar_or_list, to_list
class PagureSource(BaseSource):
@ -117,7 +118,6 @@ class PagureSource(BaseSource):
"""Get the git-web url for a project."""
raise NotImplementedError()
# This driver does not implement pipeline requirements.
def getRequireFilters(self, config):
f = PagureRefFilter(
connection_name=self.connection.connection_name,
@ -125,6 +125,7 @@ class PagureSource(BaseSource):
open=config.get('open'),
merged=config.get('merged'),
status=config.get('status'),
tags=to_list(config.get('tags'))
)
return [f]
@ -142,6 +143,7 @@ def getRequireSchema():
'open': bool,
'merged': bool,
'status': str,
'tags': scalar_or_list(str)
}
return require

View File

@ -33,6 +33,7 @@ class PagureTrigger(BaseTrigger):
refs=to_list(trigger.get('ref')),
comments=to_list(trigger.get('comment')),
statuses=to_list(trigger.get('status')),
tags=to_list(trigger.get('tag')),
)
efilters.append(f)
@ -56,6 +57,7 @@ def getSchema():
'ref': scalar_or_list(str),
'comment': scalar_or_list(str),
'status': scalar_or_list(str),
'tag': scalar_or_list(str)
}
return pagure_trigger