mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-12 15:55:03 -05:00
I'm getting circular import issues because there is a lot of cross-importing between `backend.data`, `backend.blocks`, and other components. This change reduces block-related cross-imports and thus risk of breaking circular imports.
394 lines
14 KiB
Python
394 lines
14 KiB
Python
import logging
|
|
import re
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
from typing_extensions import TypedDict
|
|
|
|
from backend.blocks._base import (
|
|
Block,
|
|
BlockCategory,
|
|
BlockOutput,
|
|
BlockSchemaInput,
|
|
BlockSchemaOutput,
|
|
)
|
|
from backend.data.model import SchemaField
|
|
|
|
from ._api import get_api
|
|
from ._auth import (
|
|
TEST_CREDENTIALS,
|
|
TEST_CREDENTIALS_INPUT,
|
|
GithubCredentials,
|
|
GithubCredentialsField,
|
|
GithubCredentialsInput,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CheckRunStatus(Enum):
|
|
QUEUED = "queued"
|
|
IN_PROGRESS = "in_progress"
|
|
COMPLETED = "completed"
|
|
|
|
|
|
class CheckRunConclusion(Enum):
|
|
SUCCESS = "success"
|
|
FAILURE = "failure"
|
|
NEUTRAL = "neutral"
|
|
CANCELLED = "cancelled"
|
|
SKIPPED = "skipped"
|
|
TIMED_OUT = "timed_out"
|
|
ACTION_REQUIRED = "action_required"
|
|
|
|
|
|
class GithubGetCIResultsBlock(Block):
|
|
class Input(BlockSchemaInput):
|
|
credentials: GithubCredentialsInput = GithubCredentialsField("repo")
|
|
repo: str = SchemaField(
|
|
description="GitHub repository",
|
|
placeholder="owner/repo",
|
|
)
|
|
target: str | int = SchemaField(
|
|
description="Commit SHA or PR number to get CI results for",
|
|
placeholder="abc123def or 123",
|
|
)
|
|
search_pattern: Optional[str] = SchemaField(
|
|
description="Optional regex pattern to search for in CI logs (e.g., error messages, file names)",
|
|
placeholder=".*error.*|.*warning.*",
|
|
default=None,
|
|
advanced=True,
|
|
)
|
|
check_name_filter: Optional[str] = SchemaField(
|
|
description="Optional filter for specific check names (supports wildcards)",
|
|
placeholder="*lint* or build-*",
|
|
default=None,
|
|
advanced=True,
|
|
)
|
|
|
|
class Output(BlockSchemaOutput):
|
|
class CheckRunItem(TypedDict, total=False):
|
|
id: int
|
|
name: str
|
|
status: str
|
|
conclusion: Optional[str]
|
|
started_at: Optional[str]
|
|
completed_at: Optional[str]
|
|
html_url: str
|
|
details_url: Optional[str]
|
|
output_title: Optional[str]
|
|
output_summary: Optional[str]
|
|
output_text: Optional[str]
|
|
annotations: list[dict]
|
|
|
|
class MatchedLine(TypedDict):
|
|
check_name: str
|
|
line_number: int
|
|
line: str
|
|
context: list[str]
|
|
|
|
check_run: CheckRunItem = SchemaField(
|
|
title="Check Run",
|
|
description="Individual CI check run with details",
|
|
)
|
|
check_runs: list[CheckRunItem] = SchemaField(
|
|
description="List of all CI check runs"
|
|
)
|
|
matched_line: MatchedLine = SchemaField(
|
|
title="Matched Line",
|
|
description="Line matching the search pattern with context",
|
|
)
|
|
matched_lines: list[MatchedLine] = SchemaField(
|
|
description="All lines matching the search pattern across all checks"
|
|
)
|
|
overall_status: str = SchemaField(
|
|
description="Overall CI status (pending, success, failure)"
|
|
)
|
|
overall_conclusion: str = SchemaField(
|
|
description="Overall CI conclusion if completed"
|
|
)
|
|
total_checks: int = SchemaField(description="Total number of CI checks")
|
|
passed_checks: int = SchemaField(description="Number of passed checks")
|
|
failed_checks: int = SchemaField(description="Number of failed checks")
|
|
|
|
def __init__(self):
|
|
super().__init__(
|
|
id="8ad9e103-78f2-4fdb-ba12-3571f2c95e98",
|
|
description="This block gets CI results for a commit or PR, with optional search for specific errors/warnings in logs.",
|
|
categories={BlockCategory.DEVELOPER_TOOLS},
|
|
input_schema=GithubGetCIResultsBlock.Input,
|
|
output_schema=GithubGetCIResultsBlock.Output,
|
|
test_input={
|
|
"repo": "owner/repo",
|
|
"target": "abc123def456",
|
|
"credentials": TEST_CREDENTIALS_INPUT,
|
|
},
|
|
test_credentials=TEST_CREDENTIALS,
|
|
test_output=[
|
|
("overall_status", "completed"),
|
|
("overall_conclusion", "success"),
|
|
("total_checks", 1),
|
|
("passed_checks", 1),
|
|
("failed_checks", 0),
|
|
(
|
|
"check_runs",
|
|
[
|
|
{
|
|
"id": 123456,
|
|
"name": "build",
|
|
"status": "completed",
|
|
"conclusion": "success",
|
|
"started_at": "2024-01-01T00:00:00Z",
|
|
"completed_at": "2024-01-01T00:05:00Z",
|
|
"html_url": "https://github.com/owner/repo/runs/123456",
|
|
"details_url": None,
|
|
"output_title": "Build passed",
|
|
"output_summary": "All tests passed",
|
|
"output_text": "Build log output...",
|
|
"annotations": [],
|
|
}
|
|
],
|
|
),
|
|
],
|
|
test_mock={
|
|
"get_ci_results": lambda *args, **kwargs: {
|
|
"check_runs": [
|
|
{
|
|
"id": 123456,
|
|
"name": "build",
|
|
"status": "completed",
|
|
"conclusion": "success",
|
|
"started_at": "2024-01-01T00:00:00Z",
|
|
"completed_at": "2024-01-01T00:05:00Z",
|
|
"html_url": "https://github.com/owner/repo/runs/123456",
|
|
"details_url": None,
|
|
"output_title": "Build passed",
|
|
"output_summary": "All tests passed",
|
|
"output_text": "Build log output...",
|
|
"annotations": [],
|
|
}
|
|
],
|
|
"total_count": 1,
|
|
}
|
|
},
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_commit_sha(api, repo: str, target: str | int) -> str:
|
|
"""Get commit SHA from either a commit SHA or PR URL."""
|
|
# If it's already a SHA, return it
|
|
|
|
if isinstance(target, str):
|
|
if re.match(r"^[0-9a-f]{6,40}$", target, re.IGNORECASE):
|
|
return target
|
|
|
|
# If it's a PR URL, get the head SHA
|
|
if isinstance(target, int):
|
|
pr_url = f"https://api.github.com/repos/{repo}/pulls/{target}"
|
|
response = await api.get(pr_url)
|
|
pr_data = response.json()
|
|
return pr_data["head"]["sha"]
|
|
|
|
raise ValueError("Target must be a commit SHA or PR URL")
|
|
|
|
@staticmethod
|
|
async def search_in_logs(
|
|
check_runs: list,
|
|
pattern: str,
|
|
) -> list[Output.MatchedLine]:
|
|
"""Search for pattern in check run logs."""
|
|
if not pattern:
|
|
return []
|
|
|
|
matched_lines = []
|
|
regex = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
|
|
|
|
for check in check_runs:
|
|
output_text = check.get("output_text", "") or ""
|
|
if not output_text:
|
|
continue
|
|
|
|
lines = output_text.split("\n")
|
|
for i, line in enumerate(lines):
|
|
if regex.search(line):
|
|
# Get context (2 lines before and after)
|
|
start = max(0, i - 2)
|
|
end = min(len(lines), i + 3)
|
|
context = lines[start:end]
|
|
|
|
matched_lines.append(
|
|
{
|
|
"check_name": check["name"],
|
|
"line_number": i + 1,
|
|
"line": line,
|
|
"context": context,
|
|
}
|
|
)
|
|
|
|
return matched_lines
|
|
|
|
@staticmethod
|
|
async def get_ci_results(
|
|
credentials: GithubCredentials,
|
|
repo: str,
|
|
target: str | int,
|
|
search_pattern: Optional[str] = None,
|
|
check_name_filter: Optional[str] = None,
|
|
) -> dict:
|
|
api = get_api(credentials, convert_urls=False)
|
|
|
|
# Get the commit SHA
|
|
commit_sha = await GithubGetCIResultsBlock.get_commit_sha(api, repo, target)
|
|
|
|
# Get check runs for the commit
|
|
check_runs_url = (
|
|
f"https://api.github.com/repos/{repo}/commits/{commit_sha}/check-runs"
|
|
)
|
|
|
|
# Get all pages of check runs
|
|
all_check_runs = []
|
|
page = 1
|
|
per_page = 100
|
|
|
|
while True:
|
|
response = await api.get(
|
|
check_runs_url, params={"per_page": per_page, "page": page}
|
|
)
|
|
data = response.json()
|
|
|
|
check_runs = data.get("check_runs", [])
|
|
all_check_runs.extend(check_runs)
|
|
|
|
if len(check_runs) < per_page:
|
|
break
|
|
page += 1
|
|
|
|
# Filter by check name if specified
|
|
if check_name_filter:
|
|
import fnmatch
|
|
|
|
filtered_runs = []
|
|
for run in all_check_runs:
|
|
if fnmatch.fnmatch(run["name"].lower(), check_name_filter.lower()):
|
|
filtered_runs.append(run)
|
|
all_check_runs = filtered_runs
|
|
|
|
# Get check run details with logs
|
|
detailed_runs = []
|
|
for run in all_check_runs:
|
|
# Get detailed output including logs
|
|
if run.get("output", {}).get("text"):
|
|
# Already has output
|
|
detailed_run = {
|
|
"id": run["id"],
|
|
"name": run["name"],
|
|
"status": run["status"],
|
|
"conclusion": run.get("conclusion"),
|
|
"started_at": run.get("started_at"),
|
|
"completed_at": run.get("completed_at"),
|
|
"html_url": run["html_url"],
|
|
"details_url": run.get("details_url"),
|
|
"output_title": run.get("output", {}).get("title"),
|
|
"output_summary": run.get("output", {}).get("summary"),
|
|
"output_text": run.get("output", {}).get("text"),
|
|
"annotations": [],
|
|
}
|
|
else:
|
|
# Try to get logs from the check run
|
|
detailed_run = {
|
|
"id": run["id"],
|
|
"name": run["name"],
|
|
"status": run["status"],
|
|
"conclusion": run.get("conclusion"),
|
|
"started_at": run.get("started_at"),
|
|
"completed_at": run.get("completed_at"),
|
|
"html_url": run["html_url"],
|
|
"details_url": run.get("details_url"),
|
|
"output_title": run.get("output", {}).get("title"),
|
|
"output_summary": run.get("output", {}).get("summary"),
|
|
"output_text": None,
|
|
"annotations": [],
|
|
}
|
|
|
|
# Get annotations if available
|
|
if run.get("output", {}).get("annotations_count", 0) > 0:
|
|
annotations_url = f"https://api.github.com/repos/{repo}/check-runs/{run['id']}/annotations"
|
|
try:
|
|
ann_response = await api.get(annotations_url)
|
|
detailed_run["annotations"] = ann_response.json()
|
|
except Exception:
|
|
pass
|
|
|
|
detailed_runs.append(detailed_run)
|
|
|
|
return {
|
|
"check_runs": detailed_runs,
|
|
"total_count": len(detailed_runs),
|
|
}
|
|
|
|
async def run(
|
|
self,
|
|
input_data: Input,
|
|
*,
|
|
credentials: GithubCredentials,
|
|
**kwargs,
|
|
) -> BlockOutput:
|
|
|
|
try:
|
|
target = int(input_data.target)
|
|
except ValueError:
|
|
target = input_data.target
|
|
|
|
result = await self.get_ci_results(
|
|
credentials,
|
|
input_data.repo,
|
|
target,
|
|
input_data.search_pattern,
|
|
input_data.check_name_filter,
|
|
)
|
|
|
|
check_runs = result["check_runs"]
|
|
|
|
# Calculate overall status
|
|
if not check_runs:
|
|
yield "overall_status", "no_checks"
|
|
yield "overall_conclusion", "no_checks"
|
|
else:
|
|
all_completed = all(run["status"] == "completed" for run in check_runs)
|
|
if all_completed:
|
|
yield "overall_status", "completed"
|
|
# Determine overall conclusion
|
|
has_failure = any(
|
|
run["conclusion"] in ["failure", "timed_out", "action_required"]
|
|
for run in check_runs
|
|
)
|
|
if has_failure:
|
|
yield "overall_conclusion", "failure"
|
|
else:
|
|
yield "overall_conclusion", "success"
|
|
else:
|
|
yield "overall_status", "pending"
|
|
yield "overall_conclusion", "pending"
|
|
|
|
# Count checks
|
|
total = len(check_runs)
|
|
passed = sum(1 for run in check_runs if run.get("conclusion") == "success")
|
|
failed = sum(
|
|
1 for run in check_runs if run.get("conclusion") in ["failure", "timed_out"]
|
|
)
|
|
|
|
yield "total_checks", total
|
|
yield "passed_checks", passed
|
|
yield "failed_checks", failed
|
|
|
|
# Output check runs
|
|
yield "check_runs", check_runs
|
|
|
|
# Search for patterns if specified
|
|
if input_data.search_pattern:
|
|
matched_lines = await self.search_in_logs(
|
|
check_runs, input_data.search_pattern
|
|
)
|
|
if matched_lines:
|
|
yield "matched_lines", matched_lines
|