mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
* feat: CI auto-fix and PR babysitting for agent PRs Watch CI failures and review feedback on PRs Open SWE opened, then dispatch confidence-gated fix runs on the originating agent thread. Adds CI webhook ingestion (check_run/check_suite/workflow_run/status), a per-PR @open-swe autofix on|off toggle, auto-response to review comments, and a polling ci_monitor graph that also flags merge conflicts. Gated by the existing autofix_mode/trigger_mode settings, the enabled-repos opt-in, base-branch and human-commit skip rules, dedupe, and a per-PR attempt cap. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: address review feedback on CI auto-fix - Security: gate the no-mention review-feedback path on author trust — require a trusted author_association (OWNER/MEMBER/COLLABORATOR) plus a GitHub write/maintain/admin permission check before dispatching a write-capable agent run, preventing privilege escalation from read/triage/outside reviewers. - Auth: reuse the originating PR thread's source + login/email when dispatching fix runs so the GitHub-token resolver authenticates them in non-bot-token deployments (bespoke github_ci source failed to resolve). - Docs: document the Commit statuses: Read-only permission required for the Status webhook event. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
"""GitHub Checks API helpers for the reviewer's PR check run.
|
|
|
|
A check run named ``Open SWE Review`` is created on the PR head SHA when an
|
|
auto-review is dispatched, so the PR's checks section shows the review as
|
|
in-progress. ``publish_review`` (or the after-agent fallback) completes it.
|
|
|
|
All calls are best-effort: check runs require the GitHub App's
|
|
``Checks: Read & write`` permission, and a missing permission must never
|
|
break review dispatch or publish.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import UTC, datetime
|
|
from typing import Literal
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
REVIEW_CHECK_RUN_NAME = "Open SWE Review"
|
|
AUTOFIX_CHECK_RUN_NAME = "Open SWE Auto-fix"
|
|
|
|
_GITHUB_API_BASE = "https://api.github.com"
|
|
|
|
CheckConclusion = Literal["success", "neutral", "failure"]
|
|
|
|
|
|
def github_headers(token: str) -> dict[str, str]:
|
|
return {
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/vnd.github+json",
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
}
|
|
|
|
|
|
def _utc_now_iso() -> str:
|
|
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
|
async def create_review_check_run(
|
|
*,
|
|
owner: str,
|
|
repo: str,
|
|
head_sha: str,
|
|
token: str,
|
|
details_url: str | None = None,
|
|
) -> int | None:
|
|
"""Create an in-progress ``Open SWE Review`` check run on ``head_sha``.
|
|
|
|
Returns the check run id, or ``None`` on any failure (most commonly the
|
|
App lacking the Checks permission).
|
|
"""
|
|
payload: dict[str, object] = {
|
|
"name": REVIEW_CHECK_RUN_NAME,
|
|
"head_sha": head_sha,
|
|
"status": "in_progress",
|
|
"started_at": _utc_now_iso(),
|
|
"output": {
|
|
"title": "Review in progress",
|
|
"summary": "Open SWE is reviewing this pull request…",
|
|
},
|
|
}
|
|
if details_url:
|
|
payload["details_url"] = details_url
|
|
url = f"{_GITHUB_API_BASE}/repos/{owner}/{repo}/check-runs"
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
url, headers=github_headers(token), json=payload, timeout=30
|
|
)
|
|
response.raise_for_status()
|
|
except httpx.HTTPError:
|
|
logger.exception(
|
|
"Failed to create review check run for %s/%s@%s "
|
|
"(does the GitHub App have Checks: Read & write?)",
|
|
owner,
|
|
repo,
|
|
head_sha,
|
|
)
|
|
return None
|
|
data = response.json()
|
|
check_run_id = data.get("id") if isinstance(data, dict) else None
|
|
return check_run_id if isinstance(check_run_id, int) else None
|
|
|
|
|
|
async def complete_review_check_run(
|
|
*,
|
|
owner: str,
|
|
repo: str,
|
|
check_run_id: int,
|
|
token: str,
|
|
conclusion: CheckConclusion,
|
|
title: str,
|
|
summary: str,
|
|
) -> bool:
|
|
"""Mark a review check run as completed. Returns True on success."""
|
|
url = f"{_GITHUB_API_BASE}/repos/{owner}/{repo}/check-runs/{check_run_id}"
|
|
payload = {
|
|
"status": "completed",
|
|
"conclusion": conclusion,
|
|
"completed_at": _utc_now_iso(),
|
|
"output": {"title": title, "summary": summary},
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.patch(
|
|
url, headers=github_headers(token), json=payload, timeout=30
|
|
)
|
|
response.raise_for_status()
|
|
except httpx.HTTPError:
|
|
logger.exception(
|
|
"Failed to complete review check run %s on %s/%s", check_run_id, owner, repo
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
async def post_autofix_status_check(
|
|
*,
|
|
owner: str,
|
|
repo: str,
|
|
head_sha: str,
|
|
token: str,
|
|
title: str,
|
|
summary: str,
|
|
details_url: str | None = None,
|
|
) -> bool:
|
|
"""Post an informational, completed ``Open SWE Auto-fix`` check on ``head_sha``.
|
|
|
|
Completed immediately as ``neutral`` so it's non-blocking and never leaves a
|
|
dangling in-progress check that could gate branch protection. Used as the
|
|
auto-fix status channel instead of a PR comment (PR comments can trigger
|
|
``issue_comment`` automation like Atlantis/Terraform).
|
|
"""
|
|
payload: dict[str, object] = {
|
|
"name": AUTOFIX_CHECK_RUN_NAME,
|
|
"head_sha": head_sha,
|
|
"status": "completed",
|
|
"conclusion": "neutral",
|
|
"started_at": _utc_now_iso(),
|
|
"completed_at": _utc_now_iso(),
|
|
"output": {"title": title, "summary": summary},
|
|
}
|
|
if details_url:
|
|
payload["details_url"] = details_url
|
|
url = f"{_GITHUB_API_BASE}/repos/{owner}/{repo}/check-runs"
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.post(
|
|
url, headers=github_headers(token), json=payload, timeout=30
|
|
)
|
|
response.raise_for_status()
|
|
except httpx.HTTPError:
|
|
logger.warning("Failed to post auto-fix status check for %s/%s@%s", owner, repo, head_sha)
|
|
return False
|
|
return True
|
|
|
|
|
|
def review_check_conclusion(surfaced_count: int) -> tuple[CheckConclusion, str, str]:
|
|
"""Map a publish result to (conclusion, title, summary).
|
|
|
|
Always ``success`` so the check is informational and non-blocking, and so
|
|
GitHub groups it under "successful checks" rather than a confusing
|
|
"neutral check". The finding count is surfaced in the title; the findings
|
|
themselves are posted as PR comments.
|
|
"""
|
|
if surfaced_count > 0:
|
|
issue_word = "issue" if surfaced_count == 1 else "issues"
|
|
return (
|
|
"success",
|
|
f"Found {surfaced_count} potential {issue_word}",
|
|
f"Open SWE surfaced {surfaced_count} potential {issue_word} on this pull request.",
|
|
)
|
|
return (
|
|
"success",
|
|
"No issues found",
|
|
"Open SWE reviewed this pull request and found no issues.",
|
|
)
|