Skip to content

Commit d0403ff

Browse files
Approve a PR by leaving "/lgtm" comment (#153)
* Add Code Owners file Signed-off-by: lugi0 <lgiorgi@redhat.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: add PR auto-approve on '/lgtm' comment Signed-off-by: lugi0 <lgiorgi@redhat.com> * fix: potential fix for label action Signed-off-by: lugi0 <lgiorgi@redhat.com> * fix: add pull request number Signed-off-by: lugi0 <lgiorgi@redhat.com> * fix: extract PR from payload Signed-off-by: lugi0 <lgiorgi@redhat.com> * fix: use bot user personal access token for pr approval Signed-off-by: lugi0 <lgiorgi@redhat.com> * Update lgtm-approval.yml * Update lgtm-approval.yml * Update lgtm-approval.yml * Update lgtm-approval.yml * Update add-remove-labels.yml * Update lgtm-approval.yml * remove trailing whitespace * remove conditional on issue_comment events * Try using review comment as well for labels * fix typo * change review behaviour in py script * enable label action for review comments as well * fix line too long * try adding +1 to review comment with gh api * fix typo * use bot PAT * Disable review reaction for the time being * approval from 'add-remove-labels' * fix typo * additional checks * try using bot PAT * don't fail with empty comment body * add required arg to dismiss * remove unused action * check for allowed users * fix typo, more logging * workaround team membership * fix typo, check for submitter/pr owner * don't add lgtm label for owner of PR * implement org/team logic * fix typo * reuse APPROVED constant * fix type imports for github library * fix typo --------- Signed-off-by: lugi0 <lgiorgi@redhat.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2dbe408 commit d0403ff

File tree

3 files changed

+98
-18
lines changed

3 files changed

+98
-18
lines changed

.github/workflows/add-remove-labels.yml

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ on:
44
types: [synchronize]
55

66
pull_request_review:
7+
types: [submitted, edited]
8+
9+
pull_request_review_comment:
10+
types: [created, edited]
711

812
issue_comment:
913
types: [created, edited, deleted]
10-
if: |
11-
contains(github.event.comment.body, '/wip') ||
12-
contains(github.event.comment.body, '/verified') ||
13-
contains(github.event.comment.body, '/lgtm') ||
14-
contains(github.event.comment.body, '/hold')
14+
# I don't believe the conditional is supported here
15+
# if: |
16+
# contains(github.event.comment.body, '/wip') ||
17+
# contains(github.event.comment.body, '/verified') ||
18+
# contains(github.event.comment.body, '/lgtm') ||
19+
# contains(github.event.comment.body, '/hold')
1520

1621

1722
permissions:
@@ -31,19 +36,41 @@ jobs:
3136
comment-id: ${{ github.event.comment.id }}
3237
reactions: '+1'
3338

39+
# This currently fails with either the bot PAT or the standard github token secret
40+
# gh: Insufficient scopes for reacting to this Pull Request Review Comment. (HTTP 403)
41+
# {"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"}
42+
# It could work if we had a token with the proper permissions.
43+
# See https://github.com/peter-evans/create-or-update-comment/issues/392 for why the action above doesn't work.
44+
# Confirmed as a bug, see: https://github.com/github/docs/issues/36899
45+
# - name: Acknowledge the review with thumbs up reaction
46+
# if: ${{ github.event.review }}
47+
# env:
48+
# GH_TOKEN: ${{ secrets.OPENDATAHUB_TESTS_BOT_PAT }}
49+
# REVIEW_COMMENT_ID: ${{ github.event.review.id }}
50+
# REPO_NAME: ${{ github.event.repository.name }}
51+
# REPO_OWNER: ${{ github.event.repository.owner.login }}
52+
# run: |
53+
# gh api \
54+
# --method POST \
55+
# -H "Accept: application/vnd.github+json" \
56+
# -H "X-GitHub-Api-Version: 2022-11-28" \
57+
# /repos/$REPO_OWNER/$REPO_NAME/pulls/comments/$REVIEW_COMMENT_ID/reactions \
58+
# -f "content=+1"
59+
3460
- uses: actions/checkout@v4
3561

3662
- name: Install uv
3763
uses: astral-sh/setup-uv@v5
3864

3965
- name: Run add remove labels
4066
env:
41-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67+
GITHUB_TOKEN: ${{ secrets.OPENDATAHUB_TESTS_BOT_PAT }}
4268
GITHUB_PR_NUMBER: "${{ github.event.pull_request.number || github.event.issue.number }}"
4369
GITHUB_EVENT_ACTION: ${{ github.event.action }}
4470
GITHUB_EVENT_REVIEW_STATE: ${{ github.event.review.state }}
4571
GITHUB_EVENT_NAME: ${{ github.event_name }}
4672
COMMENT_BODY: ${{ github.event.comment.body }}
73+
REVIEW_COMMENT_BODY: ${{ github.event.review.body }}
4774
GITHUB_USER_LOGIN: ${{ github.event.sender.login }}
4875
ACTION: "add-remove-labels"
4976
run: uv run python .github/workflows/scripts/pr_workflow.py

.github/workflows/scripts/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SUCCESS_STR: str = "success"
1212
FAILURE_STR: str = "failure"
1313
QUEUED_STR: str = "queued"
14+
APPROVED: str = "APPROVED"
1415

1516
SUPPORTED_LABELS: set[str] = {
1617
f"{LABEL_PREFIX}{WIP_LABEL_STR}",

.github/workflows/scripts/pr_workflow.py

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
from github.PullRequest import PullRequest
88
from github.Repository import Repository
9+
from github.PaginatedList import PaginatedList
10+
from github.MainClass import Github
11+
from github.GithubException import UnknownObjectException
12+
from github.Organization import Organization
13+
from github.Team import Team
14+
from github.NamedUser import NamedUser
915

1016
from constants import (
1117
ALL_LABELS_DICT,
@@ -20,8 +26,8 @@
2026
SUPPORTED_LABELS,
2127
VERIFIED_LABEL_STR,
2228
WELCOME_COMMENT,
29+
APPROVED,
2330
)
24-
from github import Github, UnknownObjectException
2531
from simple_logger.logger import get_logger
2632

2733
LOGGER = get_logger(name="pr_labeler")
@@ -41,6 +47,7 @@ class SupportedActions:
4147
def __init__(self) -> None:
4248
self.repo: Repository
4349
self.pr: PullRequest
50+
self.gh_client: Github
4451

4552
self.repo_name = os.environ["GITHUB_REPOSITORY"]
4653
self.pr_number = int(os.getenv("GITHUB_PR_NUMBER", 0))
@@ -77,8 +84,8 @@ def verify_base_config(self) -> None:
7784
)
7885

7986
def set_gh_config(self) -> None:
80-
gh_client: Github = Github(login_or_token=self.github_token)
81-
self.repo = gh_client.get_repo(full_name_or_id=self.repo_name)
87+
self.gh_client = Github(login_or_token=self.github_token)
88+
self.repo = self.gh_client.get_repo(full_name_or_id=self.repo_name)
8289
self.pr = self.repo.get_pull(number=self.pr_number)
8390

8491

@@ -87,12 +94,35 @@ def __init__(self) -> None:
8794
super().__init__()
8895
self.user_login = os.getenv("GITHUB_USER_LOGIN")
8996
self.review_state = os.getenv("GITHUB_EVENT_REVIEW_STATE")
97+
# We don't care if the body of the comment is in the discussion page or a review
9098
self.comment_body = os.getenv("COMMENT_BODY", "")
99+
if self.comment_body == "":
100+
# if it wasn't a discussion page comment, try to get a review comment, otherwise keep empty
101+
self.comment_body = os.getenv("REVIEW_COMMENT_BODY", "")
91102
self.last_commit = list(self.pr.get_commits())[-1]
92103
self.last_commit_sha = self.last_commit.sha
93104

105+
self.verify_allowed_user()
94106
self.verify_labeler_config()
95107

108+
def get_allowed_users(self) -> list[str]:
109+
org: Organization = self.gh_client.get_organization("opendatahub-io")
110+
# slug is the team name with replaced special characters,
111+
# all words to lowercase and spaces replace with a -
112+
team: Team = org.get_team_by_slug("opendatahub-tests-contributors")
113+
members: PaginatedList[NamedUser] = team.get_members()
114+
users = [member.login for member in members]
115+
# TODO: replace once bot user is part of the org and team
116+
# users = ["lugi0", "rnetser", "adolfo-ab", "tarukumar", "dbasunag", "mwaykole"]
117+
return users
118+
119+
def verify_allowed_user(self) -> None:
120+
allowed_users = self.get_allowed_users()
121+
if self.user_login not in allowed_users:
122+
LOGGER.info(f"User {self.user_login} is not allowed for this action. Exiting.")
123+
sys.exit(0)
124+
LOGGER.info(f"User {self.user_login} is allowed")
125+
96126
def verify_labeler_config(self) -> None:
97127
if self.action == self.SupportedActions.add_remove_labels_action_name and self.event_name in (
98128
"issue_comment",
@@ -101,11 +131,11 @@ def verify_labeler_config(self) -> None:
101131
if not self.user_login:
102132
sys.exit("`GITHUB_USER_LOGIN` is not set")
103133

104-
if self.event_name == "issue_comment" and not self.comment_body:
105-
sys.exit("`COMMENT_BODY` is not set")
106-
107-
if self.event_name == "pull_request_review" and not self.review_state:
108-
sys.exit("`GITHUB_EVENT_REVIEW_STATE` is not set")
134+
if (
135+
self.event_name == "issue_comment" or self.event_name == "pull_request_review"
136+
) and not self.comment_body:
137+
LOGGER.info("No comment, nothing to do. Exiting.")
138+
sys.exit(0)
109139

110140
def run_pr_label_action(self) -> None:
111141
if self.action == self.SupportedActions.pr_size_action_name:
@@ -223,6 +253,7 @@ def add_remove_pr_labels(self) -> None:
223253

224254
elif self.event_name == "pull_request_review":
225255
self.pull_request_review_label_actions()
256+
self.issue_comment_label_actions()
226257

227258
return
228259

@@ -239,15 +270,15 @@ def pull_request_review_label_actions(
239270
label_to_remove = None
240271
label_to_add = None
241272

242-
if self.review_state == "approved":
273+
if self.review_state == APPROVED:
243274
label_to_remove = change_requested_label
244275
label_to_add = lgtm_label
245276

246-
elif self.review_state == "changes_requested":
277+
elif self.review_state == "CHANGES_REQUESTED":
247278
label_to_add = change_requested_label
248279
label_to_remove = lgtm_label
249280

250-
elif self.review_state == "commented":
281+
elif self.review_state == "COMMENTED":
251282
label_to_add = f"{COMMENTED_BY_LABEL_PREFIX}{self.user_login}"
252283

253284
if label_to_add and label_to_add not in self.pr_labels:
@@ -276,12 +307,20 @@ def issue_comment_label_actions(
276307
LOGGER.info(f"Processing labels: {labels}")
277308
for label, action in labels.items():
278309
if label == LGTM_LABEL_STR:
279-
label = f"{LGTM_BY_LABEL_PREFIX}{self.user_login}"
310+
if self.user_login == self.pr.user.login:
311+
LOGGER.info("PR submitter cannot approve for their own PR")
312+
continue
313+
else:
314+
label = f"{LGTM_BY_LABEL_PREFIX}{self.user_login}"
315+
if not action[CANCEL_ACTION] or self.event_action == "deleted":
316+
self.approve_pr()
280317

281318
label_in_pr = any([label == _label.lower() for _label in self.pr_labels])
282319
LOGGER.info(f"Processing label: {label}, action: {action}")
283320

284321
if action[CANCEL_ACTION] or self.event_action == "deleted":
322+
if label == LGTM_LABEL_STR:
323+
self.dismiss_pr_approval()
285324
if label_in_pr:
286325
LOGGER.info(f"Removing label {label}")
287326
self.pr.remove_from_labels(label=label)
@@ -301,6 +340,19 @@ def add_welcome_comment_set_assignee(self) -> None:
301340
except UnknownObjectException:
302341
LOGGER.warning(f"User {self.pr.user.login} can not be assigned to the PR.")
303342

343+
def approve_pr(self) -> None:
344+
self.pr.create_review(event="APPROVE")
345+
346+
def dismiss_pr_approval(self) -> None:
347+
all_reviews = self.pr.get_reviews()
348+
current_user = self.gh_client.get_user().login
349+
LOGGER.info(f"Looking for approving review by user {current_user}")
350+
# The reviews are paginated in chronological order. We need to get the newest by our account
351+
for review in all_reviews.reversed:
352+
if review.user.login == current_user and review.state == APPROVED:
353+
LOGGER.info(f"found review by user {current_user} with id {review.id}")
354+
review.dismiss(message="Dismissing review due to '/lgtm cancel' comment")
355+
304356

305357
def main() -> None:
306358
labeler = PrLabeler()

0 commit comments

Comments
 (0)