mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-02 03:40:38 +08:00

The new feature can be enabled via the new configuration `github_app.handle_push_event`. To avoid any unwanted side-effects, the current default of this configuration is set to `false`. The high level flow (assuming the configuration is enabled): 1. receive push event from GitHub 2. extract branch and commits from event 3. find PR url for branch (currently does not support PRs from forks) 4. perform configured commands (e.g. `/describe`, `/review -i`) The push event flow is guarded by a backlog queue so that multiple push events on the same branch won't trigger multiple duplicate runs of the PR-Agent commands. Example timeline: 1. push 1 - start handling event 2. push 2 - waiting to be handled while push 1 event is still running 3. push 3 - event is dropped since handling it and handling push 2 is the same, so it is redundant 4. push 1 finished being handled 5. push 2 awakens from wait and continues handling (potentially reviewing the commits of both push 2 and push 3) All of these options are configurable and can be enabled/disabled as per the user's desire. Additional minor changes in this PR: 1. Created `DefaultDictWithTimeout` utility class to avoid too much boilerplate code in managing caches for outdated triggers. 2. Guard against running increment review when there are no new commits. 3. Minor styling changes for incremented review text.
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
import hashlib
|
|
import hmac
|
|
import time
|
|
from collections import defaultdict
|
|
from typing import Callable, Any
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def verify_signature(payload_body, secret_token, signature_header):
|
|
"""Verify that the payload was sent from GitHub by validating SHA256.
|
|
|
|
Raise and return 403 if not authorized.
|
|
|
|
Args:
|
|
payload_body: original request body to verify (request.body())
|
|
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
|
|
signature_header: header received from GitHub (x-hub-signature-256)
|
|
"""
|
|
if not signature_header:
|
|
raise HTTPException(status_code=403, detail="x-hub-signature-256 header is missing!")
|
|
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
|
|
expected_signature = "sha256=" + hash_object.hexdigest()
|
|
if not hmac.compare_digest(expected_signature, signature_header):
|
|
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
|
|
|
|
|
|
class RateLimitExceeded(Exception):
|
|
"""Raised when the git provider API rate limit has been exceeded."""
|
|
pass
|
|
|
|
|
|
class DefaultDictWithTimeout(defaultdict):
|
|
"""A defaultdict with a time-to-live (TTL)."""
|
|
|
|
def __init__(
|
|
self,
|
|
default_factory: Callable[[], Any] = None,
|
|
ttl: int = None,
|
|
refresh_interval: int = 60,
|
|
update_key_time_on_get: bool = True,
|
|
*args,
|
|
**kwargs,
|
|
):
|
|
"""
|
|
Args:
|
|
default_factory: The default factory to use for keys that are not in the dictionary.
|
|
ttl: The time-to-live (TTL) in seconds.
|
|
refresh_interval: How often to refresh the dict and delete items older than the TTL.
|
|
update_key_time_on_get: Whether to update the access time of a key also on get (or only when set).
|
|
"""
|
|
super().__init__(default_factory, *args, **kwargs)
|
|
self.__key_times = dict()
|
|
self.__ttl = ttl
|
|
self.__refresh_interval = refresh_interval
|
|
self.__update_key_time_on_get = update_key_time_on_get
|
|
self.__last_refresh = self.__time() - self.__refresh_interval
|
|
|
|
@staticmethod
|
|
def __time():
|
|
return time.monotonic()
|
|
|
|
def __refresh(self):
|
|
if self.__ttl is None:
|
|
return
|
|
request_time = self.__time()
|
|
if request_time - self.__last_refresh > self.__refresh_interval:
|
|
return
|
|
to_delete = [key for key, key_time in self.__key_times.items() if request_time - key_time > self.__ttl]
|
|
for key in to_delete:
|
|
del self[key]
|
|
self.__last_refresh = request_time
|
|
|
|
def __getitem__(self, __key):
|
|
if self.__update_key_time_on_get:
|
|
self.__key_times[__key] = self.__time()
|
|
self.__refresh()
|
|
return super().__getitem__(__key)
|
|
|
|
def __setitem__(self, __key, __value):
|
|
self.__key_times[__key] = self.__time()
|
|
return super().__setitem__(__key, __value)
|
|
|
|
def __delitem__(self, __key):
|
|
del self.__key_times[__key]
|
|
return super().__delitem__(__key)
|