diff --git a/.github/workflows/add-remove-labels.yml b/.github/workflows/add-remove-labels.yml index acfe2bf0d..645a0706b 100644 --- a/.github/workflows/add-remove-labels.yml +++ b/.github/workflows/add-remove-labels.yml @@ -4,14 +4,19 @@ on: types: [synchronize] pull_request_review: + types: [submitted, edited] + + pull_request_review_comment: + types: [created, edited] issue_comment: types: [created, edited, deleted] - if: | - contains(github.event.comment.body, '/wip') || - contains(github.event.comment.body, '/verified') || - contains(github.event.comment.body, '/lgtm') || - contains(github.event.comment.body, '/hold') + # I don't believe the conditional is supported here + # if: | + # contains(github.event.comment.body, '/wip') || + # contains(github.event.comment.body, '/verified') || + # contains(github.event.comment.body, '/lgtm') || + # contains(github.event.comment.body, '/hold') permissions: @@ -31,6 +36,27 @@ jobs: comment-id: ${{ github.event.comment.id }} reactions: '+1' + # This currently fails with either the bot PAT or the standard github token secret + # gh: Insufficient scopes for reacting to this Pull Request Review Comment. (HTTP 403) + # {"message":"Insufficient scopes for reacting to this Pull Request Review Comment.","documentation_url":"https://docs.github.com/rest/reactions/reactions#create-reaction-for-a-pull-request-review-comment","status":"403"} + # It could work if we had a token with the proper permissions. + # See https://github.com/peter-evans/create-or-update-comment/issues/392 for why the action above doesn't work. + # Confirmed as a bug, see: https://github.com/github/docs/issues/36899 + # - name: Acknowledge the review with thumbs up reaction + # if: ${{ github.event.review }} + # env: + # GH_TOKEN: ${{ secrets.OPENDATAHUB_TESTS_BOT_PAT }} + # REVIEW_COMMENT_ID: ${{ github.event.review.id }} + # REPO_NAME: ${{ github.event.repository.name }} + # REPO_OWNER: ${{ github.event.repository.owner.login }} + # run: | + # gh api \ + # --method POST \ + # -H "Accept: application/vnd.github+json" \ + # -H "X-GitHub-Api-Version: 2022-11-28" \ + # /repos/$REPO_OWNER/$REPO_NAME/pulls/comments/$REVIEW_COMMENT_ID/reactions \ + # -f "content=+1" + - uses: actions/checkout@v4 - name: Install uv @@ -38,12 +64,13 @@ jobs: - name: Run add remove labels env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENDATAHUB_TESTS_BOT_PAT }} GITHUB_PR_NUMBER: "${{ github.event.pull_request.number || github.event.issue.number }}" GITHUB_EVENT_ACTION: ${{ github.event.action }} GITHUB_EVENT_REVIEW_STATE: ${{ github.event.review.state }} GITHUB_EVENT_NAME: ${{ github.event_name }} COMMENT_BODY: ${{ github.event.comment.body }} + REVIEW_COMMENT_BODY: ${{ github.event.review.body }} GITHUB_USER_LOGIN: ${{ github.event.sender.login }} ACTION: "add-remove-labels" run: uv run python .github/workflows/scripts/pr_workflow.py diff --git a/.github/workflows/scripts/constants.py b/.github/workflows/scripts/constants.py index 5ed9a431c..21f79f618 100644 --- a/.github/workflows/scripts/constants.py +++ b/.github/workflows/scripts/constants.py @@ -11,6 +11,7 @@ SUCCESS_STR: str = "success" FAILURE_STR: str = "failure" QUEUED_STR: str = "queued" +APPROVED: str = "APPROVED" SUPPORTED_LABELS: set[str] = { f"{LABEL_PREFIX}{WIP_LABEL_STR}", diff --git a/.github/workflows/scripts/pr_workflow.py b/.github/workflows/scripts/pr_workflow.py index d404e1178..cc0e3f77f 100644 --- a/.github/workflows/scripts/pr_workflow.py +++ b/.github/workflows/scripts/pr_workflow.py @@ -6,6 +6,12 @@ from github.PullRequest import PullRequest from github.Repository import Repository +from github.PaginatedList import PaginatedList +from github.MainClass import Github +from github.GithubException import UnknownObjectException +from github.Organization import Organization +from github.Team import Team +from github.NamedUser import NamedUser from constants import ( ALL_LABELS_DICT, @@ -20,8 +26,8 @@ SUPPORTED_LABELS, VERIFIED_LABEL_STR, WELCOME_COMMENT, + APPROVED, ) -from github import Github, UnknownObjectException from simple_logger.logger import get_logger LOGGER = get_logger(name="pr_labeler") @@ -41,6 +47,7 @@ class SupportedActions: def __init__(self) -> None: self.repo: Repository self.pr: PullRequest + self.gh_client: Github self.repo_name = os.environ["GITHUB_REPOSITORY"] self.pr_number = int(os.getenv("GITHUB_PR_NUMBER", 0)) @@ -77,8 +84,8 @@ def verify_base_config(self) -> None: ) def set_gh_config(self) -> None: - gh_client: Github = Github(login_or_token=self.github_token) - self.repo = gh_client.get_repo(full_name_or_id=self.repo_name) + self.gh_client = Github(login_or_token=self.github_token) + self.repo = self.gh_client.get_repo(full_name_or_id=self.repo_name) self.pr = self.repo.get_pull(number=self.pr_number) @@ -87,12 +94,35 @@ def __init__(self) -> None: super().__init__() self.user_login = os.getenv("GITHUB_USER_LOGIN") self.review_state = os.getenv("GITHUB_EVENT_REVIEW_STATE") + # We don't care if the body of the comment is in the discussion page or a review self.comment_body = os.getenv("COMMENT_BODY", "") + if self.comment_body == "": + # if it wasn't a discussion page comment, try to get a review comment, otherwise keep empty + self.comment_body = os.getenv("REVIEW_COMMENT_BODY", "") self.last_commit = list(self.pr.get_commits())[-1] self.last_commit_sha = self.last_commit.sha + self.verify_allowed_user() self.verify_labeler_config() + def get_allowed_users(self) -> list[str]: + org: Organization = self.gh_client.get_organization("opendatahub-io") + # slug is the team name with replaced special characters, + # all words to lowercase and spaces replace with a - + team: Team = org.get_team_by_slug("opendatahub-tests-contributors") + members: PaginatedList[NamedUser] = team.get_members() + users = [member.login for member in members] + # TODO: replace once bot user is part of the org and team + # users = ["lugi0", "rnetser", "adolfo-ab", "tarukumar", "dbasunag", "mwaykole"] + return users + + def verify_allowed_user(self) -> None: + allowed_users = self.get_allowed_users() + if self.user_login not in allowed_users: + LOGGER.info(f"User {self.user_login} is not allowed for this action. Exiting.") + sys.exit(0) + LOGGER.info(f"User {self.user_login} is allowed") + def verify_labeler_config(self) -> None: if self.action == self.SupportedActions.add_remove_labels_action_name and self.event_name in ( "issue_comment", @@ -101,11 +131,11 @@ def verify_labeler_config(self) -> None: if not self.user_login: sys.exit("`GITHUB_USER_LOGIN` is not set") - if self.event_name == "issue_comment" and not self.comment_body: - sys.exit("`COMMENT_BODY` is not set") - - if self.event_name == "pull_request_review" and not self.review_state: - sys.exit("`GITHUB_EVENT_REVIEW_STATE` is not set") + if ( + self.event_name == "issue_comment" or self.event_name == "pull_request_review" + ) and not self.comment_body: + LOGGER.info("No comment, nothing to do. Exiting.") + sys.exit(0) def run_pr_label_action(self) -> None: if self.action == self.SupportedActions.pr_size_action_name: @@ -223,6 +253,7 @@ def add_remove_pr_labels(self) -> None: elif self.event_name == "pull_request_review": self.pull_request_review_label_actions() + self.issue_comment_label_actions() return @@ -239,15 +270,15 @@ def pull_request_review_label_actions( label_to_remove = None label_to_add = None - if self.review_state == "approved": + if self.review_state == APPROVED: label_to_remove = change_requested_label label_to_add = lgtm_label - elif self.review_state == "changes_requested": + elif self.review_state == "CHANGES_REQUESTED": label_to_add = change_requested_label label_to_remove = lgtm_label - elif self.review_state == "commented": + elif self.review_state == "COMMENTED": label_to_add = f"{COMMENTED_BY_LABEL_PREFIX}{self.user_login}" if label_to_add and label_to_add not in self.pr_labels: @@ -276,12 +307,20 @@ def issue_comment_label_actions( LOGGER.info(f"Processing labels: {labels}") for label, action in labels.items(): if label == LGTM_LABEL_STR: - label = f"{LGTM_BY_LABEL_PREFIX}{self.user_login}" + if self.user_login == self.pr.user.login: + LOGGER.info("PR submitter cannot approve for their own PR") + continue + else: + label = f"{LGTM_BY_LABEL_PREFIX}{self.user_login}" + if not action[CANCEL_ACTION] or self.event_action == "deleted": + self.approve_pr() label_in_pr = any([label == _label.lower() for _label in self.pr_labels]) LOGGER.info(f"Processing label: {label}, action: {action}") if action[CANCEL_ACTION] or self.event_action == "deleted": + if label == LGTM_LABEL_STR: + self.dismiss_pr_approval() if label_in_pr: LOGGER.info(f"Removing label {label}") self.pr.remove_from_labels(label=label) @@ -297,6 +336,19 @@ def issue_comment_label_actions( def add_welcome_comment(self) -> None: self.pr.create_issue_comment(body=WELCOME_COMMENT) + def approve_pr(self) -> None: + self.pr.create_review(event="APPROVE") + + def dismiss_pr_approval(self) -> None: + all_reviews = self.pr.get_reviews() + current_user = self.gh_client.get_user().login + LOGGER.info(f"Looking for approving review by user {current_user}") + # The reviews are paginated in chronological order. We need to get the newest by our account + for review in all_reviews.reversed: + if review.user.login == current_user and review.state == APPROVED: + LOGGER.info(f"found review by user {current_user} with id {review.id}") + review.dismiss(message="Dismissing review due to '/lgtm cancel' comment") + def main() -> None: labeler = PrLabeler()