Skip to content
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

Fix issue #5448: When using the GitHub resolver on a PR, automatically pipe in failure info #5449

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions investigate_pr_14.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import os
import logging
import traceback
from openhands.resolver.issue_definitions import PRHandler
from openhands.core.config import LLMConfig
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the inclusion of this file intended?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing not, but this file is great! I wonder if we could convert it into an integration test 🤔


# Set up logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Modify the PRHandler class to add logging
class DebugPRHandler(PRHandler):
def __get_pr_status(self, pull_number: int):
logger.debug(f"Fetching PR status for PR #{pull_number}")
try:
result = super().__get_pr_status(pull_number)
logger.debug(f"PR status result: {result}")

# Print detailed information about the API response
print(f"Detailed PR status for PR #{pull_number}:")
print(f"Mergeable status: {result[0]}")
print("Failed checks:")
for check in result[1]:
print(f" Name: {check['name']}")
print(f" Description: {check['description']}")
print(" ---")

return result
except Exception as e:
logger.error(f"Error in __get_pr_status: {str(e)}")
logger.debug(traceback.format_exc())
raise

def get_instruction(self, issue, prompt_template, repo_instruction=None):
logger.debug(f"Generating instruction for PR #{issue.number}")
logger.debug(f"Prompt template: {prompt_template}")
logger.debug(f"Issue data: {issue}")
try:
result = super().get_instruction(issue, prompt_template, repo_instruction)
logger.debug(f"Generated instruction: {result[0]}")
return result
except Exception as e:
logger.error(f"Error in get_instruction: {str(e)}")
logger.debug(traceback.format_exc())
raise

def get_converted_issues(self, issue_numbers=None, comment_id=None):
logger.debug(f"Getting converted issues: {issue_numbers}")
try:
result = super().get_converted_issues(issue_numbers, comment_id)
logger.debug(f"Converted issues: {result}")

# Print detailed information about each converted issue
for issue in result:
print(f"\nDetailed information for PR #{issue.number}:")
print(f"Title: {issue.title}")
print(f"Merge conflicts: {issue.has_merge_conflicts}")
print("Failed checks:")
if issue.failed_checks:
for check in issue.failed_checks:
print(f" Name: {check['name']}")
print(f" Description: {check['description']}")
print(" ---")
else:
print(" No failed checks")

return result
except Exception as e:
logger.error(f"Error in get_converted_issues: {str(e)}")
logger.debug(traceback.format_exc())
raise

def main():
try:
print("Starting main function")
# GitHub token
github_token = os.environ.get('GITHUB_TOKEN')
if not github_token:
raise ValueError("GITHUB_TOKEN environment variable is not set")
print("GitHub token retrieved")

# Create PRHandler instance
llm_config = LLMConfig(model='gpt-3.5-turbo') # Adjust as needed
pr_handler = DebugPRHandler('neubig', 'pr-viewer', github_token, llm_config)
print("PRHandler instance created")

# Call __get_pr_status directly
print("Calling __get_pr_status for PR #14")
has_conflicts, failed_checks = pr_handler._PRHandler__get_pr_status(14)
print(f"PR #14 status: has_conflicts={has_conflicts}, failed_checks={failed_checks}")

# Fetch PR #14
print("Fetching PR #14")
issues = pr_handler.get_converted_issues([14])
if not issues:
logger.error("Failed to fetch PR #14")
return
print(f"Fetched {len(issues)} issues")

pr = issues[0]
print(f"PR data: {pr}")

# Generate instruction
prompt_template = """
{{ pr_status }}

{{ body }}

{% if review_comments %}
Review Comments:
{{ review_comments }}
{% endif %}

{% if review_threads %}
Review Threads:
{{ review_threads }}
{% endif %}

{% if files %}
Files:
{{ files }}
{% endif %}

{% if thread_context %}
Thread Context:
{{ thread_context }}
{% endif %}

{% if repo_instruction %}
Repository Instruction:
{{ repo_instruction }}
{% endif %}
"""

print("Generating instruction")
instruction, _ = pr_handler.get_instruction(pr, prompt_template)
print("Instruction generated")
print(f"Final instruction:\n{instruction}")

except Exception as e:
print(f"An error occurred in main: {str(e)}")
print(traceback.format_exc())

if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"An error occurred in main: {str(e)}")
print(traceback.format_exc())
2 changes: 2 additions & 0 deletions openhands/resolver/github_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ class GithubIssue(BaseModel):
review_threads: list[ReviewThread] | None = None
thread_ids: list[str] | None = None
head_branch: str | None = None
has_merge_conflicts: bool | None = None
failed_checks: list[dict[str, str | None]] | None = None
166 changes: 166 additions & 0 deletions openhands/resolver/issue_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,129 @@ def __init__(self, owner: str, repo: str, token: str, llm_config: LLMConfig):
super().__init__(owner, repo, token, llm_config)
self.download_url = 'https://api.github.com/repos/{}/{}/pulls'

def __get_pr_status(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really intentional to use a double underscore here?

self, pull_number: int
) -> tuple[bool | None, list[dict[str, str | None]]]:
"""Get the PR's merge conflict status and CI check status.

Args:
pull_number: The number of the pull request to query.

Returns:
A tuple containing:
- bool | None: Whether the PR has merge conflicts (None if unknown)
- list[dict[str, str | None]]: List of failed CI checks with their details (empty list if no failures)
"""
query = """
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
mergeable
commits(last: 1) {
nodes {
commit {
statusCheckRollup {
contexts(first: 100) {
nodes {
... on StatusContext {
context
state
description
}
... on CheckRun {
name
conclusion
text
summary
title
}
}
}
}
}
}
}
}
}
}
"""

variables = {'owner': self.owner, 'repo': self.repo, 'pr': pull_number}
url = 'https://api.github.com/graphql'
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
}

response = requests.post(
url, json={'query': query, 'variables': variables}, headers=headers
)
response.raise_for_status()
response_json = response.json()

# Print raw API response for debugging
print(f'Raw API response for PR #{pull_number}:')
print(json.dumps(response_json, indent=2))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need these prints anymore


pr_data = (
response_json.get('data', {}).get('repository', {}).get('pullRequest', {})
)

# Check mergeable status
mergeable = pr_data.get('mergeable')
has_conflicts = None if mergeable == 'UNKNOWN' else (mergeable == 'CONFLICTING')

# Check CI status
failed_checks = []
seen_checks = set() # Track seen check names to avoid duplicates
commits = pr_data.get('commits', {}).get('nodes', [])
if commits:
status_rollup = commits[0].get('commit', {}).get('statusCheckRollup', {})
contexts = status_rollup.get('contexts', {}).get('nodes', [])

for context in contexts:
# Handle both StatusContext and CheckRun types
if 'state' in context: # StatusContext
if context['state'] in ['FAILURE', 'ERROR']:
name = context['context']
if name not in seen_checks:
seen_checks.add(name)
failed_checks.append(
{
'name': name,
'description': context.get('description')
or 'No description provided',
}
)
elif 'conclusion' in context: # CheckRun
if context['conclusion'] in [
'FAILURE',
'TIMED_OUT',
'CANCELLED',
'ACTION_REQUIRED',
]:
name = context['name']
if name not in seen_checks:
seen_checks.add(name)
description = (
context.get('text')
or context.get('summary')
or context.get('title')
or 'No description provided'
)
failed_checks.append(
{
'name': name,
'description': description,
}
)

print(f'Processed PR status for PR #{pull_number}:')
print(f'has_conflicts: {has_conflicts}')
print(f'failed_checks: {failed_checks}')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too, I suppose?


return has_conflicts, failed_checks

def __download_pr_metadata(
self, pull_number: int, comment_id: int | None = None
) -> tuple[list[str], list[int], list[str], list[ReviewThread], list[str]]:
Expand Down Expand Up @@ -571,6 +694,9 @@ def get_converted_issues(
issue['number'], comment_id=comment_id
)

# Get PR status (merge conflicts and CI checks)
has_conflicts, failed_checks = self.__get_pr_status(issue['number'])

closing_issues = self.__get_context_from_external_issues_references(
closing_issues,
closing_issues_numbers,
Expand All @@ -592,6 +718,8 @@ def get_converted_issues(
thread_ids=thread_ids,
head_branch=head_branch,
thread_comments=thread_comments,
has_merge_conflicts=has_conflicts,
failed_checks=failed_checks,
)

converted_issues.append(issue_details)
Expand Down Expand Up @@ -639,13 +767,51 @@ def get_instruction(
thread_context = '\n---\n'.join(issue.thread_comments)
images.extend(self._extract_image_urls(thread_context))

# Add PR status information
pr_status = ''
if issue.has_merge_conflicts is None:
pr_status += 'The merge status of this PR is currently unknown or pending.'
elif issue.has_merge_conflicts:
pr_status += 'This PR has merge conflicts that need to be resolved.'
elif not issue.has_merge_conflicts:
pr_status += 'This PR has no merge conflicts'

if issue.failed_checks is None:
pr_status += '\nThe CI check status is currently unknown or pending.'
elif issue.failed_checks:
pr_status += '\nThe following CI checks have failed:\n'
for check in issue.failed_checks:
pr_status += f"- {check['name']}: {check['description']}\n"
pr_status += 'Please examine the GitHub workflow files, reproduce the problem locally, and fix and test it locally.'

# Add note about running tests locally
pr_status += '\nPlease run the failing checks locally to fix the issues.'
elif not issue.failed_checks:
if pr_status:
pr_status += ' and all CI checks have passed.'
else:
pr_status += (
'This PR has no merge conflicts and all CI checks have passed.'
)

# Add a note about the lack of detailed information
if issue.failed_checks and all(
check['description'] == 'No description provided'
for check in issue.failed_checks
):
pr_status += '\n\nNote: Detailed information about the failed checks is not available. Please check the GitHub Actions tab for more information.'

# Remove leading newline if present
pr_status = pr_status.lstrip()

instruction = template.render(
issues=issues_str,
review_comments=review_comments_str,
review_threads=review_thread_str,
files=review_thread_file_str,
thread_context=thread_context,
repo_instruction=repo_instruction,
pr_status=pr_status,
)
return instruction, images

Expand Down
5 changes: 5 additions & 0 deletions openhands/resolver/prompts/resolve/basic-followup.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ An environment has been set up for you to start working. You may assume all nece
# Issues addressed
{{ issues }}

{% if pr_status %}
# PR Status
{{ pr_status }}
{% endif %}

# Review comments
{{ review_comments }}

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ reportlab = "*"
[tool.coverage.run]
concurrency = ["gevent"]


[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
Expand Down Expand Up @@ -129,6 +130,7 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle]
convention = "google"


[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"
Expand Down
Loading
Loading