| #!/usr/bin/env python3 |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| import re |
| import logging |
| import sys |
| from pathlib import Path |
| from typing import Dict, Tuple, Any, Optional, List, Union |
| |
| # Hackery to enable importing of utils from ci/scripts/jenkins |
| REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent |
| sys.path.append(str(REPO_ROOT / "ci" / "scripts" / "jenkins")) |
| |
| from git_utils import GitHubRepo |
| |
| BOT_COMMENT_START = "<!---bot-comment-->" |
| WELCOME_TEXT = "Thanks for contributing to TVM! Please refer to the contributing guidelines https://tvm.apache.org/docs/contribute/ for useful information and tips. Please request code reviews from [Reviewers](https://github.com/apache/incubator-tvm/blob/master/CONTRIBUTORS.md#reviewers) by @-ing them in a comment." |
| |
| |
| class BotCommentBuilder: |
| ALLOWLIST_USERS = {"driazati", "gigiblender", "areusch"} |
| |
| def __init__(self, github: GitHubRepo, data: Dict[str, Any]): |
| self.github = github |
| self.pr_number = data["number"] |
| self.comment_data = data["comments"]["nodes"] |
| self.author = data["author"]["login"] |
| |
| def find_bot_comment(self) -> Optional[Dict[str, Any]]: |
| """ |
| Return the existing bot comment or None if it does not exist |
| """ |
| for comment in self.comment_data: |
| logging.info(f"Checking comment {comment}") |
| if ( |
| comment["author"]["login"] == "github-actions" |
| and BOT_COMMENT_START in comment["body"] |
| ): |
| logging.info("Found existing comment") |
| return comment |
| logging.info("No existing comment found") |
| return None |
| |
| def find_existing_body(self) -> Dict[str, str]: |
| """ |
| Find existing dynamic bullet point items |
| """ |
| existing_comment = self.find_bot_comment() |
| if existing_comment is None: |
| logging.info(f"No existing comment while searching for body items") |
| return {} |
| |
| matches = re.findall( |
| r"<!--bot-comment-([a-z][a-z-]+)-start-->([\S\s]*?)<!--bot-comment-([a-z-]+)-end-->", |
| existing_comment["body"], |
| flags=re.MULTILINE, |
| ) |
| logging.info(f"Fetch body item matches: {matches}") |
| |
| items = {} |
| for start, text, end in matches: |
| if start != end: |
| raise RuntimeError( |
| f"Malformed comment found: {start} marker did not have matching end, found instead {end}" |
| ) |
| items[start] = text.strip().lstrip("* ") |
| |
| logging.info(f"Found body items: {items}") |
| return items |
| |
| def _post_comment(self, body_items: Dict[str, str]): |
| comment = BOT_COMMENT_START + "\n\n" + WELCOME_TEXT + "\n\n" |
| for key, content in body_items.items(): |
| line = self.start_key(key) + "\n * " + content.strip() + self.end_key(key) |
| logging.info(f"Adding line {line}") |
| comment += line |
| comment += "\n\n<sub>Generated by [tvm-bot](https://github.com/apache/tvm/blob/main/ci/README.md#github-actions)</sub>" |
| |
| data = {"body": comment} |
| url = f"issues/{self.pr_number}/comments" |
| |
| logging.info(f"Commenting {comment} on {url}") |
| |
| if self.author not in self.ALLOWLIST_USERS: |
| logging.info(f"Skipping comment for author {self.author}") |
| return |
| |
| existing_comment = self.find_bot_comment() |
| if existing_comment is None: |
| # Comment does not exist, post it |
| r = self.github.post(url, data) |
| else: |
| # Comment does exist, update it |
| comment_url = f"issues/comments/{existing_comment['databaseId']}" |
| r = self.github.patch(comment_url, data) |
| |
| logging.info(f"Got response from posting comment: {r}") |
| |
| def start_key(self, key: str) -> str: |
| return f"<!--bot-comment-{key}-start-->" |
| |
| def end_key(self, key: str) -> str: |
| return f"<!--bot-comment-{key}-end-->" |
| |
| def post_items(self, items: List[Tuple[str, str]]): |
| """ |
| Update or post bullet points in the PR based on 'items' which is a |
| list of (key, text) pairs |
| """ |
| # Find the existing bullet points |
| body_items = self.find_existing_body() |
| |
| # Add or update the requested items |
| for key, text in items: |
| if text is None or text.strip() == "": |
| logging.info(f"Skipping {key} since it was empty") |
| continue |
| logging.info(f"Updating comment items {key} with {text}") |
| body_items[key] = text.strip() |
| |
| # Post or update the comment |
| # print(body_items) |
| self._post_comment(body_items=body_items) |