Add support for adding and removing labels in gitlab

Also add support for triggering on label removal (addition is already
supported).

Change-Id: I2c65a53a5df66704f1621c208a2282d63d0f8074
This commit is contained in:
James E. Blair 2021-11-17 15:25:15 -08:00 committed by Niklas Borg
parent 9903b0b017
commit 56865e57d2
11 changed files with 169 additions and 14 deletions

View File

@ -190,9 +190,15 @@ the following options.
.. attr:: labels
This is only used for ``gl_merge_request`` and ``labeled`` actions. It
accepts a string or a list of strings that are searched into the list
of labels set to the merge request.
This is only used for ``gl_merge_request`` and ``labeled``
actions. It accepts a string or a list of strings that are that
must have been added for the event to match.
.. attr:: unlabels
This is only used for ``gl_merge_request`` and ``labeled``
actions. It accepts a string or a list of strings that are that
must have been removed for the event to match.
.. attr:: ref
@ -235,6 +241,16 @@ is taken from the pipeline.
*Maintainer* project's member. In case of *developer*, the *Allowed to merge*
setting in *protected branches* must be set to *Developers + Maintainers*.
.. attr:: label
A string or list of strings, each representing a label name
which should be added to the merge request.
.. attr:: unlabel
A string or list of strings, each representing a label name
which should be removed from the merge request.
Requirements Configuration
--------------------------
@ -275,7 +291,7 @@ in the *opened* state (not merged yet).
.. attr:: labels
if present, the list of labels a Merge Request must have.
A list of labels a Merge Request must have in order to be enqueued.
Reference pipelines configuration

View File

@ -0,0 +1,6 @@
---
features:
- |
Added support for adding and removing merge request labels in the
GitLab driver, as well as triggering pipelines on label removal
(label addition was already supported).

View File

@ -2127,7 +2127,7 @@ class FakeGitlabMergeRequest(object):
def _updateTimeStamp(self):
self.updated_at = datetime.datetime.now(datetime.timezone.utc)
def getMergeRequestEvent(self, action, include_labels=False):
def getMergeRequestEvent(self, action, previous_labels=None):
name = 'gl_merge_request'
data = {
'object_kind': 'merge_request',
@ -2149,9 +2149,9 @@ class FakeGitlabMergeRequest(object):
data['labels'] = [{'title': label} for label in self.labels]
data['changes'] = {}
if include_labels:
if previous_labels is not None:
data['changes']['labels'] = {
'previous': [],
'previous': [{'title': label} for label in previous_labels],
'current': data['labels']
}
return (name, data)
@ -2175,9 +2175,14 @@ class FakeGitlabMergeRequest(object):
self.approved = False
return self.getMergeRequestEvent(action='unapproved')
def getMergeRequestLabeledEvent(self, labels):
self.labels = labels
return self.getMergeRequestEvent(action='update', include_labels=True)
def getMergeRequestLabeledEvent(self, add_labels=[], remove_labels=[]):
previous_labels = self.labels
labels = set(previous_labels)
labels = labels - set(remove_labels)
labels = labels | set(add_labels)
self.labels = list(labels)
return self.getMergeRequestEvent(action='update',
previous_labels=previous_labels)
def getMergeRequestCommentedEvent(self, note):
self.addNote(note)

View File

@ -64,6 +64,8 @@ class GitlabWebServer(object):
mr_merge_re = re.compile(r'.+/projects/(?P<project>.+)/'
r'merge_requests/(?P<mr>\d+)/merge$')
mr_update_re = re.compile(r'.+/projects/(?P<project>.+)/'
r'merge_requests/(?P<mr>\d+)$')
def _get_mr(self, project, number):
project = urllib.parse.unquote(project)
@ -129,6 +131,9 @@ class GitlabWebServer(object):
m = self.mr_merge_re.match(path)
if m:
return self.put_mr_merge(data, **m.groupdict())
m = self.mr_update_re.match(path)
if m:
return self.put_mr_update(data, **m.groupdict())
self.send_response(500)
self.end_headers()
@ -231,6 +236,16 @@ class GitlabWebServer(object):
mr.mergeMergeRequest()
self.send_data({'state': 'merged'})
def put_mr_update(self, data, project, mr):
mr = self._get_mr(project, mr)
labels = set(mr.labels)
add_labels = data.get('add_labels', [''])[0].split(',')
remove_labels = data.get('remove_labels', [''])[0].split(',')
labels = labels - set(remove_labels)
labels = labels | set(add_labels)
mr.labels = list(labels)
self.send_data({})
def log_message(self, fmt, *args):
self.log.debug(fmt, *args)

View File

@ -42,6 +42,11 @@
- labeled
labels:
- gateit
- event: gl_merge_request
action:
- labeled
unlabels:
- verified
- pipeline:
name: promote

View File

@ -0,0 +1,31 @@
- pipeline:
name: check
manager: independent
trigger:
gitlab:
- event: gl_merge_request
action:
- opened
success:
gitlab:
comment: true
label:
- addme1
- addme2
unlabel:
- removeme1
- removeme2
- job:
name: base
parent: null
run: playbooks/base.yaml
- job:
name: project1-test
- project:
name: org/project1
check:
jobs:
- project1-test

View File

@ -196,15 +196,21 @@ class TestGitlabDriver(ZuulTestCase):
A = self.fake_gitlab.openFakeMergeRequest('org/project', 'master', 'A')
self.fake_gitlab.emitEvent(A.getMergeRequestLabeledEvent(
labels=('label1', 'label2')))
add_labels=('label1', 'label2')))
self.waitUntilSettled()
self.assertEqual(0, len(self.history))
self.fake_gitlab.emitEvent(A.getMergeRequestLabeledEvent(
labels=('gateit', )))
add_labels=('gateit', )))
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
A.labels = ['verified']
self.fake_gitlab.emitEvent(A.getMergeRequestLabeledEvent(
remove_labels=('verified', )))
self.waitUntilSettled()
self.assertEqual(2, len(self.history))
@simple_layout('layouts/basic-gitlab.yaml', driver='gitlab')
def test_merge_request_merged(self):
@ -639,6 +645,18 @@ class TestGitlabDriver(ZuulTestCase):
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
@simple_layout('layouts/gitlab-label-add-remove.yaml', driver='gitlab')
def test_label_add_remove(self):
A = self.fake_gitlab.openFakeMergeRequest(
'org/project1', 'master', 'A')
A.labels = ['removeme1', 'removeme2']
self.fake_gitlab.emitEvent(A.getMergeRequestOpenedEvent())
self.waitUntilSettled()
self.assertEqual(1, len(self.history))
self.assertEqual(set(A.labels), {'addme1', 'addme2'})
@simple_layout('layouts/merging-gitlab.yaml', driver='gitlab')
def test_merge_action_in_independent(self):

View File

@ -156,6 +156,8 @@ class GitlabEventConnector(threading.Thread):
label in body["changes"]["labels"]["current"]]
new_labels = set(current_labels) - set(previous_labels)
event.labels = list(new_labels)
removed_labels = set(previous_labels) - set(current_labels)
event.unlabels = list(removed_labels)
elif attrs['action'] in ('approved', 'unapproved'):
event.action = attrs['action']
else:
@ -443,6 +445,17 @@ class GitlabAPIClient():
raise MergeFailure('Merge request merge failed: %s' % e)
return resp[0]
# https://docs.gitlab.com/ee/api/merge_requests.html#update-mr
def update_mr(self, project_name, number,
zuul_event_id=None,
**params):
path = "/projects/%s/merge_requests/%s" % (
quote_plus(project_name), number)
resp = self.put(self.baseurl + path, params=params,
zuul_event_id=zuul_event_id)
self._manage_error(*resp, zuul_event_id=zuul_event_id)
return resp[0]
class GitlabConnection(ZKChangeCacheMixin, ZKBranchCacheMixin, BaseConnection):
driver_name = 'gitlab'
@ -710,6 +723,16 @@ class GitlabConnection(ZKChangeCacheMixin, ZKBranchCacheMixin, BaseConnection):
project_name, number, method, zuul_event_id=event)
log.info("Merged MR %s#%s", project_name, number)
def updateMRLabels(self, project_name, mr_number, labels, unlabels,
zuul_event_id=None):
log = get_annotated_logger(self.log, zuul_event_id)
self.gl_client.update_mr(
project_name, mr_number, zuul_event_id=zuul_event_id,
add_labels=','.join(labels),
remove_labels=','.join(unlabels))
log.debug("Added labels %s to, and removed labels %s from %s#%s",
labels, unlabels, project_name, mr_number)
class GitlabWebController(BaseWebController):

View File

@ -86,6 +86,7 @@ class GitlabTriggerEvent(TriggerEvent):
self.title = None
self.action = None
self.labels = []
self.unlabels = []
self.change_number = None
self.merge_request_description_changed = None
self.tag = None
@ -96,6 +97,7 @@ class GitlabTriggerEvent(TriggerEvent):
d["title"] = self.title
d["action"] = self.action
d["labels"] = self.labels
d["unlabels"] = self.unlabels
d["change_number"] = self.change_number
d["merge_request_description_changed"] = \
self.merge_request_description_changed
@ -108,6 +110,7 @@ class GitlabTriggerEvent(TriggerEvent):
self.title = d["title"]
self.action = d["action"]
self.labels = d["labels"]
self.unlabels = d.get("unlabels", [])
self.change_number = d["change_number"]
self.merge_request_description_changed = \
d["merge_request_description_changed"]
@ -122,6 +125,8 @@ class GitlabTriggerEvent(TriggerEvent):
r.append("mr:%s" % self.change_number)
if self.labels:
r.append("labels:%s" % ', '.join(self.labels))
if self.unlabels:
r.append("unlabels:%s" % ', '.join(self.unlabels))
return ' '.join(r)
def isPatchsetCreated(self):
@ -136,7 +141,8 @@ class GitlabTriggerEvent(TriggerEvent):
class GitlabEventFilter(EventFilter):
def __init__(
self, connection_name, trigger, types=None, actions=None,
comments=None, refs=None, labels=None, ignore_deletes=True):
comments=None, refs=None, labels=None, unlabels=None,
ignore_deletes=True):
super().__init__(connection_name, trigger)
self._types = types or []
self.types = [re.compile(x) for x in self._types]
@ -146,6 +152,7 @@ class GitlabEventFilter(EventFilter):
self._refs = refs or []
self.refs = [re.compile(x) for x in self._refs]
self.labels = labels or []
self.unlabels = unlabels or []
self.ignore_deletes = ignore_deletes
def __repr__(self):
@ -164,6 +171,8 @@ class GitlabEventFilter(EventFilter):
ret += ' ignore_deletes: %s' % self.ignore_deletes
if self.labels:
ret += ' labels: %s' % ', '.join(self.labels)
if self.unlabels:
ret += ' unlabels: %s' % ', '.join(self.unlabels)
ret += '>'
return ret
@ -210,6 +219,10 @@ class GitlabEventFilter(EventFilter):
if not set(event.labels).intersection(set(self.labels)):
return False
if self.unlabels:
if not set(event.unlabels).intersection(set(self.unlabels)):
return False
return True

View File

@ -21,6 +21,7 @@ from zuul.model import MERGER_MERGE_RESOLVE, MERGER_MERGE, MERGER_MAP, \
MERGER_SQUASH_MERGE
from zuul.lib.logutil import get_annotated_logger
from zuul.driver.gitlab.gitlabsource import GitlabSource
from zuul.driver.util import scalar_or_list
from zuul.exceptions import MergeFailure
@ -42,6 +43,12 @@ class GitlabReporter(BaseReporter):
self._create_comment = self.config.get('comment', True)
self._approval = self.config.get('approval', None)
self._merge = self.config.get('merge', False)
self._labels = self.config.get('label', [])
if not isinstance(self._labels, list):
self._labels = [self._labels]
self._unlabels = self.config.get('unlabel', [])
if not isinstance(self._unlabels, list):
self._unlabels = [self._unlabels]
def report(self, item):
"""Report on an event."""
@ -57,6 +64,8 @@ class GitlabReporter(BaseReporter):
self.addMRComment(item)
if self._approval is not None:
self.setApproval(item)
if self._labels or self._unlabels:
self.setLabels(item)
if self._merge:
self.mergeMR(item)
if not item.change.is_merged:
@ -83,6 +92,16 @@ class GitlabReporter(BaseReporter):
self.connection.approveMR(project, mr_number, patchset,
self._approval, event=item.event)
def setLabels(self, item):
log = get_annotated_logger(self.log, item.event)
project = item.change.project.name
mr_number = item.change.number
log.debug('Reporting change %s, params %s, labels: %s, unlabels: %s',
item.change, self.config, self._labels, self._unlabels)
self.connection.updateMRLabels(project, mr_number,
self._labels, self._unlabels,
zuul_event_id=item.event)
def mergeMR(self, item):
project = item.change.project.name
mr_number = item.change.number
@ -120,5 +139,7 @@ def getSchema():
'comment': bool,
'approval': bool,
'merge': bool,
'label': scalar_or_list(str),
'unlabel': scalar_or_list(str),
})
return gitlab_reporter

View File

@ -34,6 +34,7 @@ class GitlabTrigger(BaseTrigger):
comments=to_list(trigger.get('comment')),
refs=to_list(trigger.get('ref')),
labels=to_list(trigger.get('labels')),
unlabels=to_list(trigger.get('unlabels')),
)
efilters.append(f)
return efilters
@ -53,6 +54,7 @@ def getSchema():
'action': scalar_or_list(str),
'comment': scalar_or_list(str),
'ref': scalar_or_list(str),
'labels': scalar_or_list(str)
'labels': scalar_or_list(str),
'unlabels': scalar_or_list(str),
}
return gitlab_trigger