diff --git a/README.md b/README.md index 82642af..cfc058e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ export ZENHUB_TOKEN="" export GITHUB_TOKEN="" export GITHUB_OWNER="" export GITHUB_REPOSITORY= -export ZENHUB_PIPELINE="New Issues, Product Backlog, Icebox" # example +export ZENHUB_PIPELINE="New Issues,Product Backlog,Icebox" # example export FLASK_USERNAME="" # Use basic auth here, such as http://username:passowrd@localhost:8080 export FLASK_PASSWORD="" export GITHUB_REPOSITORY_TEST="" diff --git a/github-bot/harvester_github_bot/action.py b/github-bot/harvester_github_bot/action.py index e086f99..499b7fc 100644 --- a/github-bot/harvester_github_bot/action.py +++ b/github-bot/harvester_github_bot/action.py @@ -1,10 +1,9 @@ import abc class ActionRequest: - def __init__(self): - pass - def setAction(self, action): + def __init__(self, action, event_type): self.action = action + self.event_type = event_type class Action(abc.ABC): diff --git a/github-bot/harvester_github_bot/action_label.py b/github-bot/harvester_github_bot/action_label.py index 7b6ac4f..9a7f897 100644 --- a/github-bot/harvester_github_bot/action_label.py +++ b/github-bot/harvester_github_bot/action_label.py @@ -12,6 +12,8 @@ def __init__(self): pass def isMatched(self, actionRequest): + if actionRequest.event_type not in ['issue']: + return False if actionRequest.action not in ['labeled']: return False return True diff --git a/github-bot/harvester_github_bot/action_project.py b/github-bot/harvester_github_bot/action_project.py new file mode 100644 index 0000000..5a16c79 --- /dev/null +++ b/github-bot/harvester_github_bot/action_project.py @@ -0,0 +1,101 @@ +import re + +from flask import render_template + +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 +from harvester_github_bot import app, gtihub_project_manager, repo, repo_test, \ + ZENHUB_PIPELINE, GITHUB_OWNER, GITHUB_REPOSITORY, ZENHUB_PIPELINE, GITHUB_REPOSITORY_TEST + +template_re = re.compile('---\n.*?---\n', re.DOTALL) + + +class ActionProject(Action): + def __init__(self): + self.__event_type_key = "projects_v2_item" + + def isMatched(self, actionRequest): + if actionRequest.event_type not in [self.__event_type_key]: + return False + if actionRequest.action not in ['edited']: + return False + return True + + def action(self, request): + if request[self.__event_type_key]["content_type"] != "Issue": + return + + project_node_id = request[self.__event_type_key]['project_node_id'] + if gtihub_project_manager.project()["id"] != project_node_id: + app.logger.error("project is not matched") + return + + target_column = request['changes']['field_value']['to'] + if target_column["name"] not in ZENHUB_PIPELINE.split(","): + app.logger.debug('target_column is {}, ignoring'.format(target_column["name"])) + return + + issue_node_id = request[self.__event_type_key]['content_node_id'] + issue = gtihub_project_manager.get_global_issue(issue_node_id) + + if issue["number"] is None: + app.logger.error("issue number is None") + return + + it = IssueTransfer(issue["number"]) + it.create_comment_if_not_exist() + it.create_e2e_issue() + + +class IssueTransfer: + def __init__( + self, + issue_number + ): + self.__comments = None + self.__issue = repo.get_issue(issue_number) + + def create_comment_if_not_exist(self): + self.__comments = self.__issue.get_comments() + found = False + for comment in self.__comments: + if comment.body.strip().startswith('## Pre Ready-For-Testing Checklist'): + app.logger.debug('pre-merged checklist already exists, not creating a new one') + found = True + break + if not found: + app.logger.debug('pre-merge checklist does not exist, creating a new one') + self.__issue.create_comment(render_template('pre-merge.md')) + + def create_e2e_issue(self): + require_e2e = True + labels = self.__issue.get_labels() + for label in labels: + if label.name == 'not-require/test-plan': + require_e2e = False + break + if require_e2e: + found = False + for comment in self.__comments: + if comment.body.startswith('Automation e2e test issue:'): + app.logger.debug('Automation e2e test issue already exists, not creating a new one') + found = True + break + if not found: + app.logger.debug('Automation e2e test issue does not exist, creating a new one') + + issue_link = '{}/{}#{}'.format(GITHUB_OWNER, GITHUB_REPOSITORY, self.__issue.number) + issue_test_title = '[e2e] {}'.format(self.__issue.title) + issue_test_template_content = repo_test.get_contents( + ".github/ISSUE_TEMPLATE/test.md").decoded_content.decode() + issue_test_body = template_re.sub("\n", issue_test_template_content, count=1) + issue_test_body += '\nrelated issue: {}'.format(issue_link) + issue_test = repo_test.create_issue(title=issue_test_title, body=issue_test_body) + + issue_test_link = '{}/{}#{}'.format(GITHUB_OWNER, GITHUB_REPOSITORY_TEST, issue_test.number) + + # link test issue in Harvester issue + self.__issue.create_comment('Automation e2e test issue: {}'.format(issue_test_link)) + else: + app.logger.debug('label require/automation-e2e does not exists, not creating test issue') diff --git a/github-bot/harvester_github_bot/github_graphql/manager.py b/github-bot/harvester_github_bot/github_graphql/manager.py index 76570c6..9dc74aa 100644 --- a/github-bot/harvester_github_bot/github_graphql/manager.py +++ b/github-bot/harvester_github_bot/github_graphql/manager.py @@ -1,5 +1,5 @@ import requests -from harvester_github_bot.github_graphql.ql_queries import GET_ISSUE_QUERY, GET_ORGANIZATION_PROJECT_QUERY, GET_USER_PROJECT_QUERY +from harvester_github_bot.github_graphql.ql_queries import GET_ISSUE_QUERY, GET_GLOBAL_ISSUE_QUERY, GET_ORGANIZATION_PROJECT_QUERY, GET_USER_PROJECT_QUERY from harvester_github_bot.github_graphql.ql_mutation import ADD_ISSUE_TO_PROJECT_MUTATION class GitHubProjectManager: @@ -9,7 +9,9 @@ def __init__(self, organization, repository, project_number, headers): self.headers = headers self.url = "https://api.github.com/graphql" self.__project = self.__get_orgnization_project(project_number) - # self.__project = self.__get_user_project(project_number) # used in testing + + def project(self): + return self.__project def get_issue(self, issue_number): variables = { @@ -22,6 +24,16 @@ def get_issue(self, issue_number): return response.json()['data']['repository']['issue'] else: raise Exception(f"Query failed to run by returning code of {response.status_code}. {response.json()}") + + def get_global_issue(self, issue_node_id): + variables = { + 'issue_node_id': issue_node_id + } + response = requests.post(self.url, headers=self.headers, json={'query': GET_GLOBAL_ISSUE_QUERY, 'variables': variables}) + if response.status_code == 200: + return response.json()['data']['node'] + else: + raise Exception(f"Query failed to run by returning code of {response.status_code}. {response.json})") def add_issue_to_project(self, issue_id): variables = { @@ -42,17 +54,5 @@ def __get_orgnization_project(self, project_number): response = requests.post(self.url, headers=self.headers, json={'query': GET_ORGANIZATION_PROJECT_QUERY, 'variables': variables}) if response.status_code == 200: return response.json()['data']['organization']['projectV2'] - else: - raise Exception(f"Query failed to run by returning code of {response.status_code}. {response.json()}") - - - def __get_user_project(self, project_number): - variables = { - 'organization': self.organization, - 'project_number': project_number - } - response = requests.post(self.url, headers=self.headers, json={'query': GET_USER_PROJECT_QUERY, 'variables': variables}) - if response.status_code == 200: - return response.json()['data']['user']['projectV2'] else: raise Exception(f"Query failed to run by returning code of {response.status_code}. {response.json()}") \ No newline at end of file diff --git a/github-bot/harvester_github_bot/github_graphql/ql_queries.py b/github-bot/harvester_github_bot/github_graphql/ql_queries.py index 0816b46..272c941 100644 --- a/github-bot/harvester_github_bot/github_graphql/ql_queries.py +++ b/github-bot/harvester_github_bot/github_graphql/ql_queries.py @@ -35,4 +35,16 @@ } } } +""" + +GET_GLOBAL_ISSUE_QUERY = """ +query Organization($issue_node_id: ID!) { + node(id: $issue_node_id) { + ... on Issue { + title + id + number + } + } +} """ \ 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 dc02878..18432a5 100644 --- a/github-bot/harvester_github_bot/route.py +++ b/github-bot/harvester_github_bot/route.py @@ -8,6 +8,7 @@ from harvester_github_bot.issue_transfer import issue_transfer from harvester_github_bot.action import ActionRequest from harvester_github_bot.action_label import ActionLabel +from harvester_github_bot.action_project import ActionProject auth = HTTPBasicAuth() @@ -17,7 +18,6 @@ def verify_password(username, password): if check_password_hash(FLASK_USERNAME, username) and check_password_hash(FLASK_PASSWORD, password): return username - @app.route('/zenhub', methods=['POST']) @auth.login_required def zenhub(): @@ -36,10 +36,14 @@ def zenhub(): return { 'message': 'webhook handled successfully' }, http.HTTPStatus.OK - - SUPPORTED_ACTIONS = [ ActionLabel(), + ActionProject(), +] + +SUPPORTED_EVENT = [ + "projects_v2_item", + "issue" ] @app.route('/github', methods=['POST']) @@ -47,14 +51,18 @@ def zenhub(): def gh(): req = request.get_json() msg = "Skip action" + event_type = "" + + for event in SUPPORTED_EVENT: + if req.get(event) is not None: + event_type = event - if req.get('issue') is None: + if event_type == "": return { 'message': msg }, http.HTTPStatus.OK - action_request = ActionRequest() - action_request.setAction(req.get('action')) + action_request = ActionRequest(req.get('action'), event_type) for action in SUPPORTED_ACTIONS: if action.isMatched(action_request):