Merge pull request 'git-webook-entrypoint' (#2) from git-webook-entrypoint into master
All checks were successful
Build and Push Image / Build and push image (push) Successful in 38m36s

Reviewed-on: #2
This commit is contained in:
armistace 2026-05-20 23:05:54 +10:00
commit a34a3c3a67
4 changed files with 154 additions and 10 deletions

View File

@ -22,4 +22,9 @@ SEMGRAPH_API_TOKEN=
TOTAL_FLOW_TIMEOUT=600 TOTAL_FLOW_TIMEOUT=600
PER_CREW_TIMEOUT=300 PER_CREW_TIMEOUT=300
LOG_LEVEL=INFO LOG_LEVEL=INFO
# Gitea Webhook Configuration
ACCESS_GITEA_URL=http://192.168.178.160:3000
ACCESS_GITEA_TOKEN=your_gitea_personal_access_token_here
ACCESS_GITEA_SECRET=your_webhook_secret_here

View File

@ -44,14 +44,6 @@ jobs:
tags: | tags: |
git.aridgwayweb.com/armistace/pr-reviewer:latest git.aridgwayweb.com/armistace/pr-reviewer:latest
- name: Trivy Scan
run: |
TRIVY_VERSION=$(curl -s https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
wget -qO /tmp/trivy.tar.gz "https://github.com/aquasecurity/trivy/releases/download/${TRIVY_VERSION}/trivy_${TRIVY_VERSION#v}_Linux-64bit.tar.gz"
tar xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy
chmod +x /usr/local/bin/trivy
trivy image --format table --exit-code 1 --ignore-unfixed --vuln-type os,library --severity HIGH,CRITICAL git.aridgwayweb.com/armistace/pr-reviewer:latest
- name: Deploy - name: Deploy
run: | run: |
echo "Installing Kubectl" echo "Installing Kubectl"
@ -73,5 +65,8 @@ jobs:
--from-literal=LOG_LEVEL=INFO \ --from-literal=LOG_LEVEL=INFO \
--from-literal=TOTAL_FLOW_TIMEOUT=600 \ --from-literal=TOTAL_FLOW_TIMEOUT=600 \
--from-literal=PER_CREW_TIMEOUT=300 \ --from-literal=PER_CREW_TIMEOUT=300 \
--from-literal=ACCESS_GITEA_URL=${{ vars.ACCESS_GITEA_URL }} \
--from-literal=ACCESS_GITEA_TOKEN=${{ secrets.ACCESS_GITEA_TOKEN }} \
--from-literal=ACCESS_GITEA_SECRET=${{ secrets.ACCESS_GITEA_SECRET }} \
--namespace=pr-reviewer --namespace=pr-reviewer
kubectl apply -f kube/pr-reviewer_deployment.yaml && kubectl apply -f kube/pr-reviewer_service.yaml kubectl apply -f kube/pr-reviewer_deployment.yaml && kubectl apply -f kube/pr-reviewer_service.yaml

View File

@ -26,7 +26,8 @@ dependencies = [
"mcpadapt", "mcpadapt",
"pydantic>=2.5.0", "pydantic>=2.5.0",
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
"gitpython>=3.1.0" "gitpython>=3.1.0",
"requests>=2.28.0"
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@ -1,5 +1,8 @@
import logging import logging
import os import os
import hmac
import hashlib
import base64
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import uvicorn import uvicorn
@ -8,6 +11,7 @@ import asyncio
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
import time import time
import uuid import uuid
import requests
from .flow import CodeReviewFlow from .flow import CodeReviewFlow
from .state import PRReviewState, FileInfo, ContextOverrides from .state import PRReviewState, FileInfo, ContextOverrides
@ -27,6 +31,10 @@ app = FastAPI(
TOTAL_FLOW_TIMEOUT = int(os.getenv("TOTAL_FLOW_TIMEOUT", "600")) # Default 10 minutes TOTAL_FLOW_TIMEOUT = int(os.getenv("TOTAL_FLOW_TIMEOUT", "600")) # Default 10 minutes
PER_CREW_TIMEOUT = int(os.getenv("PER_CREW_TIMEOUT", "300")) # Default 5 minutes PER_CREW_TIMEOUT = int(os.getenv("PER_CREW_TIMEOUT", "300")) # Default 5 minutes
ACCESS_GITEA_URL = os.getenv("ACCESS_GITEA_URL", "http://192.168.178.160:3000")
ACCESS_GITEA_TOKEN = os.getenv("ACCESS_GITEA_TOKEN")
WEBHOOK_SECRET = os.getenv("ACCESS_GITEA_SECRET", "")
@app.get("/api/v1/health") @app.get("/api/v1/health")
async def health_check() -> Dict[str, str]: async def health_check() -> Dict[str, str]:
@ -36,6 +44,141 @@ async def health_check() -> Dict[str, str]:
return {"status": "healthy", "service": "pr-reviewer"} return {"status": "healthy", "service": "pr-reviewer"}
def verify_signature(payload: bytes, signature: str) -> bool:
if not WEBHOOK_SECRET:
return True
mac = hmac.new(WEBHOOK_SECRET.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, signature)
def fetch_pr_files(repo_full: str, pr_number: int) -> List[Dict[str, Any]]:
headers = {"Authorization": f"token {ACCESS_GITEA_TOKEN}"}
url = f"{ACCESS_GITEA_URL}/api/v1/repos/{repo_full}/pulls/{pr_number}/files"
resp = requests.get(url, headers=headers)
resp.raise_for_status()
files_data = resp.json()
files = []
for f in files_data:
filename = f["filename"]
status = f["status"]
content = None
if status in ("added", "modified"):
raw_url = f"{ACCESS_GITEA_URL}/api/v1/repos/{repo_full}/contents/{filename}?ref=pulls/{pr_number}/head"
raw_resp = requests.get(raw_url, headers=headers)
if raw_resp.ok:
raw = raw_resp.json()
if raw.get("encoding") == "base64":
content = base64.b64decode(raw["content"]).decode("utf-8")
files.append({
"filename": filename,
"status": status,
"content": content or "",
"additions": f.get("additions", 0),
"deletions": f.get("deletions", 0),
"patch": f.get("patch", ""),
})
return files
def post_pr_comment(repo_full: str, pr_number: int, comment: str) -> None:
headers = {"Authorization": f"token {ACCESS_GITEA_TOKEN}"}
url = f"{ACCESS_GITEA_URL}/api/v1/repos/{repo_full}/issues/{pr_number}/comments"
resp = requests.post(url, headers=headers, json={"body": comment})
resp.raise_for_status()
logger.info(f"Posted review comment to PR #{pr_number} in {repo_full}")
@app.post("/api/v1/gitea-webhook")
async def gitea_webhook(request: Request) -> Dict[str, Any]:
body = await request.body()
sig = request.headers.get("X-Gitea-Signature", "")
if not verify_signature(body, sig):
raise HTTPException(status_code=403, detail="Invalid signature")
data = await request.json()
event = request.headers.get("X-Gitea-Event")
if event == "pull_request":
action = data.get("action", "")
pr = data["pull_request"]
pr_number = pr["number"]
repo = data["repository"]
repo_full = repo["full_name"]
repo_url = repo.get("html_url", f"{ACCESS_GITEA_URL}/{repo_full}")
if action not in ("opened", "synchronize", "reopened"):
logger.info(f"Ignoring PR action: {action}")
return {"status": "ignored", "reason": f"action '{action}' not processed"}
if not ACCESS_GITEA_TOKEN:
raise HTTPException(status_code=500, detail="ACCESS_GITEA_TOKEN not configured")
try:
files = fetch_pr_files(repo_full, pr_number)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching PR files: {e}")
converted_files = []
for f in files:
converted_files.append(FileInfo(
path=f["filename"],
content=f.get("content"),
status=f.get("status", "modified"),
additions=f.get("additions", 0),
deletions=f.get("deletions", 0),
patch=f.get("patch"),
))
flow_inputs = {
"pr_id": str(pr_number),
"pr_title": pr["title"],
"pr_description": pr.get("body", ""),
"pr_url": f"{repo_url}/pull/{pr_number}",
"repo_name": repo_full,
"repo_url": repo_url,
"branch": pr["head"]["label"],
"base_branch": pr["base"]["label"],
"files": [f.dict() for f in converted_files],
"context_overrides": None,
}
flow = CodeReviewFlow()
loop = asyncio.get_event_loop()
with ThreadPoolExecutor() as pool:
try:
flow_result = await asyncio.wait_for(
loop.run_in_executor(pool, lambda: flow.kickoff(inputs=flow_inputs)),
timeout=TOTAL_FLOW_TIMEOUT,
)
except asyncio.TimeoutError:
logger.error(f"PR review timed out for PR #{pr_number}")
raise HTTPException(
status_code=504,
detail=f"PR review timed out after {TOTAL_FLOW_TIMEOUT} seconds",
)
if flow_result.get("error"):
logger.error(f"PR review failed for PR #{pr_number}: {flow_result['error']}")
try:
summary = flow_result.get("review_summary", "")
if summary:
comment = f"## PR Review Results\n\n{summary}"
post_pr_comment(repo_full, pr_number, comment)
except Exception as e:
logger.warning(f"Failed to post review comment: {e}")
return {
"status": "completed" if not flow_result.get("error") else "failed",
"pr_number": pr_number,
"review_summary": flow_result.get("review_summary"),
"error": flow_result.get("error"),
}
return {"status": "ignored"}
@app.post("/api/v1/review") @app.post("/api/v1/review")
async def review_pr(request: Request) -> Dict[str, Any]: async def review_pr(request: Request) -> Dict[str, Any]:
""" """