Fixed whitespace

This commit is contained in:
2025-06-02 18:53:45 -05:00
parent 1e39e92011
commit 887bae3972
+41 -40
View File
@@ -20,8 +20,8 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
Initializes the GitHubCIHelper. Initializes the GitHubCIHelper.
Args: Args:
repo_owner (str): The owner of the GitHub repository (e.g., \'\'\'bucolucas\'\'\'). repo_owner (str): The owner of the GitHub repository (e.g., '''bucolucas''').
repo_name (str): The name of the GitHub repository (e.g., \'\'\'cyclop\'\'\'). repo_name (str): The name of the GitHub repository (e.g., '''cyclop''').
github_token (str, optional): A GitHub Personal Access Token (PAT) github_token (str, optional): A GitHub Personal Access Token (PAT)
for API authentication. Recommended for for API authentication. Recommended for
private repos or higher rate limits. private repos or higher rate limits.
@@ -32,7 +32,7 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
self.repo_owner = repo_owner self.repo_owner = repo_owner
self.repo_name = repo_name self.repo_name = repo_name
self.base_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}" self.base_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
self._token = github_token or os.environ.get(\'GITHUB_TOKEN\') # Renamed to _token for consistency self._token = github_token or os.environ.get("GITHUB_TOKEN") # Renamed to _token for consistency
self.headers = { self.headers = {
"Accept": "application/vnd.github.v3+json" "Accept": "application/vnd.github.v3+json"
@@ -75,7 +75,7 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
"type": "object", "type": "object",
"properties": { "properties": {
"pull_request_number": {"type": "integer", "description": "The number of the pull request."}, "pull_request_number": {"type": "integer", "description": "The number of the pull request."},
"workflow_name": {"type": "string", "description": "The display name of the workflow (e.g., \'\'\'Python CI\'\'\').", "default": "Python CI"} "workflow_name": {"type": "string", "description": "The display name of the workflow (e.g., '''Python CI''').", "default": "Python CI"}
}, },
"required": ["pull_request_number"] "required": ["pull_request_number"]
} }
@@ -90,7 +90,7 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
"type": "object", "type": "object",
"properties": { "properties": {
"run_id": {"type": "integer", "description": "The ID of the workflow run."}, "run_id": {"type": "integer", "description": "The ID of the workflow run."},
"job_name": {"type": "string", "description": "The name of the job (e.g., \'\'\'test\'\'\').", "default": "test"} "job_name": {"type": "string", "description": "The name of the job (e.g., '''test''').", "default": "test"}
}, },
"required": ["run_id"] "required": ["run_id"]
} }
@@ -129,7 +129,7 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
return error_message return error_message
def clear(self): def clear(self):
"""Clears any sensitive state if necessary. For this tool, it\'s a no-op but present for interface consistency.""" """Clears any sensitive state if necessary. For this tool, it's a no-op but present for interface consistency."""
self.logger.info("GitHubCIHelper state cleared (no specific state to clear).") self.logger.info("GitHubCIHelper state cleared (no specific state to clear).")
@@ -140,13 +140,13 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
# Use self.session instead of requests directly # Use self.session instead of requests directly
response = self.session.request(method, url, headers=self.headers, **kwargs) response = self.session.request(method, url, headers=self.headers, **kwargs)
response.raise_for_status() response.raise_for_status()
if response.content and response.headers.get(\'Content-Type\', \'\').startswith(\'application/json\'): if response.content and response.headers.get("Content-Type", "").startswith("application/json"):
return response.json() return response.json()
elif response.content: # For non-JSON content like zip files or plain text logs elif response.content: # For non-JSON content like zip files or plain text logs
return response return response
return None return None
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
self.logger.error(f"HTTP error occurred: {e} - {e.response.text if e.response else \'No response text\'}") # Use self.logger self.logger.error(f"HTTP error occurred: {e} - {e.response.text if e.response else "No response text"}") # Use self.logger
raise raise
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Request failed: {e}") # Use self.logger self.logger.error(f"Request failed: {e}") # Use self.logger
@@ -163,10 +163,10 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
pr_response = self._make_request("GET", pr_url) # this returns a response object or parsed JSON pr_response = self._make_request("GET", pr_url) # this returns a response object or parsed JSON
pr_data = pr_response if isinstance(pr_response, dict) else pr_response.json() # Ensure pr_data is dict pr_data = pr_response if isinstance(pr_response, dict) else pr_response.json() # Ensure pr_data is dict
if not pr_data or \'head\' not in pr_data or \'sha\' not in pr_data[\'head\']: if not pr_data or "head" not in pr_data or "sha" not in pr_data["head"]:
self.logger.error(f"Could not get head SHA for PR {pull_request_number}. Response: {pr_data}") self.logger.error(f"Could not get head SHA for PR {pull_request_number}. Response: {pr_data}")
return None return None
head_sha = pr_data[\'head\'][\'sha\'] head_sha = pr_data["head"]["sha"]
runs_url = f"{self.base_url}/actions/runs?event=pull_request&head_sha={head_sha}" runs_url = f"{self.base_url}/actions/runs?event=pull_request&head_sha={head_sha}"
runs_response = self._make_request("GET", runs_url) runs_response = self._make_request("GET", runs_url)
@@ -183,17 +183,17 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
""" """
Gets the latest failed workflow run for a specific pull request and workflow name. Gets the latest failed workflow run for a specific pull request and workflow name.
""" """
self.logger.info(f"Getting latest failed run for PR #{pull_request_number}, workflow: \'{workflow_name}\'") self.logger.info(f"Getting latest failed run for PR #{pull_request_number}, workflow: '{workflow_name}'")
runs = self.get_pr_workflow_runs(pull_request_number) runs = self.get_pr_workflow_runs(pull_request_number)
if not runs: if not runs:
self.logger.info(f"No runs found for PR #{pull_request_number} to check for failures.") self.logger.info(f"No runs found for PR #{pull_request_number} to check for failures.")
return None return None
for run in sorted(runs, key=lambda r: r[\'created_at\'], reverse=True): for run in sorted(runs, key=lambda r: r["created_at"], reverse=True):
if run[\'name\'] == workflow_name and run[\'conclusion\'] == \'failure\': if run["name"] == workflow_name and run["conclusion"] == "failure":
self.logger.info(f"Found failed run {run[\'id\']} for workflow \'{workflow_name}\' in PR #{pull_request_number}") self.logger.info(f"Found failed run {run['id']} for workflow '{workflow_name}' in PR #{pull_request_number}")
return run return run
self.logger.info(f"No failed run for workflow \'{workflow_name}\' found for PR #{pull_request_number}") # Use self.logger self.logger.info(f"No failed run for workflow '{workflow_name}' found for PR #{pull_request_number}") # Use self.logger
return None return None
@metrics.measure @metrics.measure
@@ -201,9 +201,9 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
""" """
Downloads and returns the logs for a specific job within a workflow run. Downloads and returns the logs for a specific job within a workflow run.
""" """
self.logger.info(f"Getting job logs for run ID {run_id}, job name \'{job_name}\'") self.logger.info(f"Getting job logs for run ID {run_id}, job name '{job_name}'")
jobs_url = f"{self.base_url}/actions/runs/{run_id}/jobs" jobs_url = f"{self.base_url}/actions/runs/{run_id}/jobs"
target_job = None # Initialize target_job here to ensure it\'s defined for later logging target_job = None # Initialize target_job here to ensure it's defined for later logging
try: try:
jobs_response = self._make_request("GET", jobs_url) jobs_response = self._make_request("GET", jobs_url)
jobs_data = jobs_response if isinstance(jobs_response, dict) else jobs_response.json() jobs_data = jobs_response if isinstance(jobs_response, dict) else jobs_response.json()
@@ -218,26 +218,27 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
break break
if not target_job: if not target_job:
self.logger.error(f"Job \'{job_name}\' not found in run ID {run_id}") self.logger.error(f"Job '{job_name}' not found in run ID {run_id}")
return None return None
if target_job[\'status\'] != \'completed\': if target_job["status"] != "completed":
self.logger.info(f"Job \'{job_name}\' in run ID {run_id} has not completed. Status: {target_job[\'status\']}") self.logger.info(f"Job '{job_name}' in run ID {run_id} has not completed. Status: {target_job['status']}")
return f"Job \'{job_name}\' not yet completed (status: {target_job[\'status\']}). Logs may be unavailable." return f"Job '{job_name}' not yet completed (status: {target_job['status']}). Logs may be unavailable."
logs_url = f"{self.base_url}/actions/jobs/{target_job[\'id\']}/logs" logs_url = f"{self.base_url}/actions/jobs/{target_job['id']}/logs"
self.logger.info(f"Attempting to download logs from: {logs_url}") self.logger.info(f"Attempting to download logs from: {logs_url}")
log_response = self.session.get(logs_url, headers=self.headers, allow_redirects=True, stream=True) log_response = self.session.get(logs_url, headers=self.headers, allow_redirects=True, stream=True)
log_response.raise_for_status() log_response.raise_for_status()
if \'application/zip\' in log_response.headers.get(\'Content-Type\', \'\'): if 'application/zip' in log_response.headers.get('Content-Type', ''):
self.logger.info(f"Received zip file for logs of job ID {target_job[\'id\']}.") self.logger.info(f"Received zip file for logs of job ID {target_job['id']}.")
with zipfile.ZipFile(io.BytesIO(log_response.content)) as zf: with zipfile.ZipFile(io.BytesIO(log_response.content)) as zf:
log_file_names = [name for name in zf.namelist() if not name.endswith(\'/\')] log_file_names = [name for name in zf.namelist() if not name.endswith('/')]
if not log_file_names: if not log_file_names:
self.logger.error(f"No files found in the downloaded log zip for job ID {target_job[\'id\']}.") self.logger.error(f"No files found in the downloaded log zip for job ID {target_job['id']}.")
return None return None
actual_log_file_name = log_file_names[0] actual_log_file_name = log_file_names[0]
@@ -247,28 +248,28 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
actual_log_file_name = name actual_log_file_name = name
break break
self.logger.info(f"Extracting log file: {actual_log_file_name} from zip for job ID {target_job[\'id\']}.") self.logger.info(f"Extracting log file: {actual_log_file_name} from zip for job ID {target_job['id']}.")
with zf.open(actual_log_file_name) as log_file: with zf.open(actual_log_file_name) as log_file:
return log_file.read().decode(\'utf-8\') return log_file.read().decode("utf-8")
else: else:
self.logger.info(f"Received plain text logs for job ID {target_job[\'id\']}.") self.logger.info(f"Received plain text logs for job ID {target_job['id']}.")
return log_response.text return log_response.text
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
self.logger.error(f"HTTP error downloading logs for job ID {target_job.get(\'id\', \'unknown\') if target_job else \'unknown\'}: {e} - {e.response.text if e.response else \'No response text\'}", exc_info=True) self.logger.error(f"HTTP error downloading logs for job ID {target_job.get('id', 'unknown') if target_job else 'unknown'}: {e} - {e.response.text if e.response else 'No response text'}", exc_info=True)
if e.response and e.response.status_code == 404: if e.response and e.response.status_code == 404:
self.logger.error("Log download URL might be invalid or logs expired.") self.logger.error("Log download URL might be invalid or logs expired.")
return f"Error downloading logs: {e}" return f"Error downloading logs: {e}"
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
self.logger.error(f"Request failed downloading logs for job ID {target_job.get(\'id\', \'unknown\') if target_job else \'unknown\'}: {e}", exc_info=True) self.logger.error(f"Request failed downloading logs for job ID {target_job.get('id', 'unknown') if target_job else 'unknown'}: {e}", exc_info=True)
return f"Error during log download request: {e}" return f"Error during log download request: {e}"
except zipfile.BadZipFile: except zipfile.BadZipFile:
self.logger.error(f"Failed to unzip logs for job ID {target_job.get(\'id\', \'unknown\') if target_job else \'unknown\'}.", exc_info=True) self.logger.error(f"Failed to unzip logs for job ID {target_job.get('id', 'unknown') if target_job else 'unknown'}.", exc_info=True)
# Adding response text for BadZipFile can be risky if it's large binary data. # Adding response text for BadZipFile can be risky if it's large binary data.
# Consider logging only a snippet or specific headers if this occurs frequently. # Consider logging only a snippet or specific headers if this occurs frequently.
return "Failed to unzip logs." return "Failed to unzip logs."
except Exception as e: except Exception as e:
self.logger.error(f"An unexpected error occurred while processing logs for job {target_job.get(\'id\', \'unknown\') if target_job else \'unknown\'}: {e}", exc_info=True) self.logger.error(f"An unexpected error occurred while processing logs for job {target_job.get('id', 'unknown') if target_job else 'unknown'}: {e}", exc_info=True)
return f"Unexpected error processing logs: {e}" return f"Unexpected error processing logs: {e}"
@@ -356,7 +357,7 @@ class GitHubCIHelper(BaseTool): # Inherits from BaseTool
# --- Example Usage (Illustrative) --- # --- Example Usage (Illustrative) ---
if __name__ == "__main__": if __name__ == "__main__":
# This example assumes you have GITHUB_TOKEN environment variable set # This example assumes you have GITHUB_TOKEN environment variable set
# And that \'requests\' is installed. # And that 'requests' is installed.
# Replace with your actual repo owner, name, and PR number. # Replace with your actual repo owner, name, and PR number.
pr_number = 206 # Example PR pr_number = 206 # Example PR
repo_owner = "bucolucas" # Example owner repo_owner = "bucolucas" # Example owner
@@ -364,7 +365,7 @@ if __name__ == "__main__":
# Setup basic logging for the example # Setup basic logging for the example
# In a real app, logger would be configured externally # In a real app, logger would be configured externally
logging.basicConfig(level=logging.INFO, format=\'%(asctime)s - %(name)s - %(levelname)s - %(message)s\') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
example_logger = logging.getLogger("GitHubCIHelperExample") example_logger = logging.getLogger("GitHubCIHelperExample")
@@ -375,10 +376,10 @@ if __name__ == "__main__":
failed_run = helper.get_latest_failed_run_for_pr(pull_request_number=pr_number, workflow_name="Python CI") failed_run = helper.get_latest_failed_run_for_pr(pull_request_number=pr_number, workflow_name="Python CI")
if failed_run: if failed_run:
example_logger.info(f"Found failed run: ID {failed_run[\'id\']}, Status {failed_run[\'conclusion\']}") example_logger.info(f"Found failed run: ID {failed_run['id']}, Status {failed_run['conclusion']}")
example_logger.info(f"Attempting to download logs for job \'test\' in run {failed_run[\'id\']}...") example_logger.info(f"Attempting to download logs for job 'test' in run {failed_run['id']}...")
log_content = helper.get_job_logs_for_run(run_id=failed_run[\'id\'], job_name="test") log_content = helper.get_job_logs_for_run(run_id=failed_run['id'], job_name="test")
if isinstance(log_content, str) and not log_content.startswith("Error") and not log_content.startswith("Job") and not log_content.startswith("Failed"): if isinstance(log_content, str) and not log_content.startswith("Error") and not log_content.startswith("Job") and not log_content.startswith("Failed"):
example_logger.info(f"Successfully downloaded logs (length: {len(log_content)} characters).") example_logger.info(f"Successfully downloaded logs (length: {len(log_content)} characters).")
@@ -394,9 +395,9 @@ if __name__ == "__main__":
# print(f"Log start:\n{log_content[:2000]}") # print(f"Log start:\n{log_content[:2000]}")
elif log_content is None: elif log_content is None:
example_logger.error("Could not retrieve log content (returned None).") example_logger.error("Could not retrieve log content (returned None).")
else: # If it\'s an error message string from the function itself else: # If it's an error message string from the function itself
example_logger.error(f"Failed to get/process logs: {log_content}") example_logger.error(f"Failed to get/process logs: {log_content}")
else: else:
example_logger.info(f"No failed \'Python CI\' workflow run found for PR #{pr_number} or the PR doesn\'t exist/no runs yet.") example_logger.info(f"No failed 'Python CI' workflow run found for PR #{pr_number} or the PR doesn't exist/no runs yet.")