mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-04 04:40:38 +08:00
Merge pull request #264 from Codium-ai/ok/gitlab_webhook
Implementing Gitlab Webhook Secret Verification
This commit is contained in:
21
INSTALL.md
21
INSTALL.md
@ -343,6 +343,27 @@ PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
|
|||||||
review
|
review
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Method 8 - Run a GitLab webhook server
|
||||||
|
|
||||||
|
1. From the GitLab workspace or group, create an access token. Enable the "api" scope only.
|
||||||
|
2. Generate a random secret for your app, and save it for later. For example, you can use:
|
||||||
|
|
||||||
|
```
|
||||||
|
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
|
||||||
|
```
|
||||||
|
3. Follow the instructions to build the Docker image, setup a secrets file and deploy on your own server from [Method 5](#method-5-run-as-a-github-app).
|
||||||
|
4. In the secrets file, fill in the following:
|
||||||
|
- Your OpenAI key.
|
||||||
|
- In the [gitlab] section, fill in personal_access_token and shared_secret. The access token can be a personal access token, or a group or project access token.
|
||||||
|
- Set deployment_type to 'gitlab' in [configuration.toml](./pr_agent/settings/configuration.toml)
|
||||||
|
5. Create a webhook in GitLab. Set the URL to the URL of your app's server. Set the secret token to the generated secret from step 2.
|
||||||
|
In the "Trigger" section, check the ‘comments’ and ‘merge request events’ boxes.
|
||||||
|
6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Appendix - **Debugging LLM API Calls**
|
### Appendix - **Debugging LLM API Calls**
|
||||||
If you're testing your codium/pr-agent server, and need to see if calls were made successfully + the exact call logs, you can use the [LiteLLM Debugger tool](https://docs.litellm.ai/docs/debugging/hosted_debugging).
|
If you're testing your codium/pr-agent server, and need to see if calls were made successfully + the exact call logs, you can use the [LiteLLM Debugger tool](https://docs.litellm.ai/docs/debugging/hosted_debugging).
|
||||||
|
|
||||||
|
@ -154,6 +154,7 @@ There are several ways to use PR-Agent:
|
|||||||
- Allowing you to automate the review process on your private or public repositories
|
- Allowing you to automate the review process on your private or public repositories
|
||||||
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function)
|
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function)
|
||||||
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup)
|
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup)
|
||||||
|
- [Method 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server)
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@ FROM base as github_polling
|
|||||||
ADD pr_agent pr_agent
|
ADD pr_agent pr_agent
|
||||||
CMD ["python", "pr_agent/servers/github_polling.py"]
|
CMD ["python", "pr_agent/servers/github_polling.py"]
|
||||||
|
|
||||||
|
FROM base as gitlab_webhook
|
||||||
|
ADD pr_agent pr_agent
|
||||||
|
CMD ["python", "pr_agent/servers/gitlab_webhook.py"]
|
||||||
|
|
||||||
FROM base as test
|
FROM base as test
|
||||||
ADD requirements-dev.txt .
|
ADD requirements-dev.txt .
|
||||||
RUN pip install -r requirements-dev.txt && rm requirements-dev.txt
|
RUN pip install -r requirements-dev.txt && rm requirements-dev.txt
|
||||||
|
@ -98,6 +98,7 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
api_url = body["comment"]["pull_request_url"]
|
api_url = body["comment"]["pull_request_url"]
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
logging.info(body)
|
||||||
logging.info(f"Handling comment because of event={event} and action={action}")
|
logging.info(f"Handling comment because of event={event} and action={action}")
|
||||||
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)
|
||||||
@ -129,6 +130,7 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
args = split_command[1:]
|
args = split_command[1:]
|
||||||
other_args = update_settings_from_args(args)
|
other_args = update_settings_from_args(args)
|
||||||
new_command = ' '.join([command] + other_args)
|
new_command = ' '.join([command] + other_args)
|
||||||
|
logging.info(body)
|
||||||
logging.info(f"Performing command: {new_command}")
|
logging.info(f"Performing command: {new_command}")
|
||||||
await agent.handle_request(api_url, new_command)
|
await agent.handle_request(api_url, new_command)
|
||||||
|
|
||||||
|
@ -1,21 +1,51 @@
|
|||||||
|
import copy
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import APIRouter, FastAPI, Request, status
|
from fastapi import APIRouter, FastAPI, Request, status
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
|
from starlette.middleware import Middleware
|
||||||
|
from starlette_context import context
|
||||||
|
from starlette_context.middleware import RawContextMiddleware
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
|
from pr_agent.secret_providers import get_secret_provider
|
||||||
|
|
||||||
app = FastAPI()
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
||||||
|
if request.headers.get("X-Gitlab-Token") and secret_provider:
|
||||||
|
request_token = request.headers.get("X-Gitlab-Token")
|
||||||
|
secret = secret_provider.get_secret(request_token)
|
||||||
|
try:
|
||||||
|
secret_dict = json.loads(secret)
|
||||||
|
gitlab_token = secret_dict["gitlab_token"]
|
||||||
|
context["settings"] = copy.deepcopy(global_settings)
|
||||||
|
context["settings"].gitlab.personal_access_token = gitlab_token
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to validate secret {request_token}: {e}")
|
||||||
|
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
||||||
|
elif get_settings().get("GITLAB.SHARED_SECRET"):
|
||||||
|
secret = get_settings().get("GITLAB.SHARED_SECRET")
|
||||||
|
if not request.headers.get("X-Gitlab-Token") == secret:
|
||||||
|
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
||||||
|
else:
|
||||||
|
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
||||||
|
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
|
||||||
|
if not gitlab_token:
|
||||||
|
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
logging.info(json.dumps(data))
|
||||||
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
||||||
logging.info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
logging.info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
||||||
url = data['object_attributes'].get('url')
|
url = data['object_attributes'].get('url')
|
||||||
@ -28,16 +58,18 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
|||||||
background_tasks.add_task(PRAgent().handle_request, url, body)
|
background_tasks.add_task(PRAgent().handle_request, url, body)
|
||||||
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
gitlab_url = get_settings().get("GITLAB.URL", None)
|
gitlab_url = get_settings().get("GITLAB.URL", None)
|
||||||
if not gitlab_url:
|
if not gitlab_url:
|
||||||
raise ValueError("GITLAB.URL is not set")
|
raise ValueError("GITLAB.URL is not set")
|
||||||
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
|
|
||||||
if not gitlab_token:
|
|
||||||
raise ValueError("GITLAB.PERSONAL_ACCESS_TOKEN is not set")
|
|
||||||
get_settings().config.git_provider = "gitlab"
|
get_settings().config.git_provider = "gitlab"
|
||||||
|
middleware = [Middleware(RawContextMiddleware)]
|
||||||
app = FastAPI()
|
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=3000)
|
||||||
|
Reference in New Issue
Block a user