Initial commit of gitea-ai-webhook bot with CI/CD
This commit is contained in:
33
.gitea/workflows/docker-build-push.yaml
Normal file
33
.gitea/workflows/docker-build-push.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
name: Build and Push Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.ext.ben.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: gitea.ext.ben.io/${{ github.repository }}:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
# git is required for cloning repos
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Install the AI Review tool
|
||||||
|
# Installing directly from PyPI
|
||||||
|
RUN pip install --no-cache-dir xai-review
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY webhook_server.py .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Command to run the server
|
||||||
|
CMD ["uvicorn", "webhook_server:app", "--host", "0.0.0.0", "--port", "3000"]
|
||||||
31
compose.yaml
Normal file
31
compose.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
services:
|
||||||
|
ai-webhook:
|
||||||
|
build: .
|
||||||
|
container_name: gitea-ai-webhook
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- GITEA_TOKEN=${GITEA_TOKEN}
|
||||||
|
# LLM Configuration
|
||||||
|
- LLM__PROVIDER=${LLM_PROVIDER:-OPENAI}
|
||||||
|
- LLM__META__MODEL=${LLM_MODEL:-gpt-4o}
|
||||||
|
# OpenAI / Compatible API Config
|
||||||
|
- LLM__HTTP_CLIENT__API_TOKEN=${OPENAI_API_KEY}
|
||||||
|
- LLM__HTTP_CLIENT__API_URL=${OPENAI_BASE_URL:-https://api.openai.com/v1/}
|
||||||
|
# VCS Config
|
||||||
|
- VCS__PROVIDER=GITEA
|
||||||
|
- VCS__HTTP_CLIENT__API_TOKEN=${GITEA_TOKEN}
|
||||||
|
- VCS__HTTP_CLIENT__API_URL=${GITEA_API_URL:-http://gitea-server:3000/api/v1}
|
||||||
|
volumes:
|
||||||
|
- xai-cache:/root/.cache
|
||||||
|
networks:
|
||||||
|
- gitea_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
xai-cache:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
gitea_network:
|
||||||
|
external: true # Assuming Gitea is on a network, or remove if standalone
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
requests
|
||||||
|
pydantic
|
||||||
136
webhook_server.py
Normal file
136
webhook_server.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
# Configuration from environment variables
|
||||||
|
GITEA_TOKEN = os.getenv("GITEA_TOKEN")
|
||||||
|
# XAI Review configuration should also be in env vars (e.g. OPENAI_API_KEY, etc.)
|
||||||
|
|
||||||
|
class GiteaWebhookPayload(BaseModel):
|
||||||
|
action: str
|
||||||
|
number: int
|
||||||
|
pull_request: Dict[str, Any]
|
||||||
|
repository: Dict[str, Any]
|
||||||
|
sender: Dict[str, Any]
|
||||||
|
|
||||||
|
def run_review_task(payload: Dict[str, Any]):
|
||||||
|
repo_name = payload["repository"]["full_name"]
|
||||||
|
pr_number = payload["number"]
|
||||||
|
clone_url = payload["repository"]["clone_url"]
|
||||||
|
head_sha = payload["pull_request"]["head"]["sha"]
|
||||||
|
base_ref = payload["pull_request"]["base"]["ref"]
|
||||||
|
head_ref = payload["pull_request"]["head"]["ref"] # Branch name
|
||||||
|
|
||||||
|
logger.info(f"Starting review for PR #{pr_number} in {repo_name} (SHA: {head_sha})")
|
||||||
|
|
||||||
|
# Create a temporary directory for the repo
|
||||||
|
work_dir = tempfile.mkdtemp(prefix="gitea-review-")
|
||||||
|
try:
|
||||||
|
# Clone the repository
|
||||||
|
# We might need authentication for private repos.
|
||||||
|
# For simplicity, assuming public or token usage in URL if needed.
|
||||||
|
# Ideally, use the GITEA_TOKEN to authenticate.
|
||||||
|
|
||||||
|
# Inject token into URL if provided and not already present
|
||||||
|
clean_clone_url = clone_url
|
||||||
|
if GITEA_TOKEN and "://" in clone_url:
|
||||||
|
protocol, address = clone_url.split("://", 1)
|
||||||
|
# Basic check to avoid double auth
|
||||||
|
if "@" not in address:
|
||||||
|
clean_clone_url = f"{protocol}://oauth2:{GITEA_TOKEN}@{address}"
|
||||||
|
|
||||||
|
logger.info(f"Cloning {repo_name}...")
|
||||||
|
subprocess.check_call(["git", "clone", clean_clone_url, "."], cwd=work_dir)
|
||||||
|
|
||||||
|
# Checkout the head commit
|
||||||
|
logger.info(f"Checking out {head_sha}...")
|
||||||
|
subprocess.check_call(["git", "checkout", head_sha], cwd=work_dir)
|
||||||
|
|
||||||
|
# Run XAI Review
|
||||||
|
# Assuming 'ai-review' alias or 'xai-review' binary is available
|
||||||
|
# The command might be 'ai-review run' or similar depending on the tool ver.
|
||||||
|
# Based on research: 'ai-review run-summary' or just 'ai-review'
|
||||||
|
logger.info("Running AI Review...")
|
||||||
|
|
||||||
|
# We need to set environment variables for ai-review to know context if it wasn't implicit
|
||||||
|
# But usually it reads from git.
|
||||||
|
# We might need to pass specific flags.
|
||||||
|
|
||||||
|
# NOTE: This command is a placeholder based on general usage.
|
||||||
|
# We might need to adjust arguments based on actual help output.
|
||||||
|
cmd = ["ai-review", "run"]
|
||||||
|
|
||||||
|
# Prepare environment variables for ai-review
|
||||||
|
# We need to explicitly pass the pipeline config as env vars because they are dynamic per PR
|
||||||
|
env_vars = {
|
||||||
|
**os.environ,
|
||||||
|
"GitHub_Actions": "false",
|
||||||
|
"VCS__PIPELINE__PULL_NUMBER": str(pr_number),
|
||||||
|
"VCS__PIPELINE__OWNER": payload["repository"]["owner"]["login"],
|
||||||
|
"VCS__PIPELINE__REPO": payload["repository"]["name"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log the env vars for debugging (excluding secrets)
|
||||||
|
debug_env = {k: v for k, v in env_vars.items() if "TOKEN" not in k and "KEY" not in k}
|
||||||
|
logger.info(f"Running ai-review with env: {debug_env}")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=work_dir,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env_vars
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info("AI Review completed successfully.")
|
||||||
|
logger.info(result.stdout)
|
||||||
|
else:
|
||||||
|
logger.error("AI Review failed.")
|
||||||
|
logger.error(result.stderr)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Git operation failed: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An unexpected error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
# Cleanup
|
||||||
|
logger.info(f"Cleaning up {work_dir}...")
|
||||||
|
shutil.rmtree(work_dir)
|
||||||
|
|
||||||
|
@app.post("/webhook")
|
||||||
|
async def handle_webhook(request: Request, background_tasks: BackgroundTasks):
|
||||||
|
try:
|
||||||
|
payload_json = await request.json()
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||||
|
|
||||||
|
# Gitea sends headers like X-Gitea-Event: pull_request
|
||||||
|
event_type = request.headers.get("X-Gitea-Event")
|
||||||
|
if event_type != "pull_request":
|
||||||
|
return {"status": "ignored", "reason": "Not a pull_request event"}
|
||||||
|
|
||||||
|
action = payload_json.get("action")
|
||||||
|
if action not in ["opened", "synchronize", "reopened"]:
|
||||||
|
return {"status": "ignored", "reason": f"Action '{action}' ignored"}
|
||||||
|
|
||||||
|
# Run the review in the background to avoid timing out the webhook
|
||||||
|
background_tasks.add_task(run_review_task, payload_json)
|
||||||
|
|
||||||
|
return {"status": "accepted", "message": "Review queued"}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health_check():
|
||||||
|
return {"status": "ok"}
|
||||||
Reference in New Issue
Block a user