From b79a5605be2419aec423420b0ea62aba2dc2c157 Mon Sep 17 00:00:00 2001 From: Peter Hill Date: Mon, 20 Mar 2023 17:43:56 +0000 Subject: [PATCH] Add experimental support for annotations --- README.md | 3 + action.yml | 5 ++ post/action.yml | 5 ++ .../clang_tidy_review/__init__.py | 72 +++++++++++++++++++ .../clang_tidy_review/post.py | 19 +++-- .../clang_tidy_review/review.py | 21 +++--- 6 files changed, 112 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6078cb7..36d0a3c 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,9 @@ at once, so `clang-tidy-review` will only attempt to post the first it for the second workflow. Relevant when receiving PRs from forks that don't have the required permissions to post reviews. - default: false +- `annotations`: Use Annotations instead of comments. A maximum of 10 + annotations can be written fully, the rest will be summarised. This is a + limitation of the GitHub API. ## Outputs diff --git a/action.yml b/action.yml index 6b65d55..3bd2ed4 100644 --- a/action.yml +++ b/action.yml @@ -57,6 +57,10 @@ inputs: description: "Only generate but don't post the review, leaving it for the second workflow. Relevant when receiving PRs from forks that don't have the required permissions to post reviews." required: false default: false + annotations: + description: "Use annotations instead of comments. See README for limitations on annotations" + required: false + default: false pr: default: ${{ github.event.pull_request.number }} repo: @@ -83,3 +87,4 @@ runs: - --max-comments=${{ inputs.max_comments }} - --lgtm-comment-body='${{ inputs.lgtm_comment_body }}' - --split_workflow=${{ inputs.split_workflow }} + - --annotations=${{ inputs.annotations }} diff --git a/post/action.yml b/post/action.yml index d11d117..c0a4f83 100644 --- a/post/action.yml +++ b/post/action.yml @@ -19,6 +19,10 @@ inputs: description: 'Message to post on PR if no issues are found. An empty string will post no LGTM comment.' required: false default: 'clang-tidy review says "All clean, LGTM! :+1:"' + annotations: + description: "Use annotations instead of comments. See README for limitations on annotations" + required: false + default: false workflow_id: description: 'ID of the review workflow' default: ${{ github.event.workflow_run.id }} @@ -38,3 +42,4 @@ runs: - --lgtm-comment-body='${{ inputs.lgtm_comment_body }}' - --workflow_id=${{ inputs.workflow_id }} - --pr_number=${{ inputs.pr_number }} + - --annotations=${{ inputs.annotations }} diff --git a/post/clang_tidy_review/clang_tidy_review/__init__.py b/post/clang_tidy_review/clang_tidy_review/__init__.py index 375d089..81576ce 100644 --- a/post/clang_tidy_review/clang_tidy_review/__init__.py +++ b/post/clang_tidy_review/clang_tidy_review/__init__.py @@ -30,6 +30,7 @@ FIXES_FILE = "clang_tidy_review.yaml" METADATA_FILE = "clang-tidy-review-metadata.json" REVIEW_FILE = "clang-tidy-review-output.json" +MAX_ANNOTATIONS = 10 class Metadata(TypedDict): @@ -254,6 +255,16 @@ def post_review(self, review): # Re-raise the exception, causing an error in the workflow raise e + def post_annotations(self, review): + headers = { + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {self.token}", + } + url = f"{self.api_url}/repos/{self.repo_name}/check-runs" + + response = requests.post(url, json=review, headers=headers) + response.raise_for_status() + @contextlib.contextmanager def message_group(title: str): @@ -976,3 +987,64 @@ def post_review( pull_request.post_review(trimmed_review) return total_comments + + +def convert_comment_to_annotations(comment): + return { + "path": comment["path"], + "start_line": comment.get("start_line", comment["line"]), + "end_line": comment["line"], + "annotation_level": "warning", + "title": "clang-tidy", + "message": comment["body"], + } + + +def post_annotations(pull_request: PullRequest, review: Optional[PRReview]): + """Post the first 10 comments in the review as annotations""" + + body = { + "name": "clang-tidy-review", + "head_sha": pull_request.pull_request.head.sha, + "status": "completed", + "conclusion": "success", + } + + if review is None: + return + + if review["comments"] == []: + print("No warnings to report, LGTM!") + pull_request.post_annotations(body) + + comments = [] + for comment in review["comments"]: + first_line = comment["body"].splitlines()[0] + comments.append( + f"{comment['path']}:{comment.get('start_line', comment['line'])}: {first_line}" + ) + + total_comments = len(review["comments"]) + + body["conclusion"] = "neutral" + body["output"] = { + "title": "clang-tidy-review", + "summary": f"There were {total_comments} warnings", + "text": "\n".join(comments), + "annotations": [ + convert_comment_to_annotations(comment) + for comment in review["comments"][:MAX_ANNOTATIONS] + ], + } + + pull_request.post_annotations(body) + + +def bool_argument(user_input) -> bool: + """Convert text to bool""" + user_input = str(user_input).upper() + if user_input == "TRUE": + return True + if user_input == "FALSE": + return False + raise ValueError("Invalid value passed to bool_argument") diff --git a/post/clang_tidy_review/clang_tidy_review/post.py b/post/clang_tidy_review/clang_tidy_review/post.py index f96c39d..48b781e 100755 --- a/post/clang_tidy_review/clang_tidy_review/post.py +++ b/post/clang_tidy_review/clang_tidy_review/post.py @@ -15,6 +15,8 @@ load_metadata, strip_enclosing_quotes, download_artifacts, + post_annotations, + bool_argument, ) @@ -45,6 +47,12 @@ def main(): help="ID of the workflow that generated the review", default=None, ) + parser.add_argument( + "--annotations", + help="Use annotations instead of comments", + type=bool_argument, + default=False, + ) parser.add_argument("--pr_number", help="PR number", default=None) args = parser.parse_args() @@ -78,10 +86,13 @@ def main(): flush=True, ) - lgtm_comment_body = strip_enclosing_quotes(args.lgtm_comment_body) - post_review( - pull_request, review, args.max_comments, lgtm_comment_body, args.dry_run - ) + if args.annotations: + post_annotations(pull_request, review) + else: + lgtm_comment_body = strip_enclosing_quotes(args.lgtm_comment_body) + post_review( + pull_request, review, args.max_comments, lgtm_comment_body, args.dry_run + ) if __name__ == "__main__": diff --git a/post/clang_tidy_review/clang_tidy_review/review.py b/post/clang_tidy_review/clang_tidy_review/review.py index 058da76..627b9f3 100755 --- a/post/clang_tidy_review/clang_tidy_review/review.py +++ b/post/clang_tidy_review/clang_tidy_review/review.py @@ -18,6 +18,8 @@ post_review, save_metadata, strip_enclosing_quotes, + post_annotations, + bool_argument, ) @@ -76,15 +78,6 @@ def main(): type=str, default="", ) - - def bool_argument(user_input): - user_input = str(user_input).upper() - if user_input == "TRUE": - return True - if user_input == "FALSE": - return False - raise ValueError("Invalid value passed to bool_argument") - parser.add_argument( "--max-comments", help="Maximum number of comments to post at once", @@ -106,6 +99,12 @@ def bool_argument(user_input): type=bool_argument, default=False, ) + parser.add_argument( + "--annotations", + help="Use annotations instead of comments", + type=bool_argument, + default=False, + ) parser.add_argument("--token", help="github auth token") parser.add_argument( "--dry-run", help="Run and generate review, but don't post", action="store_true" @@ -158,6 +157,10 @@ def bool_argument(user_input): if args.split_workflow: print("split_workflow is enabled, not posting review") + return + + if args.annotations: + post_annotations(pull_request, review) else: lgtm_comment_body = strip_enclosing_quotes(args.lgtm_comment_body) post_review(