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