mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 16:13:15 +00:00
211 lines
11 KiB
Python
211 lines
11 KiB
Python
"""Github webhook HTTP routes."""
|
|
|
|
from fastapi import APIRouter
|
|
|
|
from . import common
|
|
from . import github as service
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/webhooks/github")
|
|
async def github_webhook(
|
|
request: common.Request, background_tasks: common.BackgroundTasks
|
|
) -> dict[str, str]:
|
|
"""Handle GitHub webhooks for issue and PR events that tag @open-swe."""
|
|
body = await request.body()
|
|
|
|
signature = request.headers.get("X-Hub-Signature-256", "")
|
|
if not common.verify_github_signature(body, signature, secret=common.GITHUB_WEBHOOK_SECRET):
|
|
common.logger.warning("Invalid GitHub webhook signature")
|
|
raise common.HTTPException(status_code=401, detail="Invalid signature")
|
|
|
|
event_type = request.headers.get("X-GitHub-Event", "")
|
|
if event_type not in common._SUPPORTED_GH_EVENTS:
|
|
common.logger.info("Ignoring unsupported GitHub event type: %s", event_type)
|
|
return {"status": "ignored", "reason": f"Unsupported event type: {event_type}"}
|
|
|
|
try:
|
|
payload = common.json.loads(body)
|
|
except common.json.JSONDecodeError:
|
|
common.logger.exception("Failed to parse GitHub webhook JSON")
|
|
return {"status": "error", "message": "Invalid JSON"}
|
|
|
|
webhook_repo = payload.get("repository", {})
|
|
webhook_repo_config = {
|
|
"owner": webhook_repo.get("owner", {}).get("login", ""),
|
|
"name": webhook_repo.get("name", ""),
|
|
}
|
|
|
|
issue = payload.get("issue", {})
|
|
is_pull_request_comment = bool(event_type == "issue_comment" and issue.get("pull_request"))
|
|
is_issue_comment = bool(event_type == "issue_comment" and not issue.get("pull_request"))
|
|
is_issue_event = event_type == "issues"
|
|
is_pull_request_event = event_type == "pull_request"
|
|
|
|
if is_pull_request_event:
|
|
action = payload.get("action", "")
|
|
if action not in common._SUPPORTED_GH_PULL_REQUEST_ACTIONS:
|
|
common.logger.info("Ignoring unsupported GitHub pull_request action: %s", action)
|
|
return {
|
|
"status": "ignored",
|
|
"reason": f"Unsupported GitHub pull_request action: {action}",
|
|
}
|
|
if action in common._GH_PR_AGENT_STATE_ACTIONS:
|
|
background_tasks.add_task(common.update_agent_thread_pr_state, payload)
|
|
if action in common._GH_PR_WATCH_TOGGLE_ACTIONS:
|
|
common.logger.info(
|
|
"Accepted GitHub PR %s webhook, scheduling reviewer watch update", action
|
|
)
|
|
background_tasks.add_task(service.process_github_pr_close, payload)
|
|
return {"status": "accepted", "message": f"Processing PR {action} for reviewer watch"}
|
|
if action in common._GH_PR_FIRST_REVIEW_ACTIONS:
|
|
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
|
|
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, "pull_request")
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
common.logger.info("Accepted GitHub PR %s webhook, scheduling auto-review task", action)
|
|
background_tasks.add_task(service.process_github_pr_ready, payload)
|
|
return {"status": "accepted", "message": f"Processing PR {action} for auto-review"}
|
|
common.logger.info("Ignoring unsupported GitHub pull_request action: %s", action)
|
|
return {
|
|
"status": "ignored",
|
|
"reason": f"Unsupported GitHub pull_request action: {action}",
|
|
}
|
|
|
|
if event_type == "push":
|
|
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
|
|
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
|
|
common.logger.info("Accepted GitHub push webhook, scheduling reviewer watch evaluation")
|
|
background_tasks.add_task(service.process_github_push_event, payload)
|
|
return {"status": "accepted", "message": "Processing GitHub push for reviewer watch"}
|
|
|
|
if event_type in common._GH_CI_EVENTS:
|
|
if not common.is_failing_ci_payload(payload, event_type):
|
|
return {"status": "ignored", "reason": "CI event is not a completed failure"}
|
|
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
|
|
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
|
|
common.logger.info(
|
|
"Accepted GitHub %s webhook, scheduling CI auto-fix evaluation", event_type
|
|
)
|
|
background_tasks.add_task(service.process_github_ci_event, payload, event_type)
|
|
return {"status": "accepted", "message": f"Processing GitHub {event_type} for auto-fix"}
|
|
|
|
if not common._is_repo_allowed(webhook_repo_config):
|
|
common.logger.debug(
|
|
"Rejecting GitHub webhook: repo '%s/%s' not in allowlist",
|
|
webhook_repo_config.get("owner"),
|
|
webhook_repo_config.get("name"),
|
|
)
|
|
return {"status": "ignored", "reason": "Repository not in allowlist"}
|
|
|
|
if is_issue_event:
|
|
action = payload.get("action", "")
|
|
if action not in common._SUPPORTED_GH_ISSUE_ACTIONS:
|
|
common.logger.info("Ignoring unsupported GitHub issue action: %s", action)
|
|
return {"status": "ignored", "reason": f"Unsupported GitHub issue action: {action}"}
|
|
if action == "edited":
|
|
changes = payload.get("changes", {})
|
|
if not any(field in changes for field in ("body", "title")):
|
|
common.logger.info("Ignoring GitHub issue edit without title/body changes")
|
|
return {"status": "ignored", "reason": "Issue edit did not change title or body"}
|
|
|
|
issue_text = f"{issue.get('title', '')}\n\n{issue.get('body', '')}".lower()
|
|
if not any(tag in issue_text for tag in common.OPEN_SWE_TAGS):
|
|
common.logger.info("Ignoring issue that does not mention @openswe or @open-swe")
|
|
return {"status": "ignored", "reason": "Issue does not mention @openswe or @open-swe"}
|
|
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
|
|
common.logger.info("Accepted GitHub issue webhook, scheduling background task")
|
|
background_tasks.add_task(service.process_github_issue, payload, event_type)
|
|
return {"status": "accepted", "message": "Processing GitHub issue event"}
|
|
|
|
action = payload.get("action", "")
|
|
supported_comment_actions = common._SUPPORTED_GH_COMMENT_ACTIONS.get(event_type)
|
|
if supported_comment_actions is None:
|
|
common.logger.info("Ignoring unsupported GitHub payload shape for event=%s", event_type)
|
|
return {"status": "ignored", "reason": f"Unsupported payload for event type: {event_type}"}
|
|
if action and action not in supported_comment_actions:
|
|
common.logger.debug("Ignoring unsupported GitHub %s action: %s", event_type, action)
|
|
return {"status": "ignored", "reason": f"Unsupported GitHub {event_type} action: {action}"}
|
|
|
|
comment = payload.get("comment") or payload.get("review", {})
|
|
comment_body = (comment.get("body") or "") if comment else ""
|
|
|
|
is_pr_related_comment = is_pull_request_comment or event_type in {
|
|
"pull_request_review_comment",
|
|
"pull_request_review",
|
|
}
|
|
autofix_command = service._parse_autofix_command(comment_body)
|
|
if autofix_command is not None and is_pr_related_comment:
|
|
if not await common._is_repo_auto_review_enabled(webhook_repo_config):
|
|
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
background_tasks.add_task(
|
|
service.process_github_autofix_command, payload, event_type, disabled=autofix_command
|
|
)
|
|
return {"status": "accepted", "message": "Processing auto-fix toggle"}
|
|
|
|
if (
|
|
event_type == "pull_request_review_comment"
|
|
and common._review_comment_reply_parent_id(payload) is not None
|
|
):
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
background_tasks.add_task(service.process_github_review_finding_reply, payload)
|
|
return {"status": "accepted", "message": "Processing review finding reply"}
|
|
|
|
review_instructions = service._parse_review_command(comment_body)
|
|
if review_instructions is not None and is_pr_related_comment:
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
background_tasks.add_task(
|
|
service.process_github_review_command,
|
|
payload,
|
|
event_type,
|
|
instructions=review_instructions,
|
|
)
|
|
return {"status": "accepted", "message": "Processing on-demand PR review"}
|
|
|
|
if not any(tag in comment_body.lower() for tag in common.OPEN_SWE_TAGS):
|
|
if service._is_actionable_review_payload(
|
|
payload, event_type
|
|
) and await common._is_repo_auto_review_enabled(webhook_repo_config):
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
background_tasks.add_task(service.process_github_autofix_review, payload, event_type)
|
|
return {"status": "accepted", "message": "Processing auto-fix review feedback"}
|
|
common.logger.debug(
|
|
"Ignoring GitHub %s%s that does not mention @openswe or @open-swe",
|
|
event_type,
|
|
f" action={action}" if action else "",
|
|
)
|
|
return {"status": "ignored", "reason": "Comment does not mention @openswe or @open-swe"}
|
|
|
|
gate_rejection = await common._enforce_public_repo_org_gate(payload, event_type)
|
|
if gate_rejection is not None:
|
|
return gate_rejection
|
|
|
|
common.logger.info("Accepted GitHub webhook: event=%s, scheduling background task", event_type)
|
|
if is_pull_request_comment or event_type in {
|
|
"pull_request_review_comment",
|
|
"pull_request_review",
|
|
}:
|
|
background_tasks.add_task(service.process_github_pr_comment, payload, event_type)
|
|
return {"status": "accepted", "message": f"Processing {event_type} event"}
|
|
|
|
if is_issue_comment:
|
|
background_tasks.add_task(service.process_github_issue, payload, event_type)
|
|
return {"status": "accepted", "message": "Processing GitHub issue comment event"}
|
|
|
|
common.logger.info("Ignoring unsupported GitHub payload shape for event=%s", event_type)
|
|
return {"status": "ignored", "reason": f"Unsupported payload for event type: {event_type}"}
|