You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mxnet.apache.org by GitBox <gi...@apache.org> on 2020/08/11 19:10:30 UTC

[GitHub] [incubator-mxnet-ci] marcoabreu commented on a change in pull request #27: Serverless based Lambda function for Labelling Successful PRs with pr-awaiting-review

marcoabreu commented on a change in pull request #27:
URL: https://github.com/apache/incubator-mxnet-ci/pull/27#discussion_r468800659



##########
File path: services/lambda-pr-status-labeler/PRStatusBot.py
##########
@@ -0,0 +1,298 @@
+# 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 ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token (Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms that the secret
+        of the webhook is matched and that each github event is signed appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(pr_number)
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        else:
+            logging.info(f'Not labeled {label}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()
+        for label in labels:

Review comment:
       You can directly check if the label is inside the collection. No need to do that loop

##########
File path: services/lambda-pr-status-labeler/PRStatusBot.py
##########
@@ -0,0 +1,298 @@
+# 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 ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token (Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms that the secret
+        of the webhook is matched and that each github event is signed appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(pr_number)
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        else:
+            logging.info(f'Not labeled {label}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()
+        for label in labels:
+            if desired_label == label.name:
+                return True
+        return False
+
+    def _parse_reviews(self, pr):
+        """
+        This method parses through the reviews of the PR and returns count of
+        4 states: Approved reviews, Comment reviews, Requested Changes reviews
+        and Dismissed reviews
+        :param pr
+        """
+        approved_count, requested_changes_count, comment_count = 0, 0, 0
+        for review in pr.get_reviews():
+            if review.state == 'APPROVED':
+                approved_count += 1
+            elif review.state == 'CHANGES_REQUESTED':
+                requested_changes_count += 1
+            elif review.state == 'COMMENTED':
+                comment_count += 1
+            elif review.state == 'DISMISSED':
+                dismissed_count += 1
+            else:
+                logging.error(f'Unknown review state {review.state}')
+        return approved_count, requested_changes_count, comment_count, dismissed_count
+
+    def _label_pr_based_on_status(self, combined_status_state, pull_request_obj):
+        """
+        This method checks the CI status of the specific commit of the PR
+        and it labels the PR accordingly
+        :param combined_status_state
+        :param pull_request_obj
+        """
+        # pseudo-code
+        # if WIP in title or PR is draft or CI failed:
+        #   pr-work-in-progress
+        # elif CI has not started yet or CI is in progress:
+        #   pr-awaiting-testing
+        # else: # CI passed checks
+        #   if pr has at least one approval and no request changes:
+        #       pr-awaiting-merge
+        #   elif pr has no review or all reviews have been dismissed/re-requested:
+        #       pr-awaiting-review
+        #   else: # pr has a review that hasn't been dismissed yet no approval
+        #       pr-awaiting-response
+
+        # combined status of PR can be 1 of the 3 potential states
+        # https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-reference
+        if combined_status_state == 'failure':
+            ci_failed = True
+        elif combined_status_state == 'pending':
+            ci_pending = True
+
+        if 'WIP' in pull_request_obj.title:
+            wip_in_title = True
+        work_in_progress_conditions = wip_in_title or pull_request_obj.draft or ci_failed
+        if work_in_progress_conditions:
+            self._add_label(pull_request_obj, 'pr-work-in-progress')
+        elif ci_pending:
+            self._add_label(pull_request_obj, 'pr-awaiting-testing')

Review comment:
       Turn labels into constants at the top

##########
File path: services/lambda-pr-status-labeler/PRStatusBot.py
##########
@@ -0,0 +1,298 @@
+# 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 ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token (Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms that the secret
+        of the webhook is matched and that each github event is signed appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(pr_number)
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        else:
+            logging.info(f'Not labeled {label}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()
+        for label in labels:
+            if desired_label == label.name:
+                return True
+        return False
+
+    def _parse_reviews(self, pr):
+        """
+        This method parses through the reviews of the PR and returns count of
+        4 states: Approved reviews, Comment reviews, Requested Changes reviews
+        and Dismissed reviews
+        :param pr
+        """
+        approved_count, requested_changes_count, comment_count = 0, 0, 0
+        for review in pr.get_reviews():
+            if review.state == 'APPROVED':
+                approved_count += 1
+            elif review.state == 'CHANGES_REQUESTED':
+                requested_changes_count += 1
+            elif review.state == 'COMMENTED':
+                comment_count += 1
+            elif review.state == 'DISMISSED':
+                dismissed_count += 1
+            else:
+                logging.error(f'Unknown review state {review.state}')
+        return approved_count, requested_changes_count, comment_count, dismissed_count
+
+    def _label_pr_based_on_status(self, combined_status_state, pull_request_obj):
+        """
+        This method checks the CI status of the specific commit of the PR
+        and it labels the PR accordingly
+        :param combined_status_state
+        :param pull_request_obj
+        """
+        # pseudo-code
+        # if WIP in title or PR is draft or CI failed:
+        #   pr-work-in-progress
+        # elif CI has not started yet or CI is in progress:
+        #   pr-awaiting-testing
+        # else: # CI passed checks
+        #   if pr has at least one approval and no request changes:
+        #       pr-awaiting-merge
+        #   elif pr has no review or all reviews have been dismissed/re-requested:
+        #       pr-awaiting-review
+        #   else: # pr has a review that hasn't been dismissed yet no approval
+        #       pr-awaiting-response
+
+        # combined status of PR can be 1 of the 3 potential states
+        # https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-reference
+        if combined_status_state == 'failure':
+            ci_failed = True
+        elif combined_status_state == 'pending':
+            ci_pending = True
+
+        if 'WIP' in pull_request_obj.title:
+            wip_in_title = True
+        work_in_progress_conditions = wip_in_title or pull_request_obj.draft or ci_failed
+        if work_in_progress_conditions:
+            self._add_label(pull_request_obj, 'pr-work-in-progress')
+        elif ci_pending:
+            self._add_label(pull_request_obj, 'pr-awaiting-testing')
+        else:  # CI passed since status=successful
+            # parse reviews to assess count of approved/requested changes/commented/dismissed reviews
+            approves, request_changes, comments, dismissed = self._parse_reviews(pull_request_obj)

Review comment:
       Does this only count from committers?

##########
File path: services/lambda-pr-status-labeler/PRStatusBot.py
##########
@@ -0,0 +1,298 @@
+# 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 ast
+import json
+import hmac
+import hashlib
+import os
+import logging
+import re
+import secret_manager
+
+from github import Github
+
+
+class PRStatusBot:
+    def __init__(self,
+                 repo=os.environ.get("repo"),
+                 github_personal_access_token=None,
+                 apply_secret=True):
+        """
+        Initializes the PR Status Bot
+        :param repo: GitHub repository that is being referenced
+        :param github_personal_access_token: GitHub authentication token (Personal access token)
+        :param apply_secret: GitHub secret credential (Secret credential that is unique to a GitHub developer)
+        """
+        self.repo = repo
+        self.github_personal_access_token = github_personal_access_token
+        if apply_secret:
+            self._get_secret()
+
+    def _get_secret(self):
+        """
+        This method is to get secret value from Secrets Manager
+        """
+        secret = json.loads(secret_manager.get_secret())
+        self.github_personal_access_token = secret["github_personal_access_token"]
+        self.webhook_secret = secret["webhook_secret"]
+
+    def _secure_webhook(self, event):
+        """
+        This method will validate the security of the webhook, it confirms that the secret
+        of the webhook is matched and that each github event is signed appropriately
+        :param event: The github event we want to validate
+        :return Response denoting success or failure of security
+        """
+
+        # Validating github event is signed
+        try:
+            git_signed = ast.literal_eval(event["Records"][0]['body'])['headers']["X-Hub-Signature"]
+        except KeyError:
+            raise Exception("WebHook from GitHub is not signed")
+        git_signed = git_signed.replace('sha1=', '')
+
+        # Signing our event with the same secret as what we assigned to github event
+        secret = self.webhook_secret
+        body = ast.literal_eval(event["Records"][0]['body'])['body']
+        secret_sign = hmac.new(key=secret.encode('utf-8'), msg=body.encode('utf-8'), digestmod=hashlib.sha1).hexdigest()
+
+        # Validating signatures match
+        return hmac.compare_digest(git_signed, secret_sign)
+
+    def _get_github_object(self):
+        """
+        This method returns github object initialized with Github personal access token
+        """
+        github_obj = Github(self.github_personal_access_token)
+        return github_obj
+
+    def _get_pull_request_object(self, github_obj, pr_number):
+        """
+        This method returns a PullRequest object based on the PR number
+        :param github_obj
+        :param pr_number
+        """
+        repo = github_obj.get_repo(self.repo)
+        pr_obj = repo.get_pull(pr_number)
+        return pr_obj
+
+    def _get_commit_object(self, github_obj, commit_sha):
+        """
+        This method returns a Commit object based on the SHA of the commit
+        :param github_obj
+        :param commit_sha
+        """
+        repo = github_obj.get_repo(self.repo)
+        commit_obj = repo.get_commit(commit_sha)
+        return commit_obj
+
+    def _drop_other_pr_labels(self, pr, desired_label):
+        labels = pr.get_labels()
+
+        for label in labels:
+            logging.info(f'Label:{label}')
+            if label.name.startswith('pr-') and label.name != desired_label:
+                try:
+                    logging.info(f'Removing {label}')
+                    # pr.remove_from_labels(label)
+                except Exception:
+                    logging.error(f'Error while removing the label {label}')
+
+    def _add_label(self, pr, label):
+        # drop other PR labels
+        self._drop_other_pr_labels(pr, label)
+
+        # check if the PR already has the desired label
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'PR {pr.number} already contains the label {label}')
+            return
+
+        # try:
+        #     pr.add_to_labels(label)
+        # except Exception:
+        #     logging.error(f'Unable to add label {label}')
+
+        # verify that label has been correctly added
+        if(self._has_desired_label(pr, label)):
+            logging.info(f'Successfully labeled {label} for PR-{pr.number}')
+        else:
+            logging.info(f'Not labeled {label}')
+        return
+
+    def _has_desired_label(self, pr, desired_label):
+        """
+        This method returns True if desired label found in PR labels
+        """
+        labels = pr.get_labels()
+        for label in labels:
+            if desired_label == label.name:
+                return True
+        return False
+
+    def _parse_reviews(self, pr):
+        """
+        This method parses through the reviews of the PR and returns count of
+        4 states: Approved reviews, Comment reviews, Requested Changes reviews
+        and Dismissed reviews
+        :param pr
+        """
+        approved_count, requested_changes_count, comment_count = 0, 0, 0
+        for review in pr.get_reviews():
+            if review.state == 'APPROVED':
+                approved_count += 1
+            elif review.state == 'CHANGES_REQUESTED':
+                requested_changes_count += 1
+            elif review.state == 'COMMENTED':
+                comment_count += 1
+            elif review.state == 'DISMISSED':
+                dismissed_count += 1
+            else:
+                logging.error(f'Unknown review state {review.state}')
+        return approved_count, requested_changes_count, comment_count, dismissed_count
+
+    def _label_pr_based_on_status(self, combined_status_state, pull_request_obj):
+        """
+        This method checks the CI status of the specific commit of the PR
+        and it labels the PR accordingly
+        :param combined_status_state
+        :param pull_request_obj
+        """
+        # pseudo-code
+        # if WIP in title or PR is draft or CI failed:
+        #   pr-work-in-progress
+        # elif CI has not started yet or CI is in progress:
+        #   pr-awaiting-testing
+        # else: # CI passed checks
+        #   if pr has at least one approval and no request changes:
+        #       pr-awaiting-merge
+        #   elif pr has no review or all reviews have been dismissed/re-requested:
+        #       pr-awaiting-review
+        #   else: # pr has a review that hasn't been dismissed yet no approval
+        #       pr-awaiting-response
+
+        # combined status of PR can be 1 of the 3 potential states
+        # https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-reference
+        if combined_status_state == 'failure':
+            ci_failed = True
+        elif combined_status_state == 'pending':
+            ci_pending = True
+
+        if 'WIP' in pull_request_obj.title:
+            wip_in_title = True
+        work_in_progress_conditions = wip_in_title or pull_request_obj.draft or ci_failed
+        if work_in_progress_conditions:
+            self._add_label(pull_request_obj, 'pr-work-in-progress')
+        elif ci_pending:
+            self._add_label(pull_request_obj, 'pr-awaiting-testing')
+        else:  # CI passed since status=successful
+            # parse reviews to assess count of approved/requested changes/commented/dismissed reviews
+            approves, request_changes, comments, dismissed = self._parse_reviews(pull_request_obj)
+            if approves > 0 and request_changes == 0:
+                self._add_label(pull_request_obj, 'pr-awaiting-merge')
+            else:
+                has_no_reviews = approves + request_changes - dismissed + comments == 0

Review comment:
       I'd like to see some units tests for all these cases. Seems too complex for me, hence please add tests




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
users@infra.apache.org