Initial commit of gitea-ai-webhook bot with CI/CD

This commit is contained in:
Ben
2026-01-01 04:39:29 +00:00
commit c8ea6d7288
6 changed files with 239 additions and 0 deletions

View 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
View File

@@ -0,0 +1,5 @@
__pycache__/
*.pyc
.env
.venv

29
Dockerfile Normal file
View 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
View 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
View File

@@ -0,0 +1,5 @@
fastapi
uvicorn
requests
pydantic

136
webhook_server.py Normal file
View 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"}