AI-Powered Code Review Automation

GitLab CI/CD + Ollama
AI Code Reviewer

ให้ AI รีวิวโค้ดอัตโนมัติ ประหยัด เป็นส่วนตัว และทรงพลัง
ด้วย Local LLM ที่รันบน Infrastructure ของคุณเอง

เนื้อหาในบทความนี้

สถานะบทความ

อัปเดตล่าสุด: กุมภาพันธ์ 2026 | ระดับความยาก: ระดับกลาง (Intermediate) | เวลาอ่าน: 45 นาที

ทดสอบกับ Ollama 0.5.x, GitLab 17.x, Python 3.11+ | รองรับ CodeLlama, DeepSeek-Coder, Qwen2.5-Coder

1 บทนำ: ทำไมต้องใช้ AI รีวิวโค้ด?

การรีวิวโค้ด (Code Review) เป็นหนึ่งในกระบวนการที่สำคัญที่สุดในการพัฒนาซอฟต์แวร์ แต่มักจะเป็นงานที่ใช้เวลานานและอาจเกิดความผิดพลาดจากมนุษย์ได้ AI Code Reviewer ช่วยแก้ปัญหานี้โดย:

ประโยชน์ของ AI Code Review

  • รวดเร็ว: รีวิวโค้ดได้ภายในไม่กี่วินาที ไม่ต้องรอ human reviewer
  • 24/7 Available: พร้อมทำงานตลอดเวลา ไม่มีวันหยุด
  • Consistent: ตรวจสอบด้วยเกณฑ์เดียวกันทุกครั้ง
  • Knowledge Base: มีความรู้จาก codebase ขนาดใหญ่ทั่วโลก
  • Security Focus: ตรวจจับช่องโหว่ความปลอดภัยได้ดี

ข้อจำกัดที่ต้องรู้

  • Hallucination: AI อาจให้ข้อมูลผิดหรือแนะนำที่ไม่เหมาะสม
  • Context Limit: ไม่เข้าใจ business context เต็มที่
  • ไม่แทนมนุษย์: ควรใช้เป็น assistant ไม่ใช่ replacement
  • Resource Intensive: ต้องการ GPU หรือ RAM สูงสำหรับ model ใหญ่
  • Latency: อาจช้าถ้า codebase ใหญ่หรือ server ไม่แรง

ทำไมต้องใช้ Local LLM (Ollama)?

Privacy & Security
โค้ดไม่ออกจาก network ของคุณ
Cost Effective
ไม่มีค่า API ต่อเนื่อง จ่ายครั้งเดียว
Offline Capable
ทำงานได้แม้ไม่มี internet
80%
ลดเวลา Code Review
60%
ตรวจจับ Bug ก่อน Production
$0
ค่า API ต่อเดือน
100%
Data Privacy

2 Architecture Overview

ระบบ AI Code Review ประกอบด้วยหลาย components ที่ทำงานร่วมกัน ด้านล่างคือ Architecture และ Flow การทำงานของระบบ

System Architecture

👨‍💻 Developer Push Code 🦊 GitLab Repository CI/CD Pipeline 🏃 Runner Execute Jobs 🦙 Ollama Local LLM Server Models: • CodeLlama • DeepSeek-Coder • Qwen2.5-Coder 📊 Review Report Pass/Fail Decision Legend: Input GitLab AI Engine Output

Workflow Flow

📤 PUSH Developer push code TRIGGER GitLab CI pipeline start 🔍 ANALYZE AI Review via Ollama ⚖️ DECIDE Score vs Threshold RESULT Pass/Fail & Report Merge OK Block MR

GitLab CI/CD

จัดการ pipeline และ trigger AI review เมื่อมี code push หรือ MR

Ollama Server

รัน Local LLM สำหรับ analyze code และให้ review comments

Review Script

Python script สำหรับเรียก Ollama API และประมวลผล

Report Generator

สร้างรายงานและตัดสินใจ pass/fail ตาม threshold

3 สิ่งที่ต้องเตรียม (Prerequisites)

ก่อนเริ่มต้น ตรวจสอบว่าคุณมีสิ่งต่อไปนี้พร้อมแล้ว:

GitLab Instance

Self-hosted หรือ GitLab.com

GitLab 15.0+ (แนะนำ 17.x)
CI/CD enabled
Runner ที่สามารถเข้าถึง Ollama server
🦙

Ollama Server

สำหรับรัน LLM

Linux server (Ubuntu 22.04+ แนะนำ)
RAM: 16GB+ (32GB+ สำหรับ model ใหญ่)
GPU: NVIDIA 8GB+ (optional แต่แนะนำ)

Docker (Optional)

สำหรับ containerized deployment

Docker 20.10+
Docker Compose v2
nvidia-container-toolkit (ถ้าใช้ GPU)

GitLab Runner

สำหรับรัน CI jobs

Runner 16.0+
Docker executor (แนะนำ)
Network access to Ollama server

ความต้องการ Hardware ตาม Model Size

Model Parameters Minimum RAM Recommended RAM GPU VRAM
CodeLlama 7B 7B 8 GB 16 GB 6 GB
DeepSeek-Coder 6.7B 6.7B 8 GB 16 GB 6 GB
Qwen2.5-Coder 7B 7B 8 GB 16 GB 6 GB
CodeLlama 13B 13B 16 GB 32 GB 12 GB
DeepSeek-Coder 33B 33B 32 GB 64 GB 24 GB

4 ขั้นตอนที่ 1: ติดตั้ง Ollama

เริ่มต้นด้วยการติดตั้ง Ollama บน server ของคุณ Ollama รองรับ Linux, macOS และ Windows

4.1 ติดตั้ง Ollama บน Linux

# ติดตั้ง Ollama ด้วย script อย่างเป็นทางการ
curl -fsSL https://ollama.com/install.sh | sh

# หรือติดตั้งด้วย Docker
docker pull ollama/ollama

# รัน Ollama container (CPU only)
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

# รัน Ollama ด้วย GPU support (NVIDIA)
docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama

หมายเหตุ: Port 11434 เป็น default port ของ Ollama API คุณสามารถเปลี่ยนได้ตามต้องการ

4.2 Pull Model ที่เหมาะสม

เลือก model ที่เหมาะกับงาน code review ของคุณ:

CodeLlama

โดย Meta เฉพาะทาง code

ollama pull codellama:7b
ollama pull codellama:13b
ollama pull codellama:34b

DeepSeek-Coder

โดย DeepSeek เก่งมาก

ollama pull deepseek-coder:6.7b
ollama pull deepseek-coder:33b

Qwen2.5-Coder

โดย Alibaba ราคาถูก

ollama pull qwen2.5-coder:7b
ollama pull qwen2.5-coder:14b

คำแนะนำ: สำหรับเริ่มต้น แนะนำให้ใช้ deepseek-coder:6.7b เพราะให้ผลลัพธ์ดีและใช้ทรัพยากรน้อย

4.3 ทดสอบ API Endpoint

# ทดสอบ Ollama API ด้วย curl
curl http://localhost:11434/api/generate -d '{
  "model": "deepseek-coder:6.7b",
  "prompt": "Review this Python code: def add(a, b): return a + b",
  "stream": false
}'

# ทดสอบ List Models
curl http://localhost:11434/api/tags

# ทดสอบด้วย Python
python3 -c "
import requests
import json

response = requests.post('http://localhost:11434/api/generate', json={
    'model': 'deepseek-coder:6.7b',
    'prompt': 'What is 2+2?',
    'stream': False
})
print(response.json())
"

Expected Response:

{
  "model": "deepseek-coder:6.7b",
  "created_at": "2026-02-16T10:00:00.000Z",
  "response": "4",
  "done": true,
  "context": [...],
  "total_duration": 1500000000,
  "load_duration": 500000000,
  "prompt_eval_count": 10,
  "eval_count": 3
}

กำหนดค่า Network Access (สำคัญ!)

GitLab Runner ต้องสามารถเข้าถึง Ollama API ได้ ดังนั้นต้องกำหนดให้ Ollama รับฟังจากทุก interface:

# กำหนด OLLAMA_HOST environment variable
export OLLAMA_HOST=0.0.0.0:11434

# หรือกำหนดใน systemd service
sudo systemctl edit ollama.service

# เพิ่มบรรทัดนี้:
[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"

# Restart service
sudo systemctl restart ollama

# ตรวจสอบว่ารับฟังจากทุก interface
ss -tlnp | grep 11434
# ควรเห็น: 0.0.0.0:11434

5 ขั้นตอนที่ 2: สร้าง GitLab CI/CD Pipeline

สร้าง pipeline สำหรับ AI code review ที่ครอบคลุม Code Quality, Security และ Best Practices

Pipeline Structure

Stage 1: Code Quality
Stage 2: Security Scan
Stage 3: Best Practices
Stage 4: Final Decision

5.1 ไฟล์ .gitlab-ci.yml

# .gitlab-ci.yml - GitLab CI/CD Pipeline for AI Code Review
# ตั้งชื่อ pipeline และกำหนด variables
variables:
  OLLAMA_HOST: "http://your-ollama-server:11434"
  OLLAMA_MODEL: "deepseek-coder:6.7b"
  REVIEW_THRESHOLD: "70"
  # Threshold สำหรับ pass/fail (0-100)
  
# กำหนด stages
stages:
  - quality
  - security
  - best_practices
  - decision

# กำหนด default settings
default:
  image: python:3.11-slim
  before_script:
    - pip install --quiet requests python-gitlab
  retry:
    max: 2
    when:
      - script_failure

# ====================
# STAGE 1: CODE QUALITY
# ====================

code_quality_review:
  stage: quality
  script:
    - echo "🔍 Running AI Code Quality Review..."
    - python scripts/ai-review.py
      --type quality
      --ollama-host $OLLAMA_HOST
      --model $OLLAMA_MODEL
      --output quality_report.json
  artifacts:
    paths:
      - quality_report.json
    expire_in: 1 week
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  tags:
    - docker

# ====================
# STAGE 2: SECURITY SCAN
# ====================

security_review:
  stage: security
  script:
    - echo "🔒 Running AI Security Review..."
    - python scripts/ai-review.py
      --type security
      --ollama-host $OLLAMA_HOST
      --model $OLLAMA_MODEL
      --output security_report.json
  artifacts:
    paths:
      - security_report.json
    expire_in: 1 week
  allow_failure: false
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  tags:
    - docker

# ====================
# STAGE 3: BEST PRACTICES
# ====================

best_practices_review:
  stage: best_practices
  script:
    - echo "📚 Running AI Best Practices Review..."
    - python scripts/ai-review.py
      --type best_practices
      --ollama-host $OLLAMA_HOST
      --model $OLLAMA_MODEL
      --output best_practices_report.json
  artifacts:
    paths:
      - best_practices_report.json
    expire_in: 1 week
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  tags:
    - docker

# ====================
# STAGE 4: FINAL DECISION
# ====================

ai_review_decision:
  stage: decision
  script:
    - echo "⚖️ Making final AI review decision..."
    - python scripts/review-decision.py
      --threshold $REVIEW_THRESHOLD
      --quality-report quality_report.json
      --security-report security_report.json
      --best-practices-report best_practices_report.json
  artifacts:
    paths:
      - final_review_report.json
      - review_summary.md
    expire_in: 1 month
  dependencies:
    - code_quality_review
    - security_review
    - best_practices_review
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  tags:
    - docker

# ====================
# OPTIONAL: POST MR COMMENT
# ====================

post_mr_comment:
  stage: .post
  script:
    - echo "💬 Posting review to Merge Request..."
    - python scripts/post-mr-comment.py
      --project-id $CI_PROJECT_ID
      --mr-iid $CI_MERGE_REQUEST_IID
      --report final_review_report.json
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  allow_failure: true
  tags:
    - docker

กำหนด GitLab CI/CD Variables

ไปที่ Settings → CI/CD → Variables และเพิ่ม variables เหล่านี้:

Variable Value Protected Masked
OLLAMA_HOST http://10.0.0.100:11434
OLLAMA_MODEL deepseek-coder:6.7b - -
REVIEW_THRESHOLD 70 - -
GITLAB_TOKEN your-access-token

6 ขั้นตอนที่ 3: สร้าง Review Script

สร้าง Python script สำหรับเรียก Ollama API และทำการ review โค้ด

6.1 โครงสร้างโฟลเดอร์

your-project/
├── .gitlab-ci.yml
├── scripts/
│   ├── ai-review.py          # Main review script
│   ├── review-decision.py    # Decision making script
│   ├── post-mr-comment.py    # Post comment to MR
│   └── prompts/
│       ├── quality.txt       # Code quality prompt
│       ├── security.txt      # Security review prompt
│       └── best_practices.txt
└── src/
    └── ...

6.2 Prompt Engineering สำหรับ Code Review

Code Quality Prompt

You are an expert code reviewer. 
Analyze the following code for quality issues:

1. Code readability and maintainability
2. Naming conventions
3. Code duplication (DRY principle)
4. Function/method length
5. Complexity (cyclomatic complexity)

CODE TO REVIEW:
{code}

Respond in JSON format:
{
  "score": 0-100,
  "issues": [
    {
      "severity": "high|medium|low",
      "file": "filename",
      "line": 10,
      "message": "description",
      "suggestion": "how to fix"
    }
  ],
  "summary": "overall assessment"
}

Security Review Prompt

You are a security expert. 
Analyze code for security vulnerabilities:

1. SQL injection risks
2. XSS vulnerabilities
3. Authentication issues
4. Sensitive data exposure
5. Insecure dependencies
6. Input validation issues

CODE TO REVIEW:
{code}

Respond in JSON format:
{
  "score": 0-100,
  "vulnerabilities": [
    {
      "severity": "critical|high|medium|low",
      "type": "vulnerability type",
      "cwe": "CWE-XXX",
      "file": "filename",
      "line": 10,
      "description": "description",
      "remediation": "how to fix"
    }
  ],
  "summary": "security assessment"
}

Performance Review Prompt

You are a performance optimization expert.
Analyze code for performance issues:

1. Time complexity (Big O)
2. Memory usage patterns
3. Database query efficiency
4. Caching opportunities
5. Async/await usage
6. Resource leaks

CODE TO REVIEW:
{code}

Respond in JSON format:
{
  "score": 0-100,
  "issues": [
    {
      "severity": "high|medium|low",
      "type": "performance issue type",
      "file": "filename",
      "line": 10,
      "current_complexity": "O(n)",
      "suggested_complexity": "O(log n)",
      "description": "description",
      "optimization": "how to optimize"
    }
  ],
  "summary": "performance assessment"
}

Best Practices Prompt

You are a software best practices expert.
Analyze code for best practices:

1. SOLID principles
2. Design patterns usage
3. Error handling
4. Logging practices
5. Testing coverage hints
6. Documentation quality

CODE TO REVIEW:
{code}

Respond in JSON format:
{
  "score": 0-100,
  "recommendations": [
    {
      "principle": "SOLID/Pattern name",
      "severity": "high|medium|low",
      "file": "filename",
      "line": 10,
      "current": "current implementation",
      "recommended": "recommended approach",
      "reason": "why this matters"
    }
  ],
  "summary": "best practices assessment"
}

การ Parse และแสดงผลลัพธ์

หลังจากได้ response จาก Ollama แล้ว ต้อง parse JSON และ format สำหรับ GitLab:

import json
import re

def parse_ai_response(response_text: str) -> dict:
    """Parse AI response and extract JSON"""
    try:
        # ลอง parse โดยตรง
        return json.loads(response_text)
    except json.JSONDecodeError:
        # หา JSON ใน text
        json_match = re.search(r'\{[\s\S]*\}', response_text)
        if json_match:
            try:
                return json.loads(json_match.group())
            except:
                pass
    
    # Fallback: return default structure
    return {
        "score": 50,
        "issues": [],
        "summary": "Unable to parse AI response"
    }

def format_for_gitlab(report: dict) -> str:
    """Format report for GitLab CI output"""
    output = []
    output.append("## 🤖 AI Code Review Report\n")
    
    # Score
    score = report.get('score', 0)
    emoji = "🟢" if score >= 70 else "🟡" if score >= 50 else "🔴"
    output.append(f"### {emoji} Overall Score: {score}/100\n")
    
    # Issues
    issues = report.get('issues', report.get('vulnerabilities', []))
    if issues:
        output.append("### 📋 Issues Found:\n")
        for issue in issues:
            severity = issue.get('severity', 'unknown')
            sev_emoji = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}.get(severity, "⚪")
            output.append(f"- {sev_emoji} **{severity.upper()}**: {issue.get('message', issue.get('description', 'N/A'))}")
            if issue.get('file'):
                output.append(f"  - File: `{issue['file']}`")
            if issue.get('line'):
                output.append(f"  - Line: {issue['line']}")
            output.append("")
    
    # Summary
    output.append(f"### 📝 Summary\n{report.get('summary', 'No summary available')}")
    
    return "\n".join(output)

7 ตัวอย่างโค้ดที่สมบูรณ์

นี่คือไฟล์โค้ดที่สมบูรณ์พร้อมใช้งาน สามารถ copy ไปใช้ได้ทันที

scripts/ai-review.py

#!/usr/bin/env python3
"""
AI Code Review Script for GitLab CI/CD
Uses Ollama Local LLM for code analysis

Usage:
    python ai-review.py --type quality --ollama-host http://localhost:11434 --model deepseek-coder:6.7b
"""

import argparse
import json
import os
import re
import sys
import subprocess
from dataclasses import dataclass
from typing import Optional, List, Dict, Any
from pathlib import Path

import requests
from requests.exceptions import RequestException


# ====================
# PROMPTS TEMPLATES
# ====================

PROMPTS = {
    "quality": """You are an expert code reviewer specializing in code quality.
Analyze the following code changes and provide a comprehensive quality review.

Focus on:
1. Code readability and maintainability
2. Naming conventions (variables, functions, classes)
3. Code duplication (DRY principle violations)
4. Function/method length and complexity
5. Cyclomatic complexity concerns
6. Code organization and structure

CODE CHANGES TO REVIEW:
```diff
{code}
```

IMPORTANT: Respond ONLY with valid JSON in this exact format:
{{
  "score": ,
  "issues": [
    {{
      "severity": "",
      "file": "",
      "line": ,
      "message": "",
      "suggestion": ""
    }}
  ],
  "positive_points": [""],
  "summary": ""
}}""",

    "security": """You are a security expert specializing in application security.
Perform a thorough security review of the following code changes.

Focus on:
1. SQL injection vulnerabilities
2. Cross-site scripting (XSS) risks
3. Authentication and authorization issues
4. Sensitive data exposure (passwords, API keys, tokens)
5. Insecure dependencies or configurations
6. Input validation gaps
7. Path traversal vulnerabilities
8. Command injection risks

CODE CHANGES TO REVIEW:
```diff
{code}
```

IMPORTANT: Respond ONLY with valid JSON in this exact format:
{{
  "score": ,
  "vulnerabilities": [
    {{
      "severity": "",
      "type": "",
      "cwe": "",
      "file": "",
      "line": ,
      "description": "",
      "remediation": ""
    }}
  ],
  "secure_practices": [""],
  "summary": ""
}}""",

    "best_practices": """You are a software engineering expert specializing in best practices.
Review the following code changes for adherence to software engineering best practices.

Focus on:
1. SOLID principles adherence
2. Appropriate design patterns usage
3. Error handling strategies
4. Logging and monitoring practices
5. Code documentation quality
6. Testing considerations
7. API design principles
8. Dependency management

CODE CHANGES TO REVIEW:
```diff
{code}
```

IMPORTANT: Respond ONLY with valid JSON in this exact format:
{{
  "score": ,
  "recommendations": [
    {{
      "principle": "",
      "severity": "",
      "file": "",
      "line": ,
      "current": "",
      "recommended": "",
      "reason": ""
    }}
  ],
  "good_practices": [""],
  "summary": ""
}}""",

    "performance": """You are a performance optimization expert.
Analyze the following code changes for potential performance issues.

Focus on:
1. Time complexity analysis (Big O notation)
2. Memory usage patterns and potential leaks
3. Database query efficiency (N+1 problems, missing indexes)
4. Caching opportunities
5. Async/await usage and blocking operations
6. Resource management (connections, file handles)
7. Loop optimization opportunities
8. Unnecessary computations

CODE CHANGES TO REVIEW:
```diff
{code}
```

IMPORTANT: Respond ONLY with valid JSON in this exact format:
{{
  "score": ,
  "issues": [
    {{
      "severity": "",
      "type": "",
      "file": "",
      "line": ,
      "current_complexity": "",
      "suggested_complexity": "",
      "description": "",
      "optimization": ""
    }}
  ],
  "optimizations_applied": [""],
  "summary": ""
}}"""
}


# ====================
# DATA CLASSES
# ====================

@dataclass
class ReviewResult:
    """Container for review results"""
    review_type: str
    score: int
    issues: List[Dict[str, Any]]
    positive_points: List[str]
    summary: str
    raw_response: str
    duration_ms: float


# ====================
# OLLAMA CLIENT
# ====================

class OllamaClient:
    """Client for interacting with Ollama API"""
    
    def __init__(self, host: str, model: str, timeout: int = 120):
        self.host = host.rstrip('/')
        self.model = model
        self.timeout = timeout
        self.api_url = f"{self.host}/api/generate"
    
    def generate(self, prompt: str) -> Dict[str, Any]:
        """Generate response from Ollama"""
        payload = {
            "model": self.model,
            "prompt": prompt,
            "stream": False,
            "options": {
                "temperature": 0.3,  # Lower temperature for more consistent reviews
                "top_p": 0.9,
                "num_predict": 4096  # Allow longer responses
            }
        }
        
        try:
            response = requests.post(
                self.api_url,
                json=payload,
                timeout=self.timeout
            )
            response.raise_for_status()
            return response.json()
        except RequestException as e:
            raise RuntimeError(f"Ollama API error: {e}")
    
    def health_check(self) -> bool:
        """Check if Ollama server is healthy"""
        try:
            response = requests.get(f"{self.host}/api/tags", timeout=5)
            return response.status_code == 200
        except:
            return False


# ====================
# GIT OPERATIONS
# ====================

class GitHelper:
    """Helper for Git operations in CI environment"""
    
    @staticmethod
    def get_changed_files() -> List[str]:
        """Get list of changed files in the current MR/commit"""
        # Try to get from CI environment variables
        if os.environ.get('CI_MERGE_REQUEST_DIFF_BASE_SHA'):
            # Merge Request context
            base_sha = os.environ['CI_MERGE_REQUEST_DIFF_BASE_SHA']
            head_sha = os.environ['CI_COMMIT_SHA']
            cmd = f"git diff --name-only {base_sha}...{head_sha}"
        elif os.environ.get('CI_COMMIT_BEFORE_SHA'):
            # Push context
            before_sha = os.environ['CI_COMMIT_BEFORE_SHA']
            head_sha = os.environ['CI_COMMIT_SHA']
            cmd = f"git diff --name-only {before_sha}...{head_sha}"
        else:
            # Fallback: last commit
            cmd = "git diff --name-only HEAD~1 HEAD"
        
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        if result.returncode == 0:
            return [f for f in result.stdout.strip().split('\n') if f]
        return []
    
    @staticmethod
    def get_diff(file_path: Optional[str] = None) -> str:
        """Get git diff for code review"""
        if os.environ.get('CI_MERGE_REQUEST_DIFF_BASE_SHA'):
            base_sha = os.environ['CI_MERGE_REQUEST_DIFF_BASE_SHA']
            head_sha = os.environ['CI_COMMIT_SHA']
            if file_path:
                cmd = f"git diff {base_sha}...{head_sha} -- {file_path}"
            else:
                cmd = f"git diff {base_sha}...{head_sha}"
        elif os.environ.get('CI_COMMIT_BEFORE_SHA'):
            before_sha = os.environ['CI_COMMIT_BEFORE_SHA']
            head_sha = os.environ['CI_COMMIT_SHA']
            if file_path:
                cmd = f"git diff {before_sha}...{head_sha} -- {file_path}"
            else:
                cmd = f"git diff {before_sha}...{head_sha}"
        else:
            if file_path:
                cmd = f"git diff HEAD~1 HEAD -- {file_path}"
            else:
                cmd = "git diff HEAD~1 HEAD"
        
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        return result.stdout if result.returncode == 0 else ""
    
    @staticmethod
    def get_file_content(file_path: str) -> str:
        """Get content of a specific file"""
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        except Exception:
            return ""


# ====================
# REVIEW ENGINE
# ====================

class CodeReviewer:
    """Main code review engine"""
    
    # File extensions to review (exclude non-code files)
    REVIEWABLE_EXTENSIONS = {
        '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs',
        '.php', '.rb', '.cs', '.cpp', '.c', '.h', '.swift', '.kt',
        '.scala', '.vue', '.svelte', '.sql', '.sh', '.yaml', '.yml',
        '.json', '.html', '.css', '.scss', '.sass', '.less'
    }
    
    # Files/patterns to skip
    SKIP_PATTERNS = {
        'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
        'poetry.lock', 'Cargo.lock', 'go.sum',
        '.min.js', '.min.css', 'dist/', 'build/', 'node_modules/'
    }
    
    def __init__(self, ollama_client: OllamaClient):
        self.ollama = ollama_client
        self.git = GitHelper()
    
    def should_review_file(self, file_path: str) -> bool:
        """Check if file should be reviewed"""
        # Check skip patterns
        for pattern in self.SKIP_PATTERNS:
            if pattern in file_path:
                return False
        
        # Check extension
        ext = Path(file_path).suffix.lower()
        return ext in self.REVIEWABLE_EXTENSIONS
    
    def review(self, review_type: str, max_files: int = 20) -> ReviewResult:
        """Perform code review"""
        import time
        
        # Get prompt template
        prompt_template = PROMPTS.get(review_type)
        if not prompt_template:
            raise ValueError(f"Unknown review type: {review_type}")
        
        # Get changed files
        changed_files = self.git.get_changed_files()
        reviewable_files = [f for f in changed_files if self.should_review_file(f)]
        
        if not reviewable_files:
            return ReviewResult(
                review_type=review_type,
                score=100,
                issues=[],
                positive_points=["No reviewable code changes found"],
                summary="No code files to review",
                raw_response="",
                duration_ms=0
            )
        
        # Limit files to prevent timeout
        reviewable_files = reviewable_files[:max_files]
        
        # Get combined diff
        diff_parts = []
        for file_path in reviewable_files:
            file_diff = self.git.get_diff(file_path)
            if file_diff:
                diff_parts.append(f"--- {file_path} ---\n{file_diff}")
        
        combined_diff = "\n\n".join(diff_parts)
        
        # Truncate if too long
        max_chars = 15000
        if len(combined_diff) > max_chars:
            combined_diff = combined_diff[:max_chars] + "\n... [truncated]"
        
        # Build prompt
        prompt = prompt_template.format(code=combined_diff)
        
        # Call Ollama
        start_time = time.time()
        response = self.ollama.generate(prompt)
        duration_ms = (time.time() - start_time) * 1000
        
        # Parse response
        raw_response = response.get('response', '')
        parsed = self._parse_response(raw_response)
        
        return ReviewResult(
            review_type=review_type,
            score=parsed.get('score', 50),
            issues=parsed.get('issues', parsed.get('vulnerabilities', parsed.get('recommendations', []))),
            positive_points=parsed.get('positive_points', parsed.get('secure_practices', parsed.get('good_practices', parsed.get('optimizations_applied', [])))),
            summary=parsed.get('summary', 'Review completed'),
            raw_response=raw_response,
            duration_ms=duration_ms
        )
    
    def _parse_response(self, response: str) -> Dict[str, Any]:
        """Parse AI response to extract JSON"""
        # Try direct JSON parse
        try:
            return json.loads(response)
        except json.JSONDecodeError:
            pass
        
        # Try to find JSON in response
        json_patterns = [
            r'\{[\s\S]*\}',  # Full JSON object
            r'```json\s*([\s\S]*?)\s*```',  # JSON in code block
            r'```\s*([\s\S]*?)\s*```'  # Any code block
        ]
        
        for pattern in json_patterns:
            match = re.search(pattern, response)
            if match:
                try:
                    json_str = match.group(1) if '```' in pattern else match.group()
                    return json.loads(json_str)
                except (json.JSONDecodeError, IndexError):
                    continue
        
        # Fallback: try to extract score
        score_match = re.search(r'"?score"?\s*[:=]\s*(\d+)', response)
        score = int(score_match.group(1)) if score_match else 50
        
        return {
            "score": score,
            "issues": [],
            "positive_points": [],
            "summary": "Could not parse full AI response. Manual review recommended."
        }


# ====================
# OUTPUT FORMATTERS
# ====================

def format_console_output(result: ReviewResult) -> str:
    """Format result for console output"""
    lines = []
    
    # Header
    lines.append(f"\n{'='*60}")
    lines.append(f"🤖 AI {result.review_type.upper()} REVIEW")
    lines.append(f"{'='*60}")
    
    # Score with emoji
    if result.score >= 80:
        emoji = "🟢"
        status = "EXCELLENT"
    elif result.score >= 70:
        emoji = "🟢"
        status = "GOOD"
    elif result.score >= 50:
        emoji = "🟡"
        status = "NEEDS IMPROVEMENT"
    else:
        emoji = "🔴"
        status = "POOR"
    
    lines.append(f"\n{emoji} Score: {result.score}/100 ({status})")
    lines.append(f"⏱️  Duration: {result.duration_ms:.0f}ms")
    
    # Issues
    if result.issues:
        lines.append(f"\n📋 Issues Found: {len(result.issues)}")
        for i, issue in enumerate(result.issues[:10], 1):  # Limit to 10
            severity = issue.get('severity', 'unknown')
            sev_emoji = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}.get(severity, "⚪")
            lines.append(f"\n  {i}. {sev_emoji} [{severity.upper()}] {issue.get('file', 'unknown')}")
            if issue.get('line'):
                lines.append(f"     Line: {issue['line']}")
            msg = issue.get('message', issue.get('description', 'No description'))
            lines.append(f"     {msg[:100]}...")
    
    # Positive points
    if result.positive_points:
        lines.append(f"\n✅ Positive Points:")
        for point in result.positive_points[:5]:
            lines.append(f"  • {point}")
    
    # Summary
    lines.append(f"\n📝 Summary:")
    lines.append(f"  {result.summary}")
    
    lines.append(f"\n{'='*60}\n")
    
    return "\n".join(lines)


def format_json_output(result: ReviewResult) -> str:
    """Format result as JSON"""
    return json.dumps({
        "review_type": result.review_type,
        "score": result.score,
        "issues": result.issues,
        "positive_points": result.positive_points,
        "summary": result.summary,
        "duration_ms": result.duration_ms,
        "timestamp": os.environ.get('CI_JOB_STARTED_AT', ''),
        "commit_sha": os.environ.get('CI_COMMIT_SHA', ''),
        "branch": os.environ.get('CI_COMMIT_REF_NAME', '')
    }, indent=2)


# ====================
# MAIN
# ====================

def main():
    parser = argparse.ArgumentParser(description='AI Code Review using Ollama')
    parser.add_argument('--type', '-t', 
                        choices=['quality', 'security', 'best_practices', 'performance'],
                        required=True,
                        help='Type of review to perform')
    parser.add_argument('--ollama-host', '-H',
                        default=os.environ.get('OLLAMA_HOST', 'http://localhost:11434'),
                        help='Ollama server URL')
    parser.add_argument('--model', '-m',
                        default=os.environ.get('OLLAMA_MODEL', 'deepseek-coder:6.7b'),
                        help='Ollama model to use')
    parser.add_argument('--output', '-o',
                        help='Output file for JSON report')
    parser.add_argument('--timeout', type=int, default=120,
                        help='API timeout in seconds')
    parser.add_argument('--max-files', type=int, default=20,
                        help='Maximum files to review')
    parser.add_argument('--verbose', '-v', action='store_true',
                        help='Enable verbose output')
    
    args = parser.parse_args()
    
    print(f"🦙 AI Code Review starting...")
    print(f"   Type: {args.type}")
    print(f"   Model: {args.model}")
    print(f"   Ollama: {args.ollama_host}")
    
    # Initialize client
    client = OllamaClient(args.ollama_host, args.model, args.timeout)
    
    # Health check
    if not client.health_check():
        print(f"❌ Error: Cannot connect to Ollama at {args.ollama_host}")
        sys.exit(1)
    
    print(f"✅ Ollama connection OK")
    
    # Initialize reviewer
    reviewer = CodeReviewer(client)
    
    # Perform review
    try:
        result = reviewer.review(args.type, args.max_files)
    except Exception as e:
        print(f"❌ Review failed: {e}")
        sys.exit(1)
    
    # Output results
    print(format_console_output(result))
    
    # Save JSON report
    json_output = format_json_output(result)
    if args.output:
        with open(args.output, 'w') as f:
            f.write(json_output)
        print(f"📄 Report saved to: {args.output}")
    
    # Exit with appropriate code
    if result.score < 50:
        print("❌ Review FAILED: Score below 50")
        sys.exit(1)
    elif result.score < 70:
        print("⚠️  Review PASSED with warnings: Score below 70")
        sys.exit(0)
    else:
        print("✅ Review PASSED")
        sys.exit(0)


if __name__ == '__main__':
    main()

scripts/review-decision.py

#!/usr/bin/env python3
"""
Review Decision Script
Aggregates multiple review reports and makes pass/fail decision

Usage:
    python review-decision.py --threshold 70 --quality-report quality_report.json
"""

import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional


class ReviewDecision:
    """Aggregate review results and make decision"""
    
    # Weights for different review types
    WEIGHTS = {
        'quality': 0.3,
        'security': 0.4,  # Security has highest weight
        'best_practices': 0.2,
        'performance': 0.1
    }
    
    def __init__(self, threshold: int = 70):
        self.threshold = threshold
        self.reports: Dict[str, dict] = {}
    
    def load_report(self, review_type: str, file_path: str) -> bool:
        """Load a review report from file"""
        try:
            with open(file_path, 'r') as f:
                self.reports[review_type] = json.load(f)
            return True
        except (FileNotFoundError, json.JSONDecodeError) as e:
            print(f"⚠️  Could not load {review_type} report: {e}")
            return False
    
    def calculate_weighted_score(self) -> float:
        """Calculate weighted average score"""
        total_weight = 0
        weighted_sum = 0
        
        for review_type, weight in self.WEIGHTS.items():
            if review_type in self.reports:
                score = self.reports[review_type].get('score', 50)
                weighted_sum += score * weight
                total_weight += weight
        
        if total_weight == 0:
            return 0
        
        return weighted_sum / total_weight
    
    def get_critical_issues(self) -> List[dict]:
        """Get all critical/high severity issues"""
        critical = []
        for review_type, report in self.reports.items():
            issues = report.get('issues', report.get('vulnerabilities', report.get('recommendations', [])))
            for issue in issues:
                severity = issue.get('severity', '').lower()
                if severity in ('critical', 'high'):
                    critical.append({
                        'type': review_type,
                        **issue
                    })
        return critical
    
    def make_decision(self) -> dict:
        """Make final pass/fail decision"""
        weighted_score = self.calculate_weighted_score()
        critical_issues = self.get_critical_issues()
        
        # Decision logic
        passed = True
        reasons = []
        
        # Check weighted score
        if weighted_score < self.threshold:
            passed = False
            reasons.append(f"Weighted score ({weighted_score:.1f}) below threshold ({self.threshold})")
        
        # Check for critical security issues (auto-fail)
        security_critical = [i for i in critical_issues if i.get('type') == 'security' and i.get('severity') == 'critical']
        if security_critical:
            passed = False
            reasons.append(f"Found {len(security_critical)} critical security vulnerabilities")
        
        # Check for too many high severity issues
        high_issues = [i for i in critical_issues if i.get('severity') == 'high']
        if len(high_issues) > 5:
            passed = False
            reasons.append(f"Too many high severity issues ({len(high_issues)})")
        
        return {
            'passed': passed,
            'weighted_score': round(weighted_score, 1),
            'threshold': self.threshold,
            'reasons': reasons,
            'critical_issues_count': len(critical_issues),
            'reports_summary': {
                rtype: {
                    'score': report.get('score'),
                    'issues_count': len(report.get('issues', report.get('vulnerabilities', [])))
                }
                for rtype, report in self.reports.items()
            }
        }


def generate_summary_markdown(decision: dict) -> str:
    """Generate markdown summary for GitLab"""
    lines = []
    
    # Header
    status_emoji = "✅" if decision['passed'] else "❌"
    lines.append(f"# {status_emoji} AI Code Review Decision")
    lines.append("")
    
    # Result
    result_text = "PASSED" if decision['passed'] else "FAILED"
    lines.append(f"## Result: **{result_text}**")
    lines.append("")
    
    # Score
    score = decision['weighted_score']
    threshold = decision['threshold']
    score_emoji = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
    lines.append(f"### {score_emoji} Weighted Score: {score}/100 (threshold: {threshold})")
    lines.append("")
    
    # Reports summary
    lines.append("### 📊 Review Breakdown")
    lines.append("")
    lines.append("| Review Type | Score | Issues |")
    lines.append("|-------------|-------|--------|")
    for rtype, summary in decision['reports_summary'].items():
        lines.append(f"| {rtype.replace('_', ' ').title()} | {summary['score']} | {summary['issues_count']} |")
    lines.append("")
    
    # Reasons if failed
    if decision['reasons']:
        lines.append("### ⚠️ Failure Reasons")
        lines.append("")
        for reason in decision['reasons']:
            lines.append(f"- {reason}")
        lines.append("")
    
    # Critical issues
    if decision['critical_issues_count'] > 0:
        lines.append(f"### 🔴 Critical/High Issues: {decision['critical_issues_count']}")
        lines.append("")
        lines.append("> Please review the detailed reports for more information.")
        lines.append("")
    
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description='Make review decision')
    parser.add_argument('--threshold', '-t', type=int, default=70,
                        help='Pass threshold (0-100)')
    parser.add_argument('--quality-report', help='Quality review report')
    parser.add_argument('--security-report', help='Security review report')
    parser.add_argument('--best-practices-report', help='Best practices report')
    parser.add_argument('--performance-report', help='Performance review report')
    
    args = parser.parse_args()
    
    decision_maker = ReviewDecision(args.threshold)
    
    # Load available reports
    if args.quality_report:
        decision_maker.load_report('quality', args.quality_report)
    if args.security_report:
        decision_maker.load_report('security', args.security_report)
    if args.best_practices_report:
        decision_maker.load_report('best_practices', args.best_practices_report)
    if args.performance_report:
        decision_maker.load_report('performance', args.performance_report)
    
    # Make decision
    decision = decision_maker.make_decision()
    
    # Output JSON
    with open('final_review_report.json', 'w') as f:
        json.dump(decision, f, indent=2)
    
    # Output markdown summary
    summary = generate_summary_markdown(decision)
    with open('review_summary.md', 'w') as f:
        f.write(summary)
    
    # Console output
    print(summary)
    
    # Exit code
    sys.exit(0 if decision['passed'] else 1)


if __name__ == '__main__':
    main()

scripts/post-mr-comment.py

#!/usr/bin/env python3
"""
Post Review Comment to GitLab Merge Request

Usage:
    python post-mr-comment.py --project-id 123 --mr-iid 45 --report final_review_report.json
"""

import argparse
import os
import json
import requests
from typing import Optional


class GitLabClient:
    """Client for GitLab API"""
    
    def __init__(self, token: str, url: str = "https://gitlab.com"):
        self.token = token
        self.url = url.rstrip('/')
        self.headers = {"PRIVATE-TOKEN": token}
    
    def post_mr_comment(self, project_id: int, mr_iid: int, body: str) -> dict:
        """Post a comment to a merge request"""
        url = f"{self.url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/notes"
        response = requests.post(url, headers=self.headers, json={"body": body})
        response.raise_for_status()
        return response.json()


def format_mr_comment(report_path: str) -> str:
    """Format review report as MR comment"""
    with open(report_path, 'r') as f:
        report = json.load(f)
    
    # Read summary markdown
    summary_path = 'review_summary.md'
    if os.path.exists(summary_path):
        with open(summary_path, 'r') as f:
            return f.read()
    
    # Fallback to basic format
    status = "✅ PASSED" if report.get('passed') else "❌ FAILED"
    score = report.get('weighted_score', 0)
    
    return f"""## 🤖 AI Code Review

{status}

**Score:** {score}/100

See pipeline artifacts for detailed reports.
"""


def main():
    parser = argparse.ArgumentParser(description='Post MR comment')
    parser.add_argument('--project-id', type=int, required=True)
    parser.add_argument('--mr-iid', type=int, required=True)
    parser.add_argument('--report', required=True)
    parser.add_argument('--gitlab-url', default=os.environ.get('CI_SERVER_URL', 'https://gitlab.com'))
    parser.add_argument('--token', default=os.environ.get('GITLAB_TOKEN'))
    
    args = parser.parse_args()
    
    if not args.token:
        print("❌ Error: GITLAB_TOKEN not provided")
        return 1
    
    # Format comment
    comment = format_mr_comment(args.report)
    
    # Post comment
    client = GitLabClient(args.token, args.gitlab_url)
    try:
        result = client.post_mr_comment(args.project_id, args.mr_iid, comment)
        print(f"✅ Comment posted to MR !{args.mr_iid}")
        return 0
    except Exception as e:
        print(f"❌ Failed to post comment: {e}")
        return 1


if __name__ == '__main__':
    exit(main())

8 การตั้งค่า Thresholds

กำหนดเกณฑ์การผ่าน/ไม่ผ่านของ code review เพื่อควบคุมคุณภาพของโค้ดที่ merge เข้าสู่ codebase

Basic Threshold Settings

70

แนะนำ:

  • 50-60: Permissive (สำหรับทีมเริ่มต้น)
  • 70-80: Standard (แนะนำสำหรับทั่วไป)
  • 85-95: Strict (สำหรับ production code)

Auto-Fail Conditions

Critical Security Vulnerabilities Auto-Fail
High Security Vulnerabilities > 3 Auto-Fail
SQL Injection Detected Auto-Fail
Hardcoded Credentials Auto-Fail

ไฟล์กำหนดค่า Thresholds

# ai-review-config.yaml
# การตั้งค่า thresholds และกฎการตัดสินใจ

thresholds:
  # Global threshold
  global: 70
  
  # Per-review type thresholds
  quality: 65
  security: 75      # สูงกว่าเพราะ security สำคัญ
  best_practices: 60
  performance: 55

# การให้น้ำหนักของแต่ละประเภท
weights:
  quality: 0.25
  security: 0.40
  best_practices: 0.20
  performance: 0.15

# Auto-fail conditions
auto_fail:
  # Security
  critical_vulnerabilities: true    # มี critical = fail ทันที
  high_vulnerabilities_max: 3       # เกิน 3  high = fail
  sql_injection: true
  hardcoded_secrets: true
  xss_vulnerabilities: true
  
  # Quality
  cyclomatic_complexity_max: 20     # complexity เกิน 20 = fail
  duplicate_code_max_percent: 15    # duplicate เกิน 15% = fail

# GitLab MR Rules
mr_rules:
  # ต้องผ่าน AI review กี่จากทั้งหมด
  require_all_reviews: false
  required_reviews:
    - security     # ต้องผ่าน security เสมอ
    - quality      # ต้องผ่าน quality
  
  # อนุญาตให้ bypass ได้หรือไม่
  allow_bypass: true
  bypass_labels:
    - "skip-ai-review"
    - "emergency-fix"
  
  # ใคร bypass ได้
  bypass_maintainer_ids:
    - 123
    - 456

# Notifications
notifications:
  # แจ้งเตือนเมื่อ score ต่ำ
  alert_on_low_score: true
  low_score_threshold: 50
  
  # Slack/Teams webhook
  webhook_url: "${SLACK_WEBHOOK_URL}"
  
  # Email ถึงผู้เกี่ยวข้อง
  email_on_fail: true
  email_recipients:
    - tech-lead@company.com

GitLab Pipeline Rules สำหรับควบคุม Merge

# เพิ่มใน .gitlab-ci.yml

# Rule: Block merge ถ้า AI review ไม่ผ่าน
ai_review_gate:
  stage: .post
  script:
    - echo "Checking AI review results..."
    - |
      if [ -f final_review_report.json ]; then
        PASSED=$(cat final_review_report.json | python3 -c "import json,sys; print(json.load(sys.stdin)['passed'])")
        if [ "$PASSED" != "True" ]; then
          echo "❌ AI Review did not pass. Merge blocked."
          exit 1
        fi
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: always
  allow_failure: false

# Rule: Allow bypass ด้วย label
.bypass_check:
  rules:
    - if: $CI_MERGE_REQUEST_LABELS =~ /skip-ai-review/
      when: never
    - when: always

9 Advanced Features

ฟีเจอร์ขั้นสูงสำหรับการใช้งาน AI Code Review อย่างมีประสิทธิภาพ

9.1 รีวิวเฉพาะไฟล์ที่เปลี่ยนแปลง (Diff Only)

การรีวิวเฉพาะส่วนที่เปลี่ยนแปลงช่วยประหยัดเวลาและทรัพยากร:

# diff_review.py - Review only changed lines

import subprocess
import re

def get_diff_with_context(file_path: str, context_lines: int = 3) -> str:
    """Get diff with surrounding context for better review"""
    cmd = f"git diff HEAD~1 HEAD --unified={context_lines} -- {file_path}"
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout

def extract_changed_lines(diff: str) -> list:
    """Extract only the changed lines from diff"""
    changed = []
    for line in diff.split('\n'):
        if line.startswith('+') and not line.startswith('+++'):
            changed.append(line[1:])  # Remove the + prefix
    return changed

def filter_reviewable_changes(diff: str) -> str:
    """Filter out non-reviewable changes (whitespace, comments)"""
    lines = []
    for line in diff.split('\n'):
        # Skip pure whitespace changes
        if line.strip() in ['', '+', '-']:
            continue
        # Skip comment-only changes
        if re.match(r'^[+\-]\s*(//|#|/\*|\*)', line):
            continue
        lines.append(line)
    return '\n'.join(lines)

9.2 Multiple Reviewers (หลาย Model)

ใช้หลาย model รีวิวพร้อมกันเพื่อเพิ่มความแม่นยำ:

# .gitlab-ci.yml - Multiple AI reviewers

# Reviewer 1: DeepSeek-Coder (Code focused)
ai_review_deepseek:
  stage: quality
  script:
    - python scripts/ai-review.py --model deepseek-coder:6.7b --output ds_report.json
  artifacts:
    paths: [ds_report.json]

# Reviewer 2: CodeLlama (General purpose)
ai_review_codellama:
  stage: quality
  script:
    - python scripts/ai-review.py --model codellama:7b --output cl_report.json
  artifacts:
    paths: [cl_report.json]

# Reviewer 3: Qwen-Coder (Fast and efficient)
ai_review_qwen:
  stage: quality
  script:
    - python scripts/ai-review.py --model qwen2.5-coder:7b --output qw_report.json
  artifacts:
    paths: [qw_report.json]

# Aggregate results
aggregate_reviews:
  stage: decision
  script:
    - python scripts/aggregate-reviews.py
        --reports ds_report.json,cl_report.json,qw_report.json
        --method consensus  # or: average, best, worst
        --output final_report.json
  needs:
    - ai_review_deepseek
    - ai_review_codellama
    - ai_review_qwen

Consensus Algorithm

Average

เฉลี่ยคะแนนจากทุก model

Consensus

ต้องเห็นชอบร่วมกัน (majority)

Best

ใช้คะแนนสูงสุด

9.3 บันทึกประวัติการรีวิว

เก็บประวัติการรีวิวเพื่อวิเคราะห์แนวโน้มและปรับปรุง:

# review_history.py - Track review history

import json
import sqlite3
from datetime import datetime
from pathlib import Path

class ReviewHistory:
    """Track and analyze review history"""
    
    def __init__(self, db_path: str = "reviews.db"):
        self.conn = sqlite3.connect(db_path)
        self._init_db()
    
    def _init_db(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS reviews (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT,
                commit_sha TEXT,
                branch TEXT,
                author TEXT,
                review_type TEXT,
                score INTEGER,
                issues_count INTEGER,
                passed BOOLEAN,
                report_json TEXT
            )
        """)
        self.conn.commit()
    
    def save_review(self, result: dict, metadata: dict):
        """Save review result to history"""
        self.conn.execute("""
            INSERT INTO reviews 
            (timestamp, commit_sha, branch, author, review_type, score, issues_count, passed, report_json)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            datetime.now().isoformat(),
            metadata.get('commit_sha'),
            metadata.get('branch'),
            metadata.get('author'),
            result.get('review_type'),
            result.get('score'),
            len(result.get('issues', [])),
            result.get('score', 0) >= 70,
            json.dumps(result)
        ))
        self.conn.commit()
    
    def get_trend(self, days: int = 30) -> dict:
        """Get score trend over time"""
        cursor = self.conn.execute("""
            SELECT date(timestamp) as day, AVG(score) as avg_score, COUNT(*) as count
            FROM reviews
            WHERE timestamp >= date('now', ?)
            GROUP BY date(timestamp)
            ORDER BY day
        """, (f'-{days} days',))
        
        return {
            'trend': [dict(zip(['day', 'avg_score', 'count'], row)) for row in cursor],
            'overall_avg': self._get_overall_avg()
        }
    
    def get_common_issues(self, limit: int = 10) -> list:
        """Get most common issues"""
        cursor = self.conn.execute("""
            SELECT review_type, COUNT(*) as count
            FROM reviews, json_each(report_json, '$.issues')
            GROUP BY review_type
            ORDER BY count DESC
            LIMIT ?
        """, (limit,))
        
        return [dict(zip(['review_type', 'count'], row)) for row in cursor]

9.4 Merge Request Comments จาก AI

AI แสดงความคิดเห็นโดยตรงบนไฟล์ใน Merge Request:

# post_line_comments.py - Post comments on specific lines

import requests
import json

def post_line_comment(
    gitlab_url: str,
    token: str,
    project_id: int,
    mr_iid: int,
    file_path: str,
    line_number: int,
    comment: str,
    severity: str = "info"
):
    """Post a comment on a specific line in MR"""
    
    # Severity emoji
    emoji = {
        "critical": "🔴",
        "high": "🟠", 
        "medium": "🟡",
        "low": "🟢",
        "info": "ℹ️"
    }.get(severity, "💬")
    
    url = f"{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/discussions"
    
    headers = {"PRIVATE-TOKEN": token}
    
    payload = {
        "body": f"{emoji} **AI Review Comment**\n\n{comment}",
        "position": {
            "base_sha": get_base_sha(project_id, mr_iid),
            "head_sha": get_head_sha(project_id, mr_iid),
            "start_sha": get_start_sha(project_id, mr_iid),
            "position_type": "text",
            "new_path": file_path,
            "new_line": line_number
        }
    }
    
    response = requests.post(url, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()

def post_review_summary_comment(
    gitlab_url: str,
    token: str,
    project_id: int,
    mr_iid: int,
    report: dict
):
    """Post overall review summary as MR comment"""
    
    passed = report.get('passed', False)
    score = report.get('weighted_score', 0)
    
    status = "✅ PASSED" if passed else "❌ NEEDS ATTENTION"
    score_bar = generate_score_bar(score)
    
    comment = f"""## 🤖 AI Code Review Summary

{status}

### Score: {score}/100
{score_bar}

### Reviews Completed:
"""
    
    for review_type, summary in report.get('reports_summary', {}).items():
        icon = "✅" if summary['score'] >= 70 else "⚠️"
        comment += f"- {icon} **{review_type}**: {summary['score']}/100 ({summary['issues_count']} issues)\n"
    
    if report.get('reasons'):
        comment += "\n### ⚠️ Issues to Address:\n"
        for reason in report['reasons']:
            comment += f"- {reason}\n"
    
    comment += "\n---\n*Powered by Ollama AI Code Reviewer*"
    
    # Post the comment
    url = f"{gitlab_url}/api/v4/projects/{project_id}/merge_requests/{mr_iid}/notes"
    headers = {"PRIVATE-TOKEN": token}
    requests.post(url, headers=headers, json={"body": comment})

def generate_score_bar(score: int) -> str:
    """Generate visual score bar"""
    filled = int(score / 10)
    empty = 10 - filled
    color = "🟩" if score >= 70 else "🟨" if score >= 50 else "🟥"
    return f"{color * filled}{'⬜' * empty}"

10 Troubleshooting & Tips

ปัญหาที่พบบ่อยและวิธีแก้ไข เคล็ดลับการใช้งาน

ปัญหา: Ollama Timeout

อาการ: Pipeline ล้มเหลวเพราะ Ollama ใช้เวลานานเกินไป
วิธีแก้:
# เพิ่ม timeout ใน .gitlab-ci.yml
ai_review:
  script:
    - python scripts/ai-review.py --timeout 300  # 5 นาที
  timeout: 10m  # GitLab job timeout

# หรือแบ่งเป็นหลาย jobs
ai_review_batch_1:
  script:
    - python scripts/ai-review.py --files "src/*.py"

ai_review_batch_2:
  script:
    - python scripts/ai-review.py --files "tests/*.py"

ปัญหา: Large Codebase

อาการ: Codebase ใหญ่เกินไป ทำให้ context overflow
วิธีแก้:
# จำกัดขนาด code ที่ส่งให้ AI
MAX_CODE_CHARS = 10000  # ~10K characters

def truncate_code(code: str, max_chars: int = MAX_CODE_CHARS) -> str:
    """Truncate code if too long"""
    if len(code) <= max_chars:
        return code
    
    # ตัดเป็นส่วนๆ และเก็บ important parts
    lines = code.split('\n')
    important_lines = []
    
    for line in lines:
        # เก็บ function definitions, class definitions
        if any(kw in line for kw in ['def ', 'class ', 'async def ', 'function ']):
            important_lines.append(line)
        # เก็บบรรทัดที่เปลี่ยนแปลง (จาก diff)
        elif line.startswith('+') or line.startswith('-'):
            important_lines.append(line)
    
    result = '\n'.join(important_lines)
    if len(result) > max_chars:
        result = result[:max_chars] + "\n... [truncated]"
    
    return result

Tips: Cost Optimization

  • ใช้ model เล็ก: deepseek-coder:6.7b เร็วกว่า codellama:34b 5-10 เท่า
  • รีวิวเฉพาะไฟล์ที่เปลี่ยน: ใช้ git diff แทนไฟล์ทั้งหมด
  • Cache model: Ollama จะ cache model ในหน่วยความจำ อย่า restart บ่อย
  • Batch processing: รวมหลายไฟล์ในการเรียก API ครั้งเดียว
  • Skip ไฟล์ไม่จำเป็น: config, lock files, generated code

Model Selection Guide

Use Case Recommended Model Why
General Code Review deepseek-coder:6.7b ดีที่สุดเมื่อเทียบกับขนาด
Security Review codellama:13b เข้าใจ security patterns ดี
Fast Feedback qwen2.5-coder:1.5b เร็วมาก สำหรับการตรวจเบื้องต้น
Enterprise/Production deepseek-coder:33b แม่นยำที่สุด ต้องการ GPU 24GB+
Multi-language Projects codellama:7b รองรับหลายภาษา

Debug Commands

# Check Ollama status curl http://localhost:11434/api/tags | jq
# Test model response ollama run deepseek-coder:6.7b "Review: def foo(): pass"
# Check GPU usage nvidia-smi -l 1
# Monitor Ollama logs journalctl -u ollama -f

📚 สรุป

สิ่งที่คุณได้เรียนรู้:

  • ติดตั้งและกำหนดค่า Ollama สำหรับ AI Code Review
  • สร้าง GitLab CI/CD Pipeline สำหรับ automated review
  • เขียน Python script สำหรับเรียก Ollama API
  • Prompt Engineering สำหรับ code review
  • กำหนด thresholds และ pass/fail rules
  • Advanced features และ troubleshooting

ขั้นตอนถัดไป:

  • ทดลองใช้กับโปรเจคจริงของคุณ
  • ปรับแต่ง prompts ให้เหมาะกับทีม
  • ตั้งค่า thresholds ตามมาตรฐานขององค์กร
  • วิเคราะห์ review history เพื่อปรับปรุง

Tip: เริ่มต้นด้วย mode "advisory" ก่อน ไม่ block MR แล้วค่อยๆ เปลี่ยนเป็น "enforcing" เมื่อทีมเริ่มคุ้นเคย

🔗 บทความที่เกี่ยวข้อง