diff --git a/github-bot/harvester_github_bot/action.py b/github-bot/harvester_github_bot/action.py new file mode 100644 index 0000000..e086f99 --- /dev/null +++ b/github-bot/harvester_github_bot/action.py @@ -0,0 +1,28 @@ +import abc + +class ActionRequest: + def __init__(self): + pass + def setAction(self, action): + self.action = action + + +class Action(abc.ABC): + # isMatched returns True if the actionRequest is matched with the action from github webook + @abc.abstractmethod + def isMatched(self, actionRequest): + raise NotImplementedError + + @abc.abstractmethod + def action(self, request): + raise NotImplementedError + +class LabelAction(abc.ABC): + # isMatched returns True if it meets the condition to execute the action + @abc.abstractmethod + def isMatched(self, request): + raise NotImplementedError + + @abc.abstractmethod + def action(self, request): + raise NotImplementedError \ No newline at end of file diff --git a/github-bot/harvester_github_bot/action_label.py b/github-bot/harvester_github_bot/action_label.py new file mode 100644 index 0000000..7b6ac4f --- /dev/null +++ b/github-bot/harvester_github_bot/action_label.py @@ -0,0 +1,26 @@ +from harvester_github_bot.label_action.create_gui_issue import CreateGUIIssue +from harvester_github_bot.label_action.create_backport import CreateBackport +from harvester_github_bot.action import Action + +ALL_LABEL_ACTIONS = [ + CreateBackport, + CreateGUIIssue +] + +class ActionLabel(Action): + def __init__(self): + pass + + def isMatched(self, actionRequest): + if actionRequest.action not in ['labeled']: + return False + return True + + def action(self, request): + for label_action in ALL_LABEL_ACTIONS: + __label_action = label_action() + if __label_action.isMatched(request): + __label_action.action(request) + + return "labeled related actions succeed" + diff --git a/github-bot/harvester_github_bot/action_sync_milestone.py b/github-bot/harvester_github_bot/action_sync_milestone.py index 982c766..dfac1cd 100644 --- a/github-bot/harvester_github_bot/action_sync_milestone.py +++ b/github-bot/harvester_github_bot/action_sync_milestone.py @@ -1,12 +1,13 @@ from harvester_github_bot import app from harvester_github_bot import zenh_api, repo from harvester_github_bot.exception import CustomException +from harvester_github_bot.action import Action -class ActionSyncMilestone: +class ActionSyncMilestone(Action): def __init__(self): pass - def isMatched(self, action): - if action not in ['opened', 'milestoned', 'demilestoned']: + def isMatched(self, actionRequest): + if actionRequest.action not in ['opened', 'milestoned', 'demilestoned']: return False return True def action(self, request): diff --git a/github-bot/harvester_github_bot/action_backport.py b/github-bot/harvester_github_bot/label_action/create_backport.py similarity index 69% rename from github-bot/harvester_github_bot/action_backport.py rename to github-bot/harvester_github_bot/label_action/create_backport.py index 0afd0e2..3d8dfba 100644 --- a/github-bot/harvester_github_bot/action_backport.py +++ b/github-bot/harvester_github_bot/label_action/create_backport.py @@ -2,16 +2,8 @@ from harvester_github_bot import app, zenh_api, repo, \ BACKPORT_LABEL_KEY from harvester_github_bot.exception import CustomException, ExistedBackportComment - -class ActionBackport: - def __init__(self): - pass - def isMatched(self, action): - if action not in ['labeled']: - return False - return True - def action(self, request): - return backport(request) +from harvester_github_bot.label_action.create_gui_issue import CREATE_GUI_ISSUE_LABEL +from harvester_github_bot.action import LabelAction # check the issue's include backport-needed/(1.0.3|v1.0.3|v1.0.3-rc0) label backport_label_pattern = r'^%s\/[\w0-9\.]+' % BACKPORT_LABEL_KEY @@ -22,35 +14,47 @@ def action(self, request): # Description: backport the issue #link-id # Copy assignees and all labels except the backport-needed and add the not-require/test-plan label. # Move the issue to the associated milestone and release. -def backport(request): - normal_labels = [] - backport_labels = [] - for label in request['issue']['labels']: - if re.match(backport_label_pattern, label['name']) is not None: - backport_labels.append(label) - else: - normal_labels.append(label) - - msg = [] - for backport_label in backport_labels: - try: - app.logger.debug(f"issue number {request['issue']['number']} start to create backport with labels {backport_label['name']}") - - bp = Backport(request['issue']['number'], normal_labels, backport_label) - bp.verify() - bp.create_issue_if_not_exist() - bp.create_comment() - r = bp.related_release() - msg.append(r) - except ExistedBackportComment as e: - app.logger.debug(f"issue number {request['issue']['number']} had created backport with labels {backport_label['name']}") - except CustomException as e: - app.logger.exception(f"Custom exception : {str(e)}") - except Exception as e: - app.logger.exception(e) +class CreateBackport(LabelAction): + def __init__(self): + pass - return ",".join(msg) - + def isMatched(self, request): + for label in request['issue']['labels']: + if re.match(backport_label_pattern, label['name']) is not None: + return True + return False + + def action(self, request): + normal_labels = [] + backport_labels = [] + for label in request['issue']['labels']: + if re.match(backport_label_pattern, label['name']) is not None: + backport_labels.append(label) + else: + # backport should not include the 'require-ui' label + # because gui issue has its own backport + if CREATE_GUI_ISSUE_LABEL not in label['name']: + normal_labels.append(label) + + msg = [] + for backport_label in backport_labels: + try: + app.logger.debug(f"issue number {request['issue']['number']} start to create backport with labels {backport_label['name']}") + + bp = Backport(request['issue']['number'], normal_labels, backport_label) + bp.verify() + bp.create_issue_if_not_exist() + bp.create_comment() + r = bp.related_release() + msg.append(r) + except ExistedBackportComment as e: + app.logger.debug(f"issue number {request['issue']['number']} had created backport with labels {backport_label['name']}") + except CustomException as e: + app.logger.exception(f"Custom exception : {str(e)}") + except Exception as e: + app.logger.exception(e) + + return ",".join(msg) class Backport: def __init__( diff --git a/github-bot/harvester_github_bot/label_action/create_gui_issue.py b/github-bot/harvester_github_bot/label_action/create_gui_issue.py new file mode 100644 index 0000000..870e06e --- /dev/null +++ b/github-bot/harvester_github_bot/label_action/create_gui_issue.py @@ -0,0 +1,63 @@ +from harvester_github_bot import repo +from harvester_github_bot.action import LabelAction +import re + +CREATE_GUI_ISSUE_LABEL = "require-ui" +AREA_UI_LABEL = "area/ui" +class CreateGUIIssue(LabelAction): + def __init__(self): + pass + + def isMatched(self, request): + matched = False + + # We can't expect the labels order from Github Webhook request. + # It might be ["require-ui/small", "area/ui"] or ["area/ui", "require-ui/small"] + # So we need to consider those two cases. + # "area/ui" is high priority label. + # If "area/ui" is in the labels, we can skip the rest of the labels. + for label in request['issue']['labels']: + if AREA_UI_LABEL in label['name']: + return False + if CREATE_GUI_ISSUE_LABEL in label['name']: + matched = True + + return matched + + def action(self, request): + self.labels = [AREA_UI_LABEL] + + for label in request['issue']['labels']: + if CREATE_GUI_ISSUE_LABEL not in label['name']: + self.labels.append(label['name']) + + self.issue_number = request['issue']['number'] + self.original_issue = repo.get_issue(self.issue_number) + + if self.__is_gui_issue_exist(): + return "GUI issue already exist" + self.__create_gui_issue() + self.__create_comment() + return "create GUI issue success" + + def __create_gui_issue(self): + issue_data = { + 'title': f"[GUI] {self.original_issue.title}", + 'body': f"GUI Issue from #{self.issue_number}", + 'labels': self.labels, + 'assignees': self.original_issue.assignees, + } + if self.original_issue.milestone is not None: + issue_data['milestone'] = self.original_issue.milestone + self.gui_issue = repo.create_issue(**issue_data) + + def __create_comment(self): + self.original_issue.create_comment(body=f"GUI issue created #{self.gui_issue.number}.") + + def __is_gui_issue_exist(self): + comment_pattern = r'GUI issue created #[\d].' + comments = self.original_issue.get_comments() + for comment in comments: + if re.match(comment_pattern, comment.body): + return True + return False \ No newline at end of file diff --git a/github-bot/harvester_github_bot/route.py b/github-bot/harvester_github_bot/route.py index 6fc8b33..cc4d2ff 100644 --- a/github-bot/harvester_github_bot/route.py +++ b/github-bot/harvester_github_bot/route.py @@ -6,7 +6,8 @@ from harvester_github_bot.config import app, FLASK_USERNAME, FLASK_PASSWORD, GITHUB_OWNER, GITHUB_REPOSITORY from harvester_github_bot.issue_transfer import issue_transfer -from harvester_github_bot.action_backport import ActionBackport +from harvester_github_bot.action import ActionRequest +from harvester_github_bot.action_label import ActionLabel from harvester_github_bot.action_sync_milestone import ActionSyncMilestone auth = HTTPBasicAuth() @@ -39,7 +40,7 @@ def zenhub(): SUPPORTED_ACTIONS = [ - ActionBackport(), + ActionLabel(), ActionSyncMilestone(), ] @@ -53,9 +54,12 @@ def gh(): return { 'message': msg }, http.HTTPStatus.OK + + action_request = ActionRequest() + action_request.setAction(req.get('action')) for action in SUPPORTED_ACTIONS: - if action.isMatched(req.get('action')): + if action.isMatched(action_request): msg = action.action(req) break