Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6077393
feat: add GitHub App manifest for AI code review (#862)
waterWang Aug 8, 2026
f55dca8
feat: add integrations/github-app/requirements.txt for AI code review…
waterWang Aug 8, 2026
a1e335b
feat: add integrations/github-app/__init__.py for AI code review GitH…
waterWang Aug 8, 2026
69289c6
feat: add integrations/github-app/config.py for AI code review GitHub…
waterWang Aug 8, 2026
54beb12
feat: add integrations/github-app/models.py for AI code review GitHub…
waterWang Aug 8, 2026
c3d9aaa
feat: add integrations/github-app/github_client.py for AI code review…
waterWang Aug 8, 2026
9a10f69
feat: add integrations/github-app/analyzers/__init__.py for AI code r…
waterWang Aug 8, 2026
5f9c1c3
feat: add integrations/github-app/analyzers/security.py for AI code r…
waterWang Aug 8, 2026
6629461
feat: add integrations/github-app/analyzers/performance.py for AI cod…
waterWang Aug 8, 2026
e4e8391
feat: add integrations/github-app/analyzers/best_practices.py for AI …
waterWang Aug 8, 2026
7b73963
feat: add integrations/github-app/reviewers/__init__.py for AI code r…
waterWang Aug 8, 2026
e3f5194
feat: add integrations/github-app/reviewers/base.py for AI code revie…
waterWang Aug 8, 2026
ac6234d
feat: add integrations/github-app/reviewers/claude.py for AI code rev…
waterWang Aug 8, 2026
d779443
feat: add integrations/github-app/reviewers/openai.py for AI code rev…
waterWang Aug 8, 2026
a0eeb17
feat: add integrations/github-app/reviewers/gemini.py for AI code rev…
waterWang Aug 8, 2026
fdcff32
feat: add integrations/github-app/reviewers/orchestrator.py for AI co…
waterWang Aug 8, 2026
ebab6f8
feat: add integrations/github-app/app.py for AI code review GitHub Ap…
waterWang Aug 8, 2026
1999a75
feat: add integrations/github-app/README.md for AI code review GitHub…
waterWang Aug 8, 2026
c6e64fa
feat: add integrations/github-app/tests/__init__.py for AI code revie…
waterWang Aug 8, 2026
5e5c384
feat: add integrations/github-app/tests/test_app.py for AI code revie…
waterWang Aug 8, 2026
96d7b32
feat: add integrations/github-app/ai-code-review.example.yml for AI c…
waterWang Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions integrations/github-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
SolFoundry AI Code Review GitHub App
=====================================

An installable GitHub App that provides automated multi-LLM code reviews
on every pull request, with security checks, performance analysis, and
best practices verification.

## Features

- **Multi-LLM Reviews**: Claude, OpenAI/Codex, and Gemini review every PR
- **3 Review Modes**: Quick (1 model), Standard (3 models), Thorough (5 models)
- **Security Checks**: 7 patterns (hardcoded secrets, SQL injection, eval, etc.)
- **Performance Analysis**: N+1 queries, sync-in-async, large lists, string concat
- **Best Practices**: Type hints, console.log, TODO/FIXME, magic numbers, bare except
- **Configurable**: Per-repository `.github/ai-code-review.yml` configuration
- **Check Runs**: Results appear as check runs on every PR
- **Inline Comments**: Findings are posted as inline review comments

## Quick Start

### 1. Install the App

Click the "Install" button on the GitHub App page and select which repositories
to grant access to.

### 2. (Optional) Configuration

Create `.github/ai-code-review.yml` in your repository:

```yaml
mode: standard # quick | standard | thorough
strictness: balanced # lenient | balanced | strict
comment_style: inline # inline | summary | both
auto_approve_threshold: 8.0
block_threshold: 4.0
languages:
- python
- javascript
- typescript
- go
- rust
```

### 3. Open a PR

The app automatically reviews every pull request and posts:
- **Check Run**: Score out of 10 with model-specific scores
- **Review Comments**: Inline comments on issues found
- **Summary Comment**: Overview of all findings

## Scoring

| Score | Meaning |
|-------|---------|
| 8.0-10 | Good — no critical/high issues |
| 6.0-7.9 | Fair — minor issues to address |
| 4.0-5.9 | Needs work — significant issues |
| 0.0-3.9 | Poor — major problems |

## Architecture

```
integrations/github-app/
├── manifest.json # GitHub App manifest
├── app.py # FastAPI webhook handler
├── config.py # Review configuration models
├── github_client.py # GitHub API client
├── models.py # Shared data models
├── requirements.txt # Python dependencies
├── README.md # This file
├── analyzers/
│ ├── security.py # Security pattern detection
│ ├── performance.py # Performance analysis
│ └── best_practices.py # Best practices checker
├── reviewers/
│ ├── base.py # Abstract reviewer interface
│ ├── claude.py # Claude reviewer
│ ├── openai.py # OpenAI/Codex reviewer
│ ├── gemini.py # Gemini reviewer
│ └── orchestrator.py # Multi-model orchestration
└── tests/
├── test_app.py # Unit tests
└── ...
```

## Environment Variables

| Variable | Description |
|----------|-------------|
| `GITHUB_APP_ID` | GitHub App ID |
| `GITHUB_PRIVATE_KEY` | App private key (PEM) |
| `GITHUB_WEBHOOK_SECRET` | Webhook secret |
| `ANTHROPIC_API_KEY` | Claude API key |
| `OPENAI_API_KEY` | OpenAI/Codex API key |
| `GEMINI_API_KEY` | Google Gemini API key |

## License

SolFoundry
"""
Empty file.
20 changes: 20 additions & 0 deletions integrations/github-app/ai-code-review.example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Example configuration for the SolFoundry AI Code Review GitHub App
# Copy to .github/ai-code-review.yml in your repository to customize.

mode: standard # quick | standard | thorough
strictness: balanced # lenient | balanced | strict
comment_style: inline # inline | summary | both

# Scoring thresholds (0-10)
auto_approve_threshold: 8.0
block_threshold: 4.0

# Languages to review
languages:
- python
- javascript
- typescript
- go
- rust
- java
- solidity
7 changes: 7 additions & 0 deletions integrations/github-app/analyzers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Static analyzers for security, performance, and best practices."""

from .security import SecurityAnalyzer
from .performance import PerformanceAnalyzer
from .best_practices import BestPracticesAnalyzer

__all__ = ["SecurityAnalyzer", "PerformanceAnalyzer", "BestPracticesAnalyzer"]
56 changes: 56 additions & 0 deletions integrations/github-app/analyzers/best_practices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Best practices and code-quality checks."""
import re
from ..models import DiffFile, Finding, Severity


class BestPracticesAnalyzer:
"""Detects common best-practice violations."""

def analyze(self, files: list[DiffFile]) -> list[Finding]:
findings: list[Finding] = []
for df in files:
if not df.patch:
continue
ext = df.extension
if ext == "py" and self._has_missing_type_hints(df.patch):
findings.append(Finding(
severity=Severity.LOW,
category="best-practice:type hints",
message="Function definitions missing type annotations.",
file=df.filename,
))
if ext in ("ts", "tsx", "js", "jsx") and re.search(r"console\.(log|debug)\s*\(", df.patch):
findings.append(Finding(
severity=Severity.LOW,
category="best-practice:console.log",
message="console.log left in code. Remove or replace with a logger.",
file=df.filename,
))
if re.search(r"(?i)\b(TODO|FIXME|HACK|XXX):?\s", df.patch):
findings.append(Finding(
severity=Severity.INFO,
category="best-practice:TODO/FIXME",
message="TODO/FIXME marker found. Consider resolving before merge.",
file=df.filename,
))
if re.search(r"[\s=(](-?[0-9]{3,})(?!\s*[%\\*/+ -])\b", df.patch) and ext in ("ts", "tsx", "py", "go", "java", "rs"):
findings.append(Finding(
severity=Severity.INFO,
category="best-practice:magic number",
message="Magic number detected. Extract to a named constant.",
file=df.filename,
))
if ext == "py" and re.search(r"except\s*:", df.patch):
findings.append(Finding(
severity=Severity.LOW,
category="best-practice:bare except",
message="Bare except clause. Catch specific exceptions.",
file=df.filename,
))
return findings

@staticmethod
def _has_missing_type_hints(patch: str) -> bool:
return bool(re.search(r"def\s+\w+\s*\([^)]*\b\w+\s*(,|\))", patch)) and not bool(
re.search(r"def\s+\w+\s*\([^)]*:\s*[\w\[\]]+", patch)
)
72 changes: 72 additions & 0 deletions integrations/github-app/analyzers/performance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Performance anti-pattern detection."""
import re
from typing import Optional
from ..models import DiffFile, Finding, Severity


class PerformanceAnalyzer:
"""Detects common performance anti-patterns."""

def analyze(self, files: list[DiffFile]) -> list[Finding]:
findings: list[Finding] = []
for df in files:
if not df.patch:
continue
ext = df.extension
if self._has_n_plus_one(df.patch):
findings.append(Finding(
severity=Severity.MEDIUM,
category="performance:N+1 query",
message="Possible N+1 query pattern. Use selectinload/joinedload or batch fetching.",
file=df.filename,
line=self._line_of(df.patch),
))
if self._has_sync_in_async(df.patch):
findings.append(Finding(
severity=Severity.MEDIUM,
category="performance:blocking call in async",
message="Blocking call inside an async function. Use async clients or offload to a thread.",
file=df.filename,
line=self._line_of(df.patch),
))
if re.search(r"\[\s*[^\]]+\s+for\s+[^\]]+\s+in\s+range\([1-9][0-9]{4,}\)", df.patch):
findings.append(Finding(
severity=Severity.LOW,
category="performance:large list comprehension",
message="Building a large list in memory. Consider a generator expression.",
file=df.filename,
))
if re.search(r"for .*:\n\s+.*\+=\s*f?[\"']", df.patch):
findings.append(Finding(
severity=Severity.LOW,
category="performance:string concat in loop",
message="String concatenation in a loop is O(n^2). Use str.join or a list buffer.",
file=df.filename,
))
return findings

@staticmethod
def _has_n_plus_one(patch: str) -> bool:
return bool(
re.search(r"\bfor\b.*\bin\b.*\b(query|select|filter|get)\.(all|one|first)\b", patch)
or re.search(r"\bfor\b.*\bin\b[^\n]*\n\s*\w+\.(query|objects)\b", patch)
)

@staticmethod
def _has_sync_in_async(patch: str) -> bool:
return bool(
re.search(r"async\s+def\s+\w+[^\n]*:\n(?:\s+[^\n]*\n)*?\s+(?:requests|httpx\.Client|time\.sleep|open\(|subprocess\b)\.", patch)
or re.search(r"async\s+def[^\n]*\n(?:[^\n]*\n){0,10}?\s*requests\.(get|post|put|delete)\b", patch)
)

@staticmethod
def _line_of(patch: str) -> Optional[int]:
line = 1
for piece in patch.splitlines():
if piece.startswith("@@"):
m = re.search(r"\+([0-9]+)", piece)
if m:
line = int(m.group(1))
elif piece and not piece.startswith(("-", "\\")):
line += 1
return line
79 changes: 79 additions & 0 deletions integrations/github-app/analyzers/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Security pattern detection for code review."""
import re
from typing import Optional
from ..models import DiffFile, Finding, Severity


class SecurityAnalyzer:
"""Detects security anti-patterns in code diffs."""

PATTERNS = [
("hardcoded_secret", Severity.CRITICAL,
r"(?i)(api[_-]?key|secret|password|passwd|token|private[_-]?key)\s*[=:]\s*[\"']([A-Za-z0-9_\-]{12,})[\"']",
"Potential hardcoded credential detected. Use environment variables or a secrets manager."),
("sql_injection", Severity.CRITICAL,
r"(?i)(execute|exec|query|raw)\s*\(\s*f[\"']|SELECT.*\+.*WHERE|\bconcat\(.*(SELECT|WHERE)",
"Possible SQL injection - string interpolation in SQL query. Use parameterized queries."),
("eval_usage", Severity.HIGH,
r"\beval\s*\(|\bexec\s*\(\s*f[\"']|\bexec\s*\(\s*str\(|Function\s*\(\s*[\"']",
"Use of eval()/exec() with dynamic input. Can lead to code injection."),
("unsafe_deserialize", Severity.HIGH,
r"pickle\.loads?\s*\(|yaml\.load\s*\(|json\.loads?\s*\(\s*request|node-serialize|unserialize\(",
"Unsafe deserialization detected. Use safe loaders (e.g., yaml.safe_load)."),
("http_url", Severity.MEDIUM,
r"https?://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[^\s\"'`]*\bhttp://",
"Plain HTTP URL detected. Use HTTPS to prevent man-in-the-middle attacks."),
("debug_enabled", Severity.LOW,
r"(?i)(debug\s*=\s*True|DEBUG\s*=\s*True|app\.debug\s*=\s*True|NODE_ENV\s*=\s*['\"]development)",
"Debug mode enabled. Ensure this is disabled in production."),
("command_injection", Severity.HIGH,
r"os\.system\s*\(\s*f[\"']|subprocess\.(run|call|Popen)\s*\(\s*f[\"']|child_process.*exec\s*\(\s*f[\"']",
"Command execution with f-string interpolation. Use shell=False and argument lists."),
]

def analyze(self, files: list[DiffFile]) -> list[Finding]:
findings: list[Finding] = []
for df in files:
if not df.patch:
continue
for name, severity, pattern, message in self.PATTERNS:
for match in re.finditer(pattern, df.patch):
line = self._patch_line_from_match(df.patch, match.start())
findings.append(Finding(
severity=severity,
category="security",
message=message,
file=df.filename,
line=line,
suggestion=self._suggestion(name),
))
return findings

@staticmethod
def _patch_line_from_match(patch: str, pos: int) -> Optional[int]:
line = 1
current = 0
for piece in patch.splitlines():
if current >= pos:
break
if piece.startswith("@@"):
m = re.search(r"\+([0-9]+)(?:,[0-9]+)?", piece)
if m:
line = int(m.group(1))
elif piece and not piece.startswith(("-", "\\")):
line += 1
current += len(piece) + 1
return line

@staticmethod
def _suggestion(name: str) -> str:
hints = {
"hardcoded_secret": "Move secrets to environment variables or a vault.",
"sql_injection": "Use parameterized queries / prepared statements.",
"eval_usage": "Replace eval/exec with a safe parser or constrained AST evaluation.",
"unsafe_deserialize": "Use yaml.safe_load or a schema-validated deserializer.",
"command_injection": "Pass arguments as a list to subprocess and keep shell=False.",
"http_url": "Use https:// URLs and HSTS headers.",
"debug_enabled": "Gate debug mode behind an env flag and disable in production.",
}
return hints.get(name, "")
Loading
Loading