[Bug, GitHub]: fix missing context in cloud resolver (#10517)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra 2025-08-27 03:07:09 -04:00 committed by GitHub
parent c0bb84dfa2
commit 9afedea170
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 364 additions and 46 deletions

View File

@ -9,12 +9,16 @@ from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.github.queries import (
get_review_threads_graphql_query,
get_thread_comments_graphql_query,
get_thread_from_comment_graphql_query,
suggested_task_issue_graphql_query,
suggested_task_pr_graphql_query,
)
from openhands.integrations.service_types import (
BaseGitService,
Branch,
Comment,
GitService,
InstallationsService,
OwnerType,
@ -695,6 +699,222 @@ class GitHubService(BaseGitService, GitService, InstallationsService):
# Parse the content to extract triggers from frontmatter
return self._parse_microagent_content(file_content, file_path)
async def get_issue_or_pr_comments(
self, repository: str, issue_number: int, max_comments: int = 10
) -> list[Comment]:
"""Get comments for an issue.
Args:
repository: Repository name in format 'owner/repo'
issue_number: The issue number
discussion_id: Not used for GitHub (kept for compatibility with GitLab)
Returns:
List of Comment objects ordered by creation date
"""
url = f'{self.BASE_URL}/repos/{repository}/issues/{issue_number}/comments'
page = 1
all_comments: list[dict] = []
while len(all_comments) < max_comments:
params = {
'per_page': 10,
'sort': 'created',
'direction': 'asc',
'page': page,
}
response, headers = await self._make_request(url, params=params)
all_comments.extend(response or [])
# Parse the Link header for rel="next"
link_header = headers.get('Link', '')
if 'rel="next"' not in link_header:
break
page += 1
return self._process_raw_comments(all_comments)
async def get_issue_or_pr_title_and_body(
self, repository: str, issue_number: int
) -> tuple[str, str]:
"""Get the title and body of an issue.
Args:
repository: Repository name in format 'owner/repo'
issue_number: The issue number
Returns:
A tuple of (title, body)
"""
url = f'{self.BASE_URL}/repos/{repository}/issues/{issue_number}'
response, _ = await self._make_request(url)
title = response.get('title') or ''
body = response.get('body') or ''
return title, body
async def get_review_thread_comments(
self,
comment_id: str,
repository: str,
pr_number: int,
) -> list[Comment]:
"""Get all comments in a review thread starting from a specific comment.
Uses GraphQL to traverse the reply chain from the given comment up to the root
comment, then finds the review thread and returns all comments in the thread.
Args:
comment_id: The GraphQL node ID of any comment in the thread
repo: Repository name
pr_number: Pull request number
Returns:
List of Comment objects representing the entire thread
"""
# Step 1: Use existing GraphQL query to get the comment and check for replyTo
variables = {'commentId': comment_id}
data = await self.execute_graphql_query(
get_thread_from_comment_graphql_query, variables
)
comment_node = data.get('data', {}).get('node')
if not comment_node:
return []
# Step 2: If replyTo exists, traverse to the root comment
root_comment_id = comment_id
reply_to = comment_node.get('replyTo')
if reply_to:
root_comment_id = reply_to['id']
# Step 3: Get all review threads and find the one containing our root comment
owner, repo = repository.split('/')
thread_id = None
after_cursor = None
has_next_page = True
while has_next_page and not thread_id:
threads_variables: dict[str, Any] = {
'owner': owner,
'repo': repo,
'number': pr_number,
'first': 50,
}
if after_cursor:
threads_variables['after'] = after_cursor
threads_data = await self.execute_graphql_query(
get_review_threads_graphql_query, threads_variables
)
review_threads_data = (
threads_data.get('data', {})
.get('repository', {})
.get('pullRequest', {})
.get('reviewThreads', {})
)
review_threads = review_threads_data.get('nodes', [])
page_info = review_threads_data.get('pageInfo', {})
# Search for the thread containing our root comment
for thread in review_threads:
first_comments = thread.get('comments', {}).get('nodes', [])
for first_comment in first_comments:
if first_comment.get('id') == root_comment_id:
thread_id = thread.get('id')
break
if thread_id:
break
# Update pagination variables
has_next_page = page_info.get('hasNextPage', False)
after_cursor = page_info.get('endCursor')
if not thread_id:
# Fallback: return just the comments we found during traversal
logger.warning(
f'Could not find review thread for comment {comment_id}, returning traversed comments'
)
return []
# Step 4: Get all comments from the review thread using the thread ID
all_thread_comments = []
after_cursor = None
has_next_page = True
while has_next_page:
comments_variables: dict[str, Any] = {}
comments_variables['threadId'] = thread_id
comments_variables['page'] = 50
if after_cursor:
comments_variables['after'] = after_cursor
thread_comments_data = await self.execute_graphql_query(
get_thread_comments_graphql_query, comments_variables
)
thread_node = thread_comments_data.get('data', {}).get('node')
if not thread_node:
break
comments_data = thread_node.get('comments', {})
comments_nodes = comments_data.get('nodes', [])
page_info = comments_data.get('pageInfo', {})
all_thread_comments.extend(comments_nodes)
has_next_page = page_info.get('hasNextPage', False)
after_cursor = page_info.get('endCursor')
return self._process_raw_comments(all_thread_comments)
def _truncate_comment(
self, comment_body: str, max_comment_length: int = 500
) -> str:
"""Truncate comment body to a maximum length."""
if len(comment_body) > max_comment_length:
return comment_body[:max_comment_length] + '...'
return comment_body
def _process_raw_comments(
self, comments_data: list, max_comments: int = 10
) -> list[Comment]:
"""Convert raw comment data to Comment objects."""
comments: list[Comment] = []
for comment in comments_data:
author = 'unknown'
if comment.get('author'):
author = comment.get('author', {}).get('login', 'unknown')
elif comment.get('user'):
author = comment.get('user', {}).get('login', 'unknown')
comments.append(
Comment(
id=str(comment.get('id', 'unknown')),
body=self._truncate_comment(comment.get('body', '')),
author=author,
created_at=datetime.fromisoformat(
comment.get('createdAt', '').replace('Z', '+00:00')
)
if comment.get('createdAt')
else datetime.fromtimestamp(0),
updated_at=datetime.fromisoformat(
comment.get('updatedAt', '').replace('Z', '+00:00')
)
if comment.get('updatedAt')
else datetime.fromtimestamp(0),
system=False,
)
)
# Sort comments by creation date to maintain chronological order
comments.sort(key=lambda c: c.created_at)
return comments[-max_comments:]
github_service_cls = os.environ.get(
'OPENHANDS_GITHUB_SERVICE_CLS',

View File

@ -45,3 +45,80 @@ suggested_task_issue_graphql_query = """
}
}
"""
get_thread_from_comment_graphql_query = """
query GetThreadFromComment($commentId: ID!) {
node(id: $commentId) {
... on PullRequestReviewComment {
id
body
author {
login
}
createdAt
updatedAt
replyTo {
id
body
author {
login
}
createdAt
updatedAt
}
}
}
}
"""
get_review_threads_graphql_query = """
query($owner: String!, $repo: String!, $number: Int!, $first: Int = 50, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: $first, after: $after) {
nodes {
id
path
isResolved
comments(first: 1) {
nodes {
id
databaseId
body
author {
login
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
"""
get_thread_comments_graphql_query = """
query ($threadId: ID!, $page: Int = 50, $after: String) {
node(id: $threadId) {
... on PullRequestReviewThread {
id
path
isResolved
comments(first: $page, after: $after) {
nodes {
id
databaseId
body
author { login }
createdAt
}
pageInfo { hasNextPage endCursor }
}
}
}
}
"""

View File

@ -727,7 +727,7 @@ class GitLabService(BaseGitService, GitService):
continue
comment = Comment(
id=comment_data['id'],
id=str(comment_data['id']),
body=comment_data['body'],
author=comment_data.get('author', {}).get('username', 'unknown'),
created_at=datetime.fromisoformat(

View File

@ -141,7 +141,7 @@ class Repository(BaseModel):
class Comment(BaseModel):
id: int
id: str
body: str
author: str
created_at: datetime

View File

@ -1,22 +0,0 @@
You are requested to fix issue number #{{ issue_number }} in a repository.
A comment on the issue has been addressed to you.
# Steps to Handle the Comment
1. Address the comment. Use the $GITHUB_TOKEN and GitHub API to read issue title, body, and comments if you need more context
2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed
3. Run the tests, and if they pass you are done!
4. You do NOT need to write new tests if there are only changes to documentation or configuration files.
When you're done, make sure to
1. Re-read the issue title, body, and comments and make sure that you have successfully implemented all requirements.
2. Create a new branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
3. Commit your changes with a clear commit message
4. Push the branch to GitHub
5. Use the `create_pr` tool to open a new PR
6. The PR description should:
- Follow the repository's PR template (check `.github/pull_request_template.md` if it exists)
- Mention that it "fixes" or "closes" the issue number
- Include all required sections from the template

View File

@ -1 +0,0 @@
{{ issue_comment }}

View File

@ -0,0 +1,41 @@
{% if issue_number %}
You are requested to fix issue #{{ issue_number }}: "{{ issue_title }}" in a repository.
A comment on the issue has been addressed to you.
{% else %}
Your task is to fix the issue: "{{ issue_title }}".
{% endif %}
# Issue Body
{{ issue_body }}
{% if previous_comments %}
# Previous Comments
For reference, here are the previous comments on the issue:
{% for comment in previous_comments %}
- @{{ comment.author }} said:
{{ comment.body }}
{% if not loop.last %}\n\n{% endif %}
{% endfor %}
{% endif %}
# Guidelines
1. Review the task carefully.
2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed
3. Run the tests, and if they pass you are done!
4. You do NOT need to write new tests if there are only changes to documentation or configuration files.
# Final Checklist
Re-read the issue title, body, and comments and make sure that you have successfully implemented all requirements.
Use the $GITHUB_TOKEN and GitHub APIs to
1. Create a new branch using `openhands/` as a prefix (e.g `openhands/update-readme`)
2. Commit your changes with a clear commit message
3. Push the branch to GitHub
4. Use the `create_pr` tool to open a new PR
5. The PR description should:
- Follow the repository's PR template (check `.github/pull_request_template.md` if it exists)
- Mention that it "fixes" or "closes" the issue number
- Include all required sections from the template

View File

@ -1,17 +0,0 @@
Your tasking is to fix an issue in your repository. Do the following
1. Read the issue body and comments using the $GITHUB_TOKEN and Github API
2. For all changes to actual application code (e.g. in Python or Javascript), add an appropriate test to the testing directory to make sure that the issue has been fixed
3. Run the tests, and if they pass you are done!
4. You do NOT need to write new tests if there are only changes to documentation or configuration files.
When you're done, make sure to
1. Create a new branch with a descriptive name (e.g., `openhands/fix-issue-123`)
2. Commit your changes with a clear commit message
3. Push the branch to GitHub
4. Use the `create_pr` tool to open a new PR
5. The PR description should:
- Follow the repository's PR template (check `.github/pull_request_template.md` if it exists)
- Mention that it "fixes" or "closes" the issue number
- Include all required sections from the template

View File

@ -1 +0,0 @@
Please fix issue number #{{ issue_number }} in your repository.

View File

@ -0,0 +1,5 @@
{% if issue_comment %}
{{ issue_comment }}
{% else %}
Please fix issue number #{{ issue_number }}.
{% endif %}

View File

@ -1,7 +1,23 @@
You are checked out to branch {{ branch_name }}, which has an open PR #{{ pr_number }}.
A comment on the PR has been addressed to you. Do NOT respond to this comment via the GitHub API.
You are checked out to branch {{ branch_name }}, which has an open PR #{{ pr_number }}: "{{ pr_title }}".
A comment on the PR has been addressed to you.
{% if file_location %} The comment is in the file `{{ file_location }}` on line #{{ line_number }}{% endif %}.
# PR Description
{{ pr_body }}
{% if comments %}
# Previous Comments
You may find these other comments relevant:
{% for comment in comments %}
- @{{ comment.author }} said at {{ comment.created_at }}:
{{ comment.body }}
{% if not loop.last %}\n\n{% endif %}
{% endfor %}
{% endif %}
{% if file_location %}
# Comment location
The comment is in the file `{{ file_location }}` on line #{{ line_number }}
{% endif %}.
# Steps to Handle the Comment