first build attempt
Some checks failed
Build and Push Image / Build and push image (push) Failing after 1m58s

This commit is contained in:
Andrew Ridgway 2026-05-19 23:24:27 +10:00
parent b436a81300
commit bf6fe21ea6
Signed by: armistace
GPG Key ID: C8D9EAC514B47EF1
27 changed files with 449 additions and 634 deletions

View File

@ -1,36 +1,25 @@
# LLM Configuration # LLM Configuration
# Choose one of the following LLM providers: # Provider options: openai, anthropic, ollama
# For OpenAI:
# Required
LLM_MODEL=gpt-4 LLM_MODEL=gpt-4
LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=your_openai_api_key_here
LLM_PROVIDER=openai LLM_PROVIDER=openai
# For Anthropic: # Required for OpenAI/Anthropic
# LLM_MODEL=claude-3-opus-20240229 LLM_BASE_URL=https://api.openai.com/v1
# LLM_BASE_URL=https://api.anthropic.com LLM_API_KEY=your_api_key_here
# LLM_API_KEY=your_anthropic_api_key_here
# LLM_PROVIDER=anthropic
# For Ollama (local): # For Ollama (local or network):
# LLM_MODEL=llama2 # LLM_MODEL=llama2
# LLM_BASE_URL=http://localhost:11434 # LLM_BASE_URL=http://localhost:11434
# LLM_API_KEY=ollama # Ollama doesn't require a real API key
# LLM_PROVIDER=ollama # LLM_PROVIDER=ollama
# MCP Server Configuration # Optional: Semgrep App URL and Token
# Hadolint MCP Server (installed via pip in Docker)
# Checkov MCP Server (installed via pip in Docker)
# Semgrep MCP Server (native, no configuration needed)
# Trivy MCP Server (native, no configuration needed)
# Optional: Semgrep App URL and Token for SEMgrep App functionality
SEMGRAPH_APP_URL= SEMGRAPH_APP_URL=
SEMGRAPH_API_TOKEN= SEMGRAPH_API_TOKEN=
# Timeout Configuration (in seconds) # Timeout Configuration (seconds)
TOTAL_FLOW_TIMEOUT=600 TOTAL_FLOW_TIMEOUT=600
PER_CREW_TIMEOUT=300 PER_CREW_TIMEOUT=300
# Other Configuration
LOG_LEVEL=INFO LOG_LEVEL=INFO

View File

@ -66,7 +66,15 @@ jobs:
chmod 644 /etc/apt/sources.list.d/kubernetes.list chmod 644 /etc/apt/sources.list.d/kubernetes.list
apt-get update apt-get update
apt-get install kubectl apt-get install kubectl
kubectl delete namespace pr-reviewer kubectl delete namespace pr-reviewer --ignore-not-found
kubectl create namespace pr-reviewer kubectl create namespace pr-reviewer
kubectl create secret docker-registry regcred --docker-server=${{ vars.DOCKER_SERVER }} --docker-username=${{ vars.DOCKER_USERNAME }} --docker-password='${{ secrets.DOCKER_PASSWORD }}' --docker-email=${{ vars.DOCKER_EMAIL }} --namespace=pr-reviewer kubectl create secret docker-registry regcred --docker-server=${{ vars.DOCKER_SERVER }} --docker-username=${{ vars.DOCKER_USERNAME }} --docker-password='${{ secrets.DOCKER_PASSWORD }}' --docker-email=${{ vars.DOCKER_EMAIL }} --namespace=pr-reviewer
kubectl apply -f kube/pr-reviewer_pod.yaml && kubectl apply -f kube/pr-reviewer_deployment.yaml && kubectl apply -f kube/pr-reviewer_service.yaml kubectl create secret generic pr-reviewer-env \
--from-literal=LLM_PROVIDER=ollama \
--from-literal=LLM_MODEL=${{ vars.OLLAMA_MODEL }} \
--from-literal=LLM_BASE_URL=http://${{ vars.OLLAMA_SERVER }} \
--from-literal=LOG_LEVEL=INFO \
--from-literal=TOTAL_FLOW_TIMEOUT=600 \
--from-literal=PER_CREW_TIMEOUT=300 \
--namespace=pr-reviewer
kubectl apply -f kube/pr-reviewer_deployment.yaml && kubectl apply -f kube/pr-reviewer_service.yaml

5
.gitignore vendored
View File

@ -1 +1,6 @@
__pycache__/
.pytest_cache/
.benchmarks/
.spec/ .spec/
.env
.venv/

View File

@ -1,5 +1,5 @@
# Stage 1: Base with system dependencies and tool installations # Stage 1: Builder
FROM python:3.12-slim as builder FROM python:3.12-slim AS builder
# Install system dependencies # Install system dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
@ -7,58 +7,52 @@ RUN apt-get update && apt-get install -y \
curl \ curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Hadolint (for Dockerfile linting) # Install Tools
RUN curl -Lo /bin/hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64 && \ RUN curl -Lo /bin/hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64 && chmod +x /bin/hadolint
chmod +x /bin/hadolint RUN pip install checkov semgrep
# Install Checkov (for Kubernetes security scanning)
RUN pip install checkov
# Install Trivy (for container and IaC scanning) - Native MCP server
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Install Semgrep (for code scanning) - Will use native MCP server # Install UV
RUN pip install semgrep
# Install UV package manager
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Stage 2: App with source code and UV sync WORKDIR /app
COPY pyproject.toml .
# Create virtual environment and install dependencies
RUN uv venv /opt/venv
RUN uv pip install --python /opt/venv/bin/python .
# Stage 2: Final
FROM python:3.12-slim FROM python:3.12-slim
# Create non-root user # Install system dependencies needed at runtime
RUN useradd --create-home --shell /bin/bash app
WORKDIR /app
USER app
# Install runtime dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
git \ git \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy UV from builder stage # Create non-root user
COPY --from=builder /bin/uv /bin/uv RUN useradd --create-home --shell /bin/bash app
COPY --from=builder /bin/uvx /bin/uvx WORKDIR /app
# Copy virtual environment and tools from builder
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /bin/hadolint /bin/hadolint
# Copy other tools if needed (Trivy, etc.)
COPY --from=builder /usr/local/bin/trivy /usr/local/bin/trivy
# Copy application code # Copy application code
COPY --chown=app:app pyproject.toml . COPY src/ ./src/
COPY --chown=app:app README.md . COPY mcp_servers/ ./mcp_servers/
COPY --chown=app:app src/ ./src/ COPY crews/ ./crews/
COPY --chown=app:app mcp_servers/ ./mcp_servers/ COPY tools/ ./tools/
COPY --chown=app:app crews/ ./crews/ COPY config/ ./config/
COPY --chown=app:app tools/ ./tools/ COPY contexts/ ./contexts/
COPY --chown=app:app config/ ./config/ COPY README.md .
COPY --chown=app:app contexts/ ./contexts/
# Install Python dependencies using UV # Set the environment variables to use the venv
RUN uv sync --frozen --no-dev ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONPATH="/app/src"
USER app
# Set environment variables
ENV PYTHONPATH=/app/src
ENV PATH="/app/.venv/bin:$PATH"
# Expose port
EXPOSE 8000 EXPOSE 8000
# Set entrypoint
ENTRYPOINT ["uvicorn", "src.pr_reviewer.main:app", "--host", "0.0.0.0", "--port", "8000"] ENTRYPOINT ["uvicorn", "src.pr_reviewer.main:app", "--host", "0.0.0.0", "--port", "8000"]

270
README.md
View File

@ -1,185 +1,191 @@
# PR Reviewer # PR Reviewer
An automated pull request review system using CrewAI and MCP (Model Context Protocol). Automated pull request review system using [CrewAI](https://crewai.com) Flows and MCP (Model Context Protocol) tools.
## Overview Performs three parallel reviews — code quality, security, and infrastructure — then synthesizes a consolidated report via a REST API.
This system provides automated code, security, and infrastructure reviews for pull requests using a multi-agent approach. It leverages CrewAI for orchestrating specialized review agents and MCP (Model Context Protocol) for integrating with various static analysis tools.
## Features ## Features
- **Code Review**: Uses Semgrep (via MCP) to check code quality, best practices, and maintainability - **Code Review** — style, best practices, maintainability (powered by Semgrep)
- **Security Review**: Uses Trivy (native MCP) to identify security vulnerabilities - **Security Review** — vulnerabilities, injection risks, auth issues (powered by Trivy)
- **Infrastructure Review**: Uses Hadolint and Checkov (via MCP wrappers) to review Dockerfiles and Kubernetes manifests - **Infrastructure Review** — Dockerfiles, Kubernetes manifests, IaC (powered by Hadolint + Checkov)
- **Contextual Review**: Incorporates customizable guidelines for code, security, and infrastructure reviews - **Summarisation** — merges all three reviews into a single actionable report
- **Automated Orchestration**: Uses CrewAI Flows to manage the review process - **REST API** — FastAPI endpoints for health check and review trigger
- **REST API**: FastAPI endpoint for triggering reviews - **Dockerized** — multi-stage build with all tools bundled
- **Containerized**: Docker support for easy deployment
## Architecture ## Architecture
The system follows a modular architecture with: ```
- State management using Pydantic models POST /api/v1/review
- LLM factory for flexible provider support (OpenAI, Anthropic, Ollama)
- Context resolution system for incorporating review guidelines
- Crew-based implementation for each review type (code, security, infrastructure) CodeReviewFlow (CrewAI Flow)
- MCP server integrations for static analysis tools
- Flow-based orchestration for managing the review process ┌────┼──────────────┐
- RESTful API for integration with CI/CD systems ▼ ▼ ▼
Code Security Infra
Review Review Review
│ │ │
└─────┼────────────┘
Summariser
JSON Response
```
## Installation LLM-agnostic via CrewAI's LLM abstraction — works with OpenAI, Anthropic, or Ollama.
## Quick Start
### Prerequisites ### Prerequisites
- Python 3.10-3.13
- UV package manager
- Git
- Docker (optional, for containerized deployment)
### Local Development - Docker
1. Clone the repository - An LLM provider (OpenAI API key, Anthropic key, or a running Ollama instance)
2. Install UV package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh`
3. Activate UV environment: `source $HOME/.local/bin/env`
4. Create virtual environment: `uv venv .venv`
5. Activate virtual environment: `source .venv/bin/activate`
6. Install dependencies: `uv pip install -e .`
7. Configure environment variables (see `.env.example`)
### Docker Deployment ### Setup
1. Build the Docker image: `docker build -t pr-reviewer .`
2. Run the container: `docker run -p 8000:8000 --env-file .env pr-reviewer`
## Usage
### API Endpoints
#### Health Check
```bash ```bash
GET /api/v1/health cp .env.example .env
# Edit .env with your LLM provider details
``` ```
Returns the health status of the service.
#### Trigger PR Review ### Run
```bash ```bash
POST /api/v1/review docker compose up
``` ```
Initiates a pull request review.
Request Body: Server starts at `http://localhost:8000`.
```json
{ ### Test
```bash
# Health check
curl http://localhost:8000/api/v1/health
# Trigger a review
curl -X POST http://localhost:8000/api/v1/review \
-H "Content-Type: application/json" \
-d '{
"pr_id": "123", "pr_id": "123",
"title": "Add new feature", "title": "Add user authentication",
"description": "This PR adds a new feature to the application", "repo": {"name": "myapp/backend", "url": "https://github.com/myapp/backend"},
"repo": { "source": {"branch": "feature/auth"},
"name": "my-repo", "target": {"branch": "main"},
"url": "https://github.com/user/my-repo"
},
"source": {
"branch": "feature/new-feature",
"commit": "abc123"
},
"target": {
"branch": "main",
"commit": "def456"
},
"files": [ "files": [
{ {
"path": "src/main.py", "path": "auth.py",
"content": "print('Hello World')", "status": "added",
"status": "modified", "content": "def login(user, pwd):\n if user == \"admin\" and pwd == \"admin\":\n return True",
"additions": 1, "additions": 3,
"deletions": 0 "deletions": 0
} }
], ]
"context": { }'
"code_review": "Follow PEP8 guidelines",
"security_review": "Check for SQL injection vulnerabilities",
"infra_review": "Ensure Dockerfile follows best practices"
}
}
``` ```
Response: ## API
### `GET /api/v1/health`
Returns service status.
```json
{"status": "healthy", "service": "pr-reviewer"}
```
### `POST /api/v1/review`
Triggers a full PR review.
**Request body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `pr_id` | string | yes | PR identifier |
| `title` | string | yes | PR title |
| `description` | string | no | PR description |
| `repo.name` | string | yes | Repository name |
| `repo.url` | string | yes | Repository URL |
| `source.branch` | string | yes | Source branch |
| `source.commit` | string | no | Source commit SHA |
| `target.branch` | string | yes | Target branch |
| `target.commit` | string | no | Target commit SHA |
| `files[]` | array | no | Changed files |
| `files[].path` | string | yes | File path |
| `files[].content` | string | no | File contents |
| `files[].status` | string | yes | `added`, `modified`, `removed` |
| `files[].additions` | int | no | Lines added |
| `files[].deletions` | int | no | Lines removed |
| `files[].patch` | string | no | Unified diff |
| `context.code_review` | string | no | Code review guidelines override |
| `context.security_review` | string | no | Security review guidelines override |
| `context.infra_review` | string | no | Infrastructure review guidelines override |
**Response:**
```json ```json
{ {
"review_id": "uuid-string", "review_id": "uuid",
"status": "completed", "status": "completed",
"timestamp": "2023-05-08T10:00:00Z", "timestamp": "2024-01-01T00:00:00Z",
"results": { "results": {
"code_review": "Code review results...", "code_review": "...",
"security_review": "Security review results...", "security_review": "...",
"infra_review": "Infrastructure review results...", "infra_review": "...",
"summary": "Synthesized review summary..." "summary": "..."
}, },
"metadata": { "metadata": {
"processing_time_seconds": 45.2, "processing_time_seconds": 290.22,
"pr_id": "123", "pr_id": "123",
"repo": { "repo": {"name": "myapp/backend", "url": "https://github.com/myapp/backend"}
"name": "my-repo",
"url": "https://github.com/user/my-repo"
}
} }
} }
``` ```
## Configuration ## Configuration
### Environment Variables All configuration via environment variables in `.env`:
See `.env.example` for detailed configuration options.
### Context Files | Variable | Default | Description |
Default review guidelines are located in `contexts/defaults/`: |----------|---------|-------------|
- `code_review.md`: Coding practice guidelines | `LLM_MODEL` | (required) | Model name (e.g. `gpt-4`, `gemma4:31b-cloud`) |
- `security_review.md`: Security guidelines | `LLM_PROVIDER` | (required) | `openai`, `anthropic`, or `ollama` |
- `infra_review.md`: Infrastructure guidelines | `LLM_BASE_URL` | — | API base URL |
| `LLM_API_KEY` | — | API key (not needed for Ollama) |
These can be overridden via the API context parameter. | `TOTAL_FLOW_TIMEOUT` | `600` | Max seconds for full review |
| `PER_CREW_TIMEOUT` | `300` | Max seconds per crew |
| `LOG_LEVEL` | `INFO` | Logging level |
## Development ## Development
### Running Tests
```bash ```bash
# Run unit tests # Install deps
pytest uv pip install -e ".[dev]"
# Run tests with coverage # Run tests
pytest --cov=src.pr_reviewer pytest tests/
# Run specific test categories # Run server locally
pytest tests/unit/ uvicorn src.pr_reviewer.main:app --reload
pytest tests/integration/
``` ```
### Code Style ## Project Structure
The project uses Black for code formatting and Flake8 for linting.
Run formatting:
```bash
black src/
``` ```
├── config/ # Shared agent/task YAML configs
Run linting: ├── contexts/ # Default review guidelines (markdown)
```bash ├── crews/ # Crew definitions (code, security, infra, summariser)
flake8 src/ ├── mcp_servers/ # MCP tool wrappers (Hadolint, Checkov)
├── src/pr_reviewer/ # Core application code
│ ├── main.py # FastAPI app
│ ├── flow.py # CrewAI Flow orchestration
│ ├── state.py # Pydantic state models
│ ├── llm.py # LLM factory
│ └── context.py # Context resolution
├── tests/ # Unit and integration tests
├── docker-compose.yaml
├── Dockerfile
└── pyproject.toml
``` ```
## Deployment
### Kubernetes
Kubernetes manifests are available in the `k8s/` directory:
- Secret for LLM configuration
- Deployment for the PR Reviewer service
- Service for exposing the API
### Gitea Actions
GitHub Actions workflow for CI/CD is available in `.gitea/workflows/deploy.yaml`.
## License
MIT
## Contributing
1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Open a pull request

View File

View File

@ -1,7 +0,0 @@
# Summarizer Agent Configuration
summariser:
role: Senior Code Review Coordinator
goal: Synthesize individual review results into a cohesive, actionable review report
backstory: You are a senior technical lead with extensive experience in code review practices across multiple domains. You excel at combining feedback from different reviewers into a clear, prioritized, and actionable summary that helps development teams improve their code efficiently.
verbose: true
allow_delegation: false

View File

@ -1,16 +0,0 @@
# Summarizer Task Configuration
summarise_task:
description: |
Synthesize the results from code, security, and infrastructure reviews into a cohesive review report.
Code Review Results: {code_review_results}
Security Review Results: {security_review_results}
Infrastructure Review Results: {infra_review_results}
Context: {context}
expected_output: |
A comprehensive review report that includes:
- Executive summary of all findings
- Prioritized list of issues (critical, high, medium, low)
- Specific recommendations for each domain (code, security, infrastructure)
- Overall assessment and recommendation (e.g., Approved, Approved with Minor Changes, Significant Changes Needed)
- Summary of positive aspects of the PR
agent: summariser

View File

@ -1,19 +1,21 @@
from crewai import CrewBase, Agent, Task, Crew from crewai import Agent, Task, Crew
from crewai.project import CrewBase, agent, task, crew
from crewai_tools import MCPServerAdapter from crewai_tools import MCPServerAdapter
from mcp import StdioServerParameters from mcp import StdioServerParameters
import os import os
from typing import Dict, Any from typing import Dict, Any
from pr_reviewer.llm import get_llm
class CodeReviewCrew(CrewBase): @CrewBase
class CodeReviewCrew:
"""Code Review Crew for conducting code quality reviews.""" """Code Review Crew for conducting code quality reviews."""
agents_config = "config/agents.yaml" agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml" tasks_config = "config/tasks.yaml"
def __init__(self): def __init__(self):
super().__init__() self.llm = get_llm()
# Configure Semgrep MCP server connection
self.semgrep_server_params = StdioServerParameters( self.semgrep_server_params = StdioServerParameters(
command="semgrep", command="semgrep",
args=["--metrics=off", "--json", "--stdin-display-name", "scanned_code", "--"], args=["--metrics=off", "--json", "--stdin-display-name", "scanned_code", "--"],
@ -24,32 +26,38 @@ class CodeReviewCrew(CrewBase):
} }
) )
@Agent @agent
def code_reviewer(self) -> Agent: def code_reviewer(self) -> Agent:
"""Senior Software Engineer agent for code review.""" """Senior Software Engineer agent for code review."""
return Agent( return Agent(
config=self.agents_config["code_reviewer"], config=self.agents_config["code_reviewer"],
tools=[], # Tools will be added via MCP adapter in the task llm=self.llm,
tools=[],
verbose=True verbose=True
) )
@Task @task
def code_review_task(self) -> Task: def code_review_task(self) -> Task:
"""Task for conducting code review.""" """Task for conducting code review."""
return Task( return Task(
config=self.tasks_config["code_review_task"], config=self.tasks_config["code_review_task"],
) )
@Crew @crew
def crew(self) -> Crew: def crew(self) -> Crew:
"""Create the Code Review crew.""" """Create the Code Review crew."""
# Create MCP server adapter for Semgrep tools = []
try:
semgrep_adapter = MCPServerAdapter(self.semgrep_server_params) semgrep_adapter = MCPServerAdapter(self.semgrep_server_params)
if hasattr(semgrep_adapter, 'tools'):
tools = semgrep_adapter.tools
except Exception as e:
print(f"MCP adapter not available: {e}")
return Crew( return Crew(
agents=[self.code_reviewer()], agents=[self.code_reviewer()],
tasks=[self.code_review_task()], tasks=[self.code_review_task()],
process="sequential", process="sequential",
verbose=True, verbose=True,
tools=semgrep_adapter.tools if hasattr(semgrep_adapter, 'tools') else [], tools=tools,
) )

View File

@ -1,60 +1,65 @@
from crewai import CrewBase, Agent, Task, Crew from crewai import Agent, Task, Crew
from crewai.project import CrewBase, agent, task, crew
from crewai_tools import MCPServerAdapter from crewai_tools import MCPServerAdapter
from mcp import StdioServerParameters from mcp import StdioServerParameters
import os import os
from typing import Dict, Any from typing import Dict, Any
from pr_reviewer.llm import get_llm
class InfraReviewCrew(CrewBase): @CrewBase
class InfraReviewCrew:
"""Infrastructure Review Crew for conducting infrastructure reviews.""" """Infrastructure Review Crew for conducting infrastructure reviews."""
agents_config = "config/agents.yaml" agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml" tasks_config = "config/tasks.yaml"
def __init__(self): def __init__(self):
super().__init__() self.llm = get_llm()
# Configure Hadolint MCP server connection
self.hadolint_server_params = StdioServerParameters( self.hadolint_server_params = StdioServerParameters(
command="python", command="python",
args=["/home/armistace/dev/pr_reviewer/mcp_servers/hadolint_mcp.py"], args=["/app/mcp_servers/hadolint_mcp.py"],
env=os.environ env=os.environ
) )
# Configure Checkov MCP server connection
self.checkov_server_params = StdioServerParameters( self.checkov_server_params = StdioServerParameters(
command="python", command="python",
args=["/home/armistace/dev/pr_reviewer/mcp_servers/checkov_mcp.py"], args=["/app/mcp_servers/checkov_mcp.py"],
env=os.environ env=os.environ
) )
@Agent @agent
def infra_reviewer(self) -> Agent: def infra_reviewer(self) -> Agent:
"""DevOps/Platform Engineer agent for infrastructure review.""" """DevOps/Platform Engineer agent for infrastructure review."""
return Agent( return Agent(
config=self.agents_config["infra_reviewer"], config=self.agents_config["infra_reviewer"],
tools=[], # Tools will be added via MCP adapter in the task llm=self.llm,
tools=[],
verbose=True verbose=True
) )
@Task @task
def infra_review_task(self) -> Task: def infra_review_task(self) -> Task:
"""Task for conducting infrastructure review.""" """Task for conducting infrastructure review."""
return Task( return Task(
config=self.tasks_config["infra_review_task"], config=self.tasks_config["infra_review_task"],
) )
@Crew @crew
def crew(self) -> Crew: def crew(self) -> Crew:
"""Create the Infrastructure Review crew.""" """Create the Infrastructure Review crew."""
# Create MCP server adapters for Hadolint and Checkov
hadolint_adapter = MCPServerAdapter(self.hadolint_server_params)
checkov_adapter = MCPServerAdapter(self.checkov_server_params)
# Combine tools from both adapters
all_tools = [] all_tools = []
try:
hadolint_adapter = MCPServerAdapter(self.hadolint_server_params)
if hasattr(hadolint_adapter, 'tools'): if hasattr(hadolint_adapter, 'tools'):
all_tools.extend(hadolint_adapter.tools) all_tools.extend(hadolint_adapter.tools)
except Exception as e:
print(f"Hadolint MCP adapter not available: {e}")
try:
checkov_adapter = MCPServerAdapter(self.checkov_server_params)
if hasattr(checkov_adapter, 'tools'): if hasattr(checkov_adapter, 'tools'):
all_tools.extend(checkov_adapter.tools) all_tools.extend(checkov_adapter.tools)
except Exception as e:
print(f"Checkov MCP adapter not available: {e}")
return Crew( return Crew(
agents=[self.infra_reviewer()], agents=[self.infra_reviewer()],

View File

@ -1,41 +1,41 @@
from crewai import CrewBase, Agent, Task, Crew from crewai import Agent, Task, Crew
from crewai.project import CrewBase, agent, task, crew
from crewai_tools import MCPServerAdapter from crewai_tools import MCPServerAdapter
from mcp import StdioServerParameters from mcp import StdioServerParameters
import os import os
from typing import Dict, Any from typing import Dict, Any
from pr_reviewer.llm import get_llm
class SecurityReviewCrew(CrewBase): @CrewBase
class SecurityReviewCrew:
"""Security Review Crew for conducting security reviews.""" """Security Review Crew for conducting security reviews."""
agents_config = "config/agents.yaml" agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml" tasks_config = "config/tasks.yaml"
def __init__(self): def __init__(self):
super().__init__() self.llm = get_llm()
# Trivy uses native MCP server, so we don't need to configure a wrapper. self.trivy_server_params = None
# However, we might need to set up connection parameters if required by the native server.
# For now, we assume the native Trivy MCP server is available at a known address or via stdio.
# We'll leave the MCP server configuration empty and rely on the native server being available.
self.trivy_server_params = None # Placeholder for if we need to configure stdio parameters
@Agent @agent
def security_reviewer(self) -> Agent: def security_reviewer(self) -> Agent:
"""Application Security Engineer agent for security review.""" """Application Security Engineer agent for security review."""
return Agent( return Agent(
config=self.agents_config["security_reviewer"], config=self.agents_config["security_reviewer"],
tools=[], # Tools will be added via MCP adapter in the task llm=self.llm,
tools=[],
verbose=True verbose=True
) )
@Task @task
def security_review_task(self) -> Task: def security_review_task(self) -> Task:
"""Task for conducting security review.""" """Task for conducting security review."""
return Task( return Task(
config=self.tasks_config["security_review_task"], config=self.tasks_config["security_review_task"],
) )
@Crew @crew
def crew(self) -> Crew: def crew(self) -> Crew:
"""Create the Security Review crew.""" """Create the Security Review crew."""
# If we had an MCP server to wrap, we would create an adapter here. # If we had an MCP server to wrap, we would create an adapter here.

View File

@ -1,37 +1,40 @@
from crewai import CrewBase, Agent, Task, Crew from crewai import Agent, Task, Crew
from crewai.project import CrewBase, agent, task, crew
from crewai_tools import MCPServerAdapter from crewai_tools import MCPServerAdapter
from mcp import StdioServerParameters from mcp import StdioServerParameters
import os import os
from typing import Dict, Any from typing import Dict, Any
from pr_reviewer.llm import get_llm
class SummariserCrew(CrewBase): @CrewBase
class SummariserCrew:
"""Summariser Crew for synthesizing review results.""" """Summariser Crew for synthesizing review results."""
agents_config = "config/agents.yaml" agents_config = "config/agents.yaml"
tasks_config = "config/tasks.yaml" tasks_config = "config/tasks.yaml"
def __init__(self): def __init__(self):
super().__init__() self.llm = get_llm()
# The summarizer doesn't need MCP server connections as it works with text results
@Agent @agent
def summariser(self) -> Agent: def summariser(self) -> Agent:
"""Senior Code Review Coordinator agent for summarizing reviews.""" """Senior Code Review Coordinator agent for summarizing reviews."""
return Agent( return Agent(
config=self.agents_config["summariser"], config=self.agents_config["summariser"],
tools=[], # No tools needed for summarization llm=self.llm,
tools=[],
verbose=True verbose=True
) )
@Task @task
def summarise_task(self) -> Task: def summarise_task(self) -> Task:
"""Task for synthesizing review results.""" """Task for synthesizing review results."""
return Task( return Task(
config=self.tasks_config["summarise_task"], config=self.tasks_config["summarise_task"],
) )
@Crew @crew
def crew(self) -> Crew: def crew(self) -> Crew:
"""Create the Summariser crew.""" """Create the Summariser crew."""
return Crew( return Crew(

16
docker-compose.yaml Normal file
View File

@ -0,0 +1,16 @@
services:
pr-reviewer:
build:
context: .
dockerfile: Dockerfile
image: pr-reviewer:latest
ports:
- "8000:8000"
env_file:
- .env
volumes:
- ./src:/app/src
- ./config:/app/config
restart: always
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@ -6,7 +6,7 @@ metadata:
app: pr-reviewer app: pr-reviewer
namespace: pr-reviewer namespace: pr-reviewer
spec: spec:
replicas: 3 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: pr-reviewer app: pr-reviewer
@ -20,5 +20,15 @@ spec:
image: git.aridgwayweb.com/armistace/pr-reviewer:latest image: git.aridgwayweb.com/armistace/pr-reviewer:latest
ports: ports:
- containerPort: 8000 - containerPort: 8000
envFrom:
- secretRef:
name: pr-reviewer-env
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
imagePullSecrets: imagePullSecrets:
- name: regcred - name: regcred

View File

@ -1,13 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: pr-reviewer
namespace: pr-reviewer
spec:
containers:
- name: pr-reviewer
image: git.aridgwayweb.com/armistace/pr-reviewer:latest
ports:
- containerPort: 8000
imagePullSecrets:
- name: regcred

View File

@ -19,10 +19,11 @@ classifiers = [
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
] ]
dependencies = [ dependencies = [
"crewai>=0.28.0", "crewai[tools]>=0.80.0",
"fastapi>=0.104.0", "fastapi>=0.104.0",
"uvicorn>=0.24.0", "uvicorn>=0.24.0",
"mcp>=0.1.0", "mcp>=0.1.0",
"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"

View File

@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""
Simple test to verify the basic components work without Docker.
This tests the core components without requiring Docker build.
"""
import sys
import os
# Add the project root to the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def test_imports():
"""Test that all modules can be imported."""
try:
# Test core modules
from src.pr_reviewer.state import FileInfo, ContextOverrides, PRReviewState
from src.pr_reviewer.llm import create_llm
from src.pr_reviewer.context import resolve_context
print("✓ Core modules imported successfully")
# Test state creation
state = PRReviewState(
pr_id="123",
pr_title="Test PR",
repo_name="test-repo",
repo_url="https://github.com/test/repo",
branch="feature",
base_branch="main"
)
print("✓ State creation works")
# Test context resolution (will use default files if they exist)
context = resolve_context(state)
print(f"✓ Context resolution works: {list(context.keys())}")
# Test file info
file_info = FileInfo(
path="test.py",
content="print('hello')",
status="added",
additions=1,
deletions=0
)
print("✓ FileInfo creation works")
# Test context overrides
context_overrides = ContextOverrides(
code_review="Custom code review",
security_review="Custom security review"
)
print("✓ ContextOverrides creation works")
print("\n✓ All basic component tests passed!")
return True
except Exception as e:
print(f"✗ Test failed with error: {e}")
import traceback
traceback.print_exc()
return False
def test_crew_imports():
"""Test that crew modules can be imported."""
try:
from crews.code_review_crew.code_review_crew import CodeReviewCrew
from crews.security_review_crew.security_review_crew import SecurityReviewCrew
from crews.infra_review_crew.infra_review_crew import InfraReviewCrew
from crews.summariser_crew.summariser_crew import SummariserCrew
print("✓ Crew modules imported successfully")
# Try to instantiate (might fail due to missing dependencies, but that's ok for import test)
code_crew = CodeReviewCrew()
security_crew = SecurityReviewCrew()
infra_crew = InfraReviewCrew()
summariser_crew = SummariserCrew()
print("✓ Crew instantiation works")
return True
except Exception as e:
print(f"⚠ Crew test warning (may be expected if dependencies missing): {e}")
# This might fail due to missing crewai or other dependencies, which is ok for this test
return True # Don't fail the overall test for this
def test_api_imports():
"""Test that API modules can be imported."""
try:
from src.pr_reviewer.main import app
from src.pr_reviewer.flow import CodeReviewFlow
print("✓ API modules imported successfully")
return True
except Exception as e:
print(f"✗ API import failed: {e}")
return False
if __name__ == "__main__":
print("Running simple component tests...\n")
success = True
success &= test_imports()
success &= test_crew_imports()
success &= test_api_imports()
if success:
print("\n🎉 All tests passed! The basic components are working.")
print("\nTo test with Docker:")
print("1. Fix any Docker build issues if needed")
print("2. Run: ./start.sh")
print("3. Or manually: docker build -t pr-reviewer . && docker run -p 8000:8000 pr-reviewer")
else:
print("\n❌ Some tests failed. Please check the errors above.")
sys.exit(1)

View File

@ -6,7 +6,6 @@ from .context import resolve_context
import os import os
from datetime import datetime from datetime import datetime
# Import the crews
from crews.code_review_crew.code_review_crew import CodeReviewCrew from crews.code_review_crew.code_review_crew import CodeReviewCrew
from crews.security_review_crew.security_review_crew import SecurityReviewCrew from crews.security_review_crew.security_review_crew import SecurityReviewCrew
from crews.infra_review_crew.infra_review_crew import InfraReviewCrew from crews.infra_review_crew.infra_review_crew import InfraReviewCrew
@ -16,51 +15,27 @@ from crews.summariser_crew.summariser_crew import SummariserCrew
class CodeReviewFlow(Flow[PRReviewState]): class CodeReviewFlow(Flow[PRReviewState]):
@start() @start()
def receive_pr(self, inputs): def receive_pr(self):
"""Initialize the PR review state with input data.""" print(f"Received PR review request for PR #{self.state.pr_id}")
print(f"Received PR review request for PR #{inputs.get('pr_id')}")
# Initialize the state if isinstance(self.state.files, list) and self.state.files and isinstance(self.state.files[0], dict):
self.state.pr_id = inputs.get("pr_id", "")
self.state.pr_title = inputs.get("pr_title", "")
self.state.pr_description = inputs.get("pr_description", "")
self.state.pr_url = inputs.get("pr_url", "")
self.state.repo_name = inputs.get("repo_name", "")
self.state.repo_url = inputs.get("repo_url", "")
self.state.branch = inputs.get("branch", "")
self.state.base_branch = inputs.get("base_branch", "")
# Convert files from list of dicts to list of FileInfo objects if needed
files_input = inputs.get("files", [])
if files_input and isinstance(files_input[0], dict):
# Convert dicts to FileInfo objects
from .state import FileInfo from .state import FileInfo
self.state.files = [FileInfo(**file_dict) for file_dict in files_input] self.state.files = [FileInfo(**file_dict) for file_dict in self.state.files]
else:
self.state.files = files_input
# Handle context_overrides context_input = self.state.context_overrides
context_overrides_input = inputs.get("context_overrides") if isinstance(context_input, dict):
if context_overrides_input and isinstance(context_overrides_input, dict):
from .state import ContextOverrides from .state import ContextOverrides
self.state.context_overrides = ContextOverrides(**context_overrides_input) self.state.context_overrides = ContextOverrides(**context_input)
else:
self.state.context_overrides = context_overrides_input
self.state.started_at = datetime.now() self.state.started_at = datetime.now()
# Resolve context
self.state.resolved_context = resolve_context(self.state) self.state.resolved_context = resolve_context(self.state)
return self.state return self.state
@listen(receive_pr) @listen(receive_pr)
def run_code_review(self): def run_code_review(self):
"""Run the code review crew."""
print("Starting code review...") print("Starting code review...")
# Instantiate and run the code review crew
code_crew = CodeReviewCrew() code_crew = CodeReviewCrew()
# The crew's kickoff method expects inputs matching the task template variables
inputs = { inputs = {
"pr_title": self.state.pr_title, "pr_title": self.state.pr_title,
"pr_description": self.state.pr_description, "pr_description": self.state.pr_description,
@ -70,15 +45,11 @@ class CodeReviewFlow(Flow[PRReviewState]):
result = code_crew.crew().kickoff(inputs=inputs) result = code_crew.crew().kickoff(inputs=inputs)
self.state.code_review_results = str(result) self.state.code_review_results = str(result)
print("Code review completed.") print("Code review completed.")
return self.state return self.state
@listen(receive_pr) @listen(receive_pr)
def run_security_review(self): def run_security_review(self):
"""Run the security review crew."""
print("Starting security review...") print("Starting security review...")
# Instantiate and run the security review crew
security_crew = SecurityReviewCrew() security_crew = SecurityReviewCrew()
inputs = { inputs = {
"pr_title": self.state.pr_title, "pr_title": self.state.pr_title,
@ -89,15 +60,11 @@ class CodeReviewFlow(Flow[PRReviewState]):
result = security_crew.crew().kickoff(inputs=inputs) result = security_crew.crew().kickoff(inputs=inputs)
self.state.security_review_results = str(result) self.state.security_review_results = str(result)
print("Security review completed.") print("Security review completed.")
return self.state return self.state
@listen(receive_pr) @listen(receive_pr)
def run_infra_review(self): def run_infra_review(self):
"""Run the infrastructure review crew."""
print("Starting infrastructure review...") print("Starting infrastructure review...")
# Instantiate and run the infrastructure review crew
infra_crew = InfraReviewCrew() infra_crew = InfraReviewCrew()
inputs = { inputs = {
"pr_title": self.state.pr_title, "pr_title": self.state.pr_title,
@ -108,15 +75,11 @@ class CodeReviewFlow(Flow[PRReviewState]):
result = infra_crew.crew().kickoff(inputs=inputs) result = infra_crew.crew().kickoff(inputs=inputs)
self.state.infra_review_results = str(result) self.state.infra_review_results = str(result)
print("Infrastructure review completed.") print("Infrastructure review completed.")
return self.state return self.state
@listen(and_(run_code_review, run_security_review, run_infra_review)) @listen(and_(run_code_review, run_security_review, run_infra_review))
def summarise(self): def summarise(self):
"""Summarize the review results."""
print("Starting summarisation...") print("Starting summarisation...")
# Instantiate and run the summariser crew
summariser_crew = SummariserCrew() summariser_crew = SummariserCrew()
inputs = { inputs = {
"code_review_results": self.state.code_review_results, "code_review_results": self.state.code_review_results,
@ -128,15 +91,11 @@ class CodeReviewFlow(Flow[PRReviewState]):
self.state.review_summary = str(result) self.state.review_summary = str(result)
self.state.completed_at = datetime.now() self.state.completed_at = datetime.now()
print("Summarisation completed.") print("Summarisation completed.")
return self.state return self.state
@listen(summarise) @listen(summarise)
def format_response(self): def format_response(self):
"""Format the final response."""
print("Formatting final response...") print("Formatting final response...")
# Return the final state as the response
return { return {
"pr_id": self.state.pr_id, "pr_id": self.state.pr_id,
"pr_title": self.state.pr_title, "pr_title": self.state.pr_title,

View File

@ -1,4 +1,5 @@
import logging import logging
import os
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
@ -54,6 +55,8 @@ async def review_pr(request: Request) -> Dict[str, Any]:
except Exception: except Exception:
raise HTTPException(status_code=422, detail="Invalid JSON payload") raise HTTPException(status_code=422, detail="Invalid JSON payload")
logger.info(f"Payload keys: {payload.keys() if isinstance(payload, dict) else 'Not a dict'}")
# Validate and extract required fields according to the API specification # Validate and extract required fields according to the API specification
# Request schema: # Request schema:
# { # {
@ -94,11 +97,16 @@ async def review_pr(request: Request) -> Dict[str, Any]:
title = payload.get("title") title = payload.get("title")
description = payload.get("description") description = payload.get("description")
logger.info(f"pr_id from payload: {pr_id}")
logger.info(f"title from payload: {title}")
# Extract repo information # Extract repo information
repo_data = payload.get("repo", {}) repo_data = payload.get("repo", {})
repo_name = repo_data.get("name") repo_name = repo_data.get("name")
repo_url = repo_data.get("url") repo_url = repo_data.get("url")
logger.info(f"repo_name from payload: {repo_name}")
# Extract source information # Extract source information
source_data = payload.get("source", {}) source_data = payload.get("source", {})
source_branch = source_data.get("branch") source_branch = source_data.get("branch")
@ -158,6 +166,22 @@ async def review_pr(request: Request) -> Dict[str, Any]:
infra_review=context_data.get("infra_review") infra_review=context_data.get("infra_review")
) )
# Debug: Print the inputs being passed to the flow
flow_inputs = {
"pr_id": pr_id,
"pr_title": title,
"pr_description": description,
"pr_url": f"{repo_url}/pull/{pr_id}", # Construct PR URL
"repo_name": repo_name,
"repo_url": repo_url,
"branch": source_branch, # Using source branch as the active branch
"base_branch": target_branch, # Using target branch as base
"files": [file.dict() for file in files], # Convert to dict for flow
"context_overrides": context_overrides.dict() if context_overrides else None
}
logger.info(f"Flow inputs: {flow_inputs}")
logger.info(f"Flow inputs keys: {flow_inputs.keys()}")
# Initialize and run the flow with timeout # Initialize and run the flow with timeout
flow = CodeReviewFlow() flow = CodeReviewFlow()
@ -169,18 +193,7 @@ async def review_pr(request: Request) -> Dict[str, Any]:
flow_result = await asyncio.wait_for( flow_result = await asyncio.wait_for(
loop.run_in_executor( loop.run_in_executor(
pool, pool,
lambda: flow.kickoff(inputs={ lambda: flow.kickoff(inputs=flow_inputs)
"pr_id": pr_id,
"pr_title": title,
"pr_description": description,
"pr_url": f"{repo_url}/pull/{pr_id}", # Construct PR URL
"repo_name": repo_name,
"repo_url": repo_url,
"branch": source_branch, # Using source branch as the active branch
"base_branch": target_branch, # Using target branch as base
"files": [file.dict() for file in files], # Convert to dict for flow
"context_overrides": context_overrides.dict() if context_overrides else None
})
), ),
timeout=TOTAL_FLOW_TIMEOUT timeout=TOTAL_FLOW_TIMEOUT
) )

View File

@ -23,14 +23,14 @@ class ContextOverrides(BaseModel):
class PRReviewState(BaseModel): class PRReviewState(BaseModel):
"""State of the PR review process.""" """State of the PR review process."""
# Input fields # Input fields
pr_id: str pr_id: str = ""
pr_title: str pr_title: str = ""
pr_description: Optional[str] = None pr_description: Optional[str] = None
pr_url: Optional[str] = None pr_url: Optional[str] = None
repo_name: str repo_name: str = ""
repo_url: str repo_url: str = ""
branch: str branch: str = ""
base_branch: str base_branch: str = ""
files: List[FileInfo] = Field(default_factory=list) files: List[FileInfo] = Field(default_factory=list)
context_overrides: Optional[ContextOverrides] = None context_overrides: Optional[ContextOverrides] = None
# Internal fields # Internal fields

View File

@ -1,12 +0,0 @@
#!/bin/bash
# Simple start script to build Docker image and run tests
set -e # Exit on any error
echo "Building Docker image..."
docker build -t pr-reviewer-test:latest .
echo "Running tests..."
python test_docker.py
echo "All tests completed!"

View File

@ -1,142 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify the Dockerized PR Reviewer application works correctly.
This script builds the Docker image, runs it, and tests the API endpoints.
"""
import time
import requests
import docker
import json
import sys
from typing import Dict, Any
def test_dockerized_app():
"""Test the Dockerized PR Reviewer application."""
client = docker.from_env()
try:
# Build the Docker image
print("Building Docker image...")
image, build_logs = client.images.build(
path=".",
tag="pr-reviewer-test:latest",
rm=True,
forcerm=True
)
print("Docker image built successfully.")
# Run the container
print("Starting container...")
container = client.containers.run(
image="pr-reviewer-test:latest",
detach=True,
ports={'8000/tcp': 8000},
environment={
"LLM_MODEL": "test-model",
"LLM_BASE_URL": "http://localhost:11434", # Using Ollama as example
"LLM_API_KEY": "ollama", # Ollama doesn't need a real key
"LLM_PROVIDER": "ollama"
}
)
print(f"Container started with ID: {container.id}")
# Wait for the container to be ready
print("Waiting for container to be ready...")
max_wait = 30 # seconds
start_time = time.time()
while time.time() - start_time < max_wait:
try:
response = requests.get("http://localhost:8000/api/v1/health", timeout=5)
if response.status_code == 200:
print("Container is ready!")
break
except requests.exceptions.ConnectionError:
print("Waiting for container to start...")
time.sleep(2)
else:
raise TimeoutError("Container did not become ready within the timeout period")
# Test the health endpoint
print("Testing health endpoint...")
health_response = requests.get("http://localhost:8000/api/v1/health")
assert health_response.status_code == 200, f"Health check failed: {health_response.status_code}"
health_data = health_response.json()
assert health_data["status"] == "healthy", f"Unexpected health status: {health_data['status']}"
print("Health endpoint test passed.")
# Test the review endpoint with minimal valid data
print("Testing review endpoint...")
test_payload = {
"pr_id": "123",
"title": "Test PR",
"description": "This is a test PR",
"repo": {
"name": "test-repo",
"url": "https://github.com/test/test-repo"
},
"source": {
"branch": "feature/test",
"commit": "abc123"
},
"target": {
"branch": "main",
"commit": "def456"
},
"files": [
{
"path": "src/main.py",
"content": "print('Hello World')",
"status": "modified",
"additions": 1,
"deletions": 0
}
],
"context": {
"code_review": "Follow basic coding standards",
"security_review": "Check for obvious security issues",
"infra_review": "Ensure basic infrastructure practices"
}
}
review_response = requests.post(
"http://localhost:8000/api/v1/review",
json=test_payload,
timeout=30 # Longer timeout for the review process
)
# We expect this to either succeed (200) or fail with a 500 due to LLM issues
# Since we're not actually connecting to a real LLM, we expect a 500
print(f"Review endpoint responded with status: {review_response.status_code}")
if review_response.status_code == 200:
review_data = review_response.json()
print("Review endpoint test passed.")
print(f"Review ID: {review_data.get('review_id')}")
print(f"Status: {review_data.get('status')}")
else:
print(f"Review endpoint returned error status {review_response.status_code} (expected due to lack of real LLM)")
print(f"Response: {review_response.text}")
# Clean up
print("Cleaning up...")
container.stop()
container.remove()
client.images.remove(image="pr-reviewer-test:latest", force=True)
print("Test completed successfully.")
except Exception as e:
print(f"Test failed with error: {e}")
# Try to clean up if possible
try:
if 'container' in locals():
container.stop()
container.remove()
if 'image' in locals():
client.images.remove(image="pr-reviewer-test:latest", force=True)
except:
pass
raise
if __name__ == "__main__":
test_dockerized_app()

View File

@ -0,0 +1,31 @@
import pytest
import requests
import os
from dotenv import load_dotenv
load_dotenv()
BASE_URL = "http://localhost:8000/api/v1"
def test_health_endpoint():
"""Test the health check endpoint."""
response = requests.get(f"{BASE_URL}/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy", "service": "pr-reviewer"}
def test_trigger_review_invalid_pr():
"""Test triggering a review with an invalid PR payload."""
payload = {"pr_id": "invalid-id"}
response = requests.post(f"{BASE_URL}/review", json=payload)
# Depending on implementation, this might be 400 or 202 (async)
assert response.status_code in [200, 400, 422]
def test_trigger_review_missing_payload():
"""Test triggering a review with no payload."""
response = requests.post(f"{BASE_URL}/review", json={})
assert response.status_code == 422 # FastAPI default for missing required body fields
def test_get_status_nonexistent():
"""Test getting status for a non-existent review."""
response = requests.get(f"{BASE_URL}/status/non-existent-id")
assert response.status_code == 404

View File

@ -0,0 +1,75 @@
import pytest
import requests
import json
from dotenv import load_dotenv
load_dotenv()
BASE_URL = "http://localhost:8000/api/v1"
# Mock PR data for testing - comprehensive payload
MOCK_PR_DATA = {
"pr_id": "123",
"title": "Fix authentication vulnerability",
"description": "This PR addresses a critical authentication bypass vulnerability",
"repo": {
"name": "secure-app",
"url": "https://github.com/example/secure-app"
},
"source": {
"branch": "fix-auth-bypass",
"commit": "a1b2c3d4e5f6"
},
"target": {
"branch": "main",
"commit": "f6e5d4c3b2a1"
},
"files": [
{
"path": "src/auth.py",
"content": "def authenticate_user(username, password):\n # Vulnerable authentication implementation\n if username == 'admin' and password == 'password123':\n return True\n return False",
"status": "modified",
"additions": 5,
"deletions": 3,
"patch": "@@ -1,5 +1,5 @@\n def authenticate_user(username, password):\n- # Simple authentication\n- if username == 'admin' and password == 'password123':\n+ # Fixed authentication with proper validation\n+ if validate_credentials(username, password):\n return True\n return False"
}
],
"context": {
"code_review": "Focus on security best practices and authentication logic",
"security_review": "Identify potential vulnerabilities in authentication flow",
"infra_review": "Verify secure deployment configurations"
}
}
def test_full_review_workflow():
"""Test the full PR review workflow with mock data."""
# Trigger a review
response = requests.post(f"{BASE_URL}/review", json=MOCK_PR_DATA)
# Print response for debugging
print(f"Status Code: {response.status_code}")
print(f"Response: {response.text}")
# Validate response
assert response.status_code == 200
data = response.json()
assert "review_id" in data
assert data["status"] in ["completed", "failed"] # Allow either status
assert "results" in data
assert "metadata" in data
# Validate results structure
results = data["results"]
assert "code_review" in results or "code_review" in str(results) # At least present in the response
assert "security_review" in results or "security_review" in str(results)
assert "infra_review" in results or "infra_review" in str(results)
assert "summary" in results or "summary" in str(results)
# Validate metadata
metadata = data["metadata"]
assert "processing_time_seconds" in metadata
assert metadata["pr_id"] == MOCK_PR_DATA["pr_id"]
assert metadata["repo"]["name"] == MOCK_PR_DATA["repo"]["name"]
print("Full review workflow test passed!")