-
-
Notifications
You must be signed in to change notification settings - Fork 18
Reduce PR Review API calls with GraphQL #654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9b16b88
e3ca164
97e8373
fcc4ff0
82a5e56
2cd1a8f
225b5ae
17ecbf6
d677962
ed2c359
f8b7d20
96351b5
98e356e
21bc3c1
0401e63
4fbec37
d4e5844
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,6 +97,56 @@ | |
| } | ||
| """ | ||
|
|
||
| GRAPHQL_PR_DETAILS = """ | ||
| query($owner: String!, $repo: String!, $number: Int!) { | ||
| repository(owner: $owner, name: $repo) { | ||
| pullRequest(number: $number) { | ||
| body | ||
| } | ||
| labels(first: 100) { | ||
| nodes { | ||
| name | ||
| description | ||
| } | ||
| } | ||
| } | ||
| } | ||
| """ # Note: Limited to first 100 labels. Sufficient for most repos; add pagination if needed. | ||
|
|
||
| GRAPHQL_PR_REVIEWS_COMMENTS = """ | ||
| query($owner: String!, $repo: String!, $number: Int!, $botLogin: String!) { | ||
| repository(owner: $owner, name: $repo) { | ||
| pullRequest(number: $number) { | ||
| reviews(first: 100, author: $botLogin) { | ||
| nodes { | ||
| databaseId | ||
| state | ||
| body | ||
| author { login } | ||
| comments(last: 100) { | ||
| nodes { | ||
| databaseId | ||
| author { login } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| """ | ||
|
|
||
| GRAPHQL_LABEL_AND_COMMENT_ISSUE = """ | ||
| mutation($issueId: ID!, $labelIds: [ID!]!, $body: String!) { | ||
| addLabelsToLabelable(input: {labelableId: $issueId, labelIds: $labelIds}) { | ||
| labelable { id } | ||
| } | ||
| addComment(input: {subjectId: $issueId, body: $body}) { | ||
| commentEdge { node { id } } | ||
| } | ||
| } | ||
| """ | ||
|
|
||
|
|
||
| class Action: | ||
| """Handles GitHub Actions API interactions and event processing.""" | ||
|
|
@@ -274,27 +324,29 @@ def graphql_request(self, query: str, variables: dict | None = None) -> dict: | |
| print(result.get("errors")) | ||
| return result | ||
|
|
||
| def update_pr_description(self, number: int, new_summary: str, max_retries: int = 2): | ||
| """Updates PR description with summary, retrying if description is None.""" | ||
| def update_pr_description(self, number: int, new_summary: str, current_body: str | None = None): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π‘ MEDIUM: |
||
| """Updates PR description with summary, uses cached body if provided to avoid redundant API calls.""" | ||
| import time | ||
|
|
||
| url = f"{GITHUB_API_URL}/repos/{self.repository}/pulls/{number}" | ||
| description = "" | ||
| for i in range(max_retries + 1): | ||
| description = self.get(url).json().get("body") or "" | ||
| if description: | ||
| break | ||
| if i < max_retries: | ||
| print("No current PR description found, retrying...") | ||
| time.sleep(1) | ||
| if current_body is None: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π LOW: Inside |
||
| for i in range(3): | ||
| current_body = self.get(url).json().get("body") or "" | ||
| if current_body: | ||
| break | ||
| if i < 2: | ||
| print("No current PR description found, retrying...") | ||
| time.sleep(1) | ||
|
|
||
| start = "## π οΈ PR Summary" | ||
| if start in description: | ||
| if start in current_body: | ||
| print("Existing PR Summary found, replacing.") | ||
| updated_description = description.split(start)[0].rstrip() + "\n\n" + new_summary | ||
| updated_description = current_body.split(start)[0].rstrip() + "\n\n" + new_summary | ||
| else: | ||
| print("PR Summary not found, appending.") | ||
| updated_description = (description.rstrip() + "\n\n" + new_summary) if description.strip() else new_summary | ||
| updated_description = ( | ||
| (current_body.rstrip() + "\n\n" + new_summary) if current_body.strip() else new_summary | ||
| ) | ||
|
|
||
| self.patch(url, json={"body": updated_description}) | ||
|
|
||
|
|
@@ -398,6 +450,71 @@ def handle_alert(self, number: int, node_id: str, issue_type: str, username: str | |
| if block: | ||
| self.block_user(username) | ||
|
|
||
| def get_pr_details_and_labels(self, pr_number: int) -> dict: | ||
| """Gets PR details and repository labels in a single GraphQL query.""" | ||
| owner, repo = self.repository.split("/") | ||
| variables = {"owner": owner, "repo": repo, "number": pr_number} | ||
| result = self.graphql_request(GRAPHQL_PR_DETAILS, variables=variables) | ||
| return result.get("data", {}).get("repository", {}) | ||
|
|
||
| def get_pr_reviews_and_comments(self, pr_number: int) -> tuple[list, list]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π LOW: |
||
| """Gets PR reviews and comments by bot in a single GraphQL query.""" | ||
| owner, repo = self.repository.split("/") | ||
| bot_login = self.get_username() | ||
| if not bot_login: | ||
| return [], [] | ||
|
|
||
| variables = {"owner": owner, "repo": repo, "number": pr_number, "botLogin": bot_login} | ||
| result = self.graphql_request(GRAPHQL_PR_REVIEWS_COMMENTS, variables=variables) | ||
|
|
||
| pr_data = result.get("data", {}).get("repository", {}).get("pullRequest", {}) | ||
| reviews = pr_data.get("reviews", {}).get("nodes", []) | ||
|
|
||
| # Flatten comments from all reviews | ||
| comments = [] | ||
| for review in reviews: | ||
| comments.extend(review.get("comments", {}).get("nodes", [])) | ||
|
|
||
| return reviews, comments | ||
|
|
||
| def batch_delete_review_comments(self, comment_ids: list[int]) -> None: | ||
| """Delete PR review comments using REST API (GraphQL batch mutations hit complexity limits).""" | ||
| for comment_id in comment_ids: | ||
| self.delete(f"{GITHUB_API_URL}/repos/{self.repository}/pulls/comments/{comment_id}") | ||
|
|
||
| def get_multiple_issue_node_ids(self, issue_numbers: list[int]) -> dict[int, str]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π LOW: |
||
| """Gets multiple issue node IDs in a single GraphQL query.""" | ||
| if not issue_numbers: | ||
| return {} | ||
|
|
||
| owner, repo = self.repository.split("/") | ||
|
|
||
| # Build parameterized query to avoid injection risks | ||
| query_parts = [] | ||
| for i in range(len(issue_numbers)): | ||
| query_parts.append(f"issue{i}: issue(number: $num{i}) {{ id }}") | ||
|
|
||
| variables = {f"num{i}": num for i, num in enumerate(issue_numbers)} | ||
| variables["owner"] = owner | ||
| variables["repo"] = repo | ||
|
|
||
| # Build variable definitions | ||
| var_defs = ["$owner: String!", "$repo: String!"] + [f"$num{i}: Int!" for i in range(len(issue_numbers))] | ||
|
|
||
| query = f"""query({", ".join(var_defs)}) {{ | ||
| repository(owner: $owner, name: $repo) {{ | ||
| {" ".join(query_parts)} | ||
| }} | ||
| }}""" | ||
|
|
||
| result = self.graphql_request(query, variables) | ||
| repo_data = result.get("data", {}).get("repository", {}) | ||
| return { | ||
| num: repo_data.get(f"issue{i}", {}).get("id") | ||
| for i, num in enumerate(issue_numbers) | ||
| if repo_data.get(f"issue{i}") | ||
| } | ||
|
|
||
| def get_pr_contributors(self) -> tuple[str | None, dict]: | ||
| """Gets PR contributors and closing issues, returns (pr_credit_string, pr_data).""" | ||
| variables = {"owner": self.owner, "repo": self.repo_name, "pr_number": self.pr["number"]} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π LOW:
dismiss_previous_reviewsnow callsevent.get_username()directly and then indirectly again viaevent.get_pr_reviews_and_comments(). This double lookup is minor but unnecessary overhead on every run, and also duplicates theNonehandling logic in two places. Consider passing the already-resolvedbot_usernameintoget_pr_reviews_and_comments(or having it accept an optional precomputed login) to avoid redundant API calls and keep the responsibility for username resolution in one place.