mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-05 05:10:38 +08:00
Merge pull request #233 from zmeir/zmeir-automatic_github_app_options
Support custom deployments for github_app.py and add more options for automatic review actions
This commit is contained in:
@ -1,6 +1,8 @@
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@ -14,7 +16,7 @@ from pr_agent.config_loader import get_settings, global_settings
|
|||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.servers.utils import verify_signature
|
from pr_agent.servers.utils import verify_signature
|
||||||
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@ -34,7 +36,8 @@ async def handle_github_webhooks(request: Request, response: Response):
|
|||||||
context["installation_id"] = installation_id
|
context["installation_id"] = installation_id
|
||||||
context["settings"] = copy.deepcopy(global_settings)
|
context["settings"] = copy.deepcopy(global_settings)
|
||||||
|
|
||||||
return await handle_request(body)
|
response = await handle_request(body, event=request.headers.get("X-GitHub-Event", None))
|
||||||
|
return response or {}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/v1/marketplace_webhooks")
|
@router.post("/api/v1/marketplace_webhooks")
|
||||||
@ -48,70 +51,119 @@ async def get_body(request):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Error parsing request body", e)
|
logging.error("Error parsing request body", e)
|
||||||
raise HTTPException(status_code=400, detail="Error parsing request body") from e
|
raise HTTPException(status_code=400, detail="Error parsing request body") from e
|
||||||
body_bytes = await request.body()
|
|
||||||
signature_header = request.headers.get('x-hub-signature-256', None)
|
|
||||||
webhook_secret = getattr(get_settings().github, 'webhook_secret', None)
|
webhook_secret = getattr(get_settings().github, 'webhook_secret', None)
|
||||||
if webhook_secret:
|
if webhook_secret:
|
||||||
|
body_bytes = await request.body()
|
||||||
|
signature_header = request.headers.get('x-hub-signature-256', None)
|
||||||
verify_signature(body_bytes, webhook_secret, signature_header)
|
verify_signature(body_bytes, webhook_secret, signature_header)
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
_duplicate_requests_cache = {}
|
||||||
|
|
||||||
|
|
||||||
async def handle_request(body: Dict[str, Any]):
|
async def handle_request(body: Dict[str, Any], event: str):
|
||||||
"""
|
"""
|
||||||
Handle incoming GitHub webhook requests.
|
Handle incoming GitHub webhook requests.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
body: The request body.
|
body: The request body.
|
||||||
|
event: The GitHub event type.
|
||||||
"""
|
"""
|
||||||
action = body.get("action")
|
action = body.get("action")
|
||||||
if not action:
|
if not action:
|
||||||
return {}
|
return {}
|
||||||
agent = PRAgent()
|
agent = PRAgent()
|
||||||
|
bot_user = get_settings().github_app.bot_user
|
||||||
|
logging.info(f"action: '{action}'")
|
||||||
|
logging.info(f"event: '{event}'")
|
||||||
|
|
||||||
|
if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# handle all sorts of comment events (e.g. issue_comment)
|
||||||
if action == 'created':
|
if action == 'created':
|
||||||
if "comment" not in body:
|
if "comment" not in body:
|
||||||
return {}
|
return {}
|
||||||
comment_body = body.get("comment", {}).get("body")
|
comment_body = body.get("comment", {}).get("body")
|
||||||
sender = body.get("sender", {}).get("login")
|
sender = body.get("sender", {}).get("login")
|
||||||
if sender and 'bot' in sender:
|
if sender and bot_user in sender:
|
||||||
|
logging.info(f"Ignoring comment from {bot_user} user")
|
||||||
return {}
|
return {}
|
||||||
if "issue" not in body or "pull_request" not in body["issue"]:
|
logging.info(f"Processing comment from {sender} user")
|
||||||
|
if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]:
|
||||||
|
api_url = body["issue"]["pull_request"]["url"]
|
||||||
|
elif "comment" in body and "pull_request_url" in body["comment"]:
|
||||||
|
api_url = body["comment"]["pull_request_url"]
|
||||||
|
else:
|
||||||
return {}
|
return {}
|
||||||
pull_request = body["issue"]["pull_request"]
|
logging.info(f"Handling comment because of event={event} and action={action}")
|
||||||
api_url = pull_request.get("url")
|
|
||||||
comment_id = body.get("comment", {}).get("id")
|
comment_id = body.get("comment", {}).get("id")
|
||||||
provider = get_git_provider()(pr_url=api_url)
|
provider = get_git_provider()(pr_url=api_url)
|
||||||
await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id))
|
await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id))
|
||||||
|
|
||||||
|
# handle pull_request event:
|
||||||
elif action == "opened" or 'reopened' in action:
|
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
|
||||||
|
# as well as direct review requests from the bot
|
||||||
|
elif event == 'pull_request':
|
||||||
pull_request = body.get("pull_request")
|
pull_request = body.get("pull_request")
|
||||||
if not pull_request:
|
if not pull_request:
|
||||||
return {}
|
return {}
|
||||||
api_url = pull_request.get("url")
|
api_url = pull_request.get("url")
|
||||||
if not api_url:
|
if not api_url:
|
||||||
return {}
|
return {}
|
||||||
await agent.handle_request(api_url, "/auto_review")
|
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
||||||
|
return {}
|
||||||
|
if action in get_settings().github_app.handle_pr_actions:
|
||||||
|
if action == "review_requested":
|
||||||
|
if body.get("requested_reviewer", {}).get("login", "") != bot_user:
|
||||||
|
return {}
|
||||||
|
if pull_request.get("created_at") == pull_request.get("updated_at"):
|
||||||
|
# avoid double reviews when opening a PR for the first time
|
||||||
|
return {}
|
||||||
|
logging.info(f"Performing review because of event={event} and action={action}")
|
||||||
|
for command in get_settings().github_app.pr_commands:
|
||||||
|
logging.info(f"Performing command: {command}")
|
||||||
|
await agent.handle_request(api_url, command)
|
||||||
|
|
||||||
|
logging.info("event or action does not require handling")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
In some deployments its possible to get duplicate requests if the handling is long,
|
||||||
|
This function checks if the request is duplicate and if so - ignores it.
|
||||||
|
"""
|
||||||
|
request_hash = hash(str(body))
|
||||||
|
logging.info(f"request_hash: {request_hash}")
|
||||||
|
request_time = time.monotonic()
|
||||||
|
ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds
|
||||||
|
to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl]
|
||||||
|
for key in to_delete:
|
||||||
|
del _duplicate_requests_cache[key]
|
||||||
|
is_duplicate = request_hash in _duplicate_requests_cache
|
||||||
|
_duplicate_requests_cache[request_hash] = request_time
|
||||||
|
if is_duplicate:
|
||||||
|
logging.info(f"Ignoring duplicate request {request_hash}")
|
||||||
|
return is_duplicate
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
# Override the deployment type to app
|
if get_settings().github_app.override_deployment_type:
|
||||||
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app")
|
# Override the deployment type to app
|
||||||
|
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "app")
|
||||||
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
||||||
middleware = [Middleware(RawContextMiddleware)]
|
middleware = [Middleware(RawContextMiddleware)]
|
||||||
app = FastAPI(middleware=middleware)
|
app = FastAPI(middleware=middleware)
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=3000)
|
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "3000")))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -52,6 +52,21 @@ extra_instructions = ""
|
|||||||
deployment_type = "user"
|
deployment_type = "user"
|
||||||
ratelimit_retries = 5
|
ratelimit_retries = 5
|
||||||
|
|
||||||
|
[github_app]
|
||||||
|
# these toggles allows running the github app from custom deployments
|
||||||
|
bot_user = "github-actions[bot]"
|
||||||
|
override_deployment_type = true
|
||||||
|
# in some deployments it's possible to get duplicate requests if the handling is long,
|
||||||
|
# these settings are used to avoid handling duplicate requests.
|
||||||
|
duplicate_requests_cache = false
|
||||||
|
duplicate_requests_cache_ttl = 60 # in seconds
|
||||||
|
# settings for "pull_request" event
|
||||||
|
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
|
||||||
|
pr_commands = [
|
||||||
|
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||||
|
"/auto_review",
|
||||||
|
]
|
||||||
|
|
||||||
[gitlab]
|
[gitlab]
|
||||||
# URL to the gitlab service
|
# URL to the gitlab service
|
||||||
url = "https://gitlab.com"
|
url = "https://gitlab.com"
|
||||||
|
Reference in New Issue
Block a user