GitLab AI Code Review Setup Guide

GitLab AI Code Review Setup Guide

GitLab AI Code Review Options

GitLab Duo : GitLab’s suite of AI-powered features including code suggestions, merge request summaries, vulnerability explanations, and automated code review—powered by Anthropic’s Claude and other models.
TierAI Features Available
FreeBasic CI/CD, limited Duo
PremiumDuo Chat, MR summaries
UltimateFull Duo, security scanning

Option 1: GitLab Duo (Native)

Available on Premium and Ultimate tiers.

Enable GitLab Duo

1
2
3
4
5
6
7
8
# In your GitLab instance settings
# Settings → AI Features → Enable GitLab Duo

# Or via gitlab-rails console
Gitlab::CurrentSettings.update!(
  gitlab_duo_enabled: true,
  duo_features_enabled: true
)

Configure MR Code Review

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# .gitlab/duo-config.yml
duo:
  code_review:
    enabled: true
    auto_summary: true
    security_analysis: true

  languages:
    - javascript
    - typescript
    - python
    - go

  ignore_paths:
    - "**/*.test.*"
    - "**/fixtures/**"
    - "docs/**"

Duo in Merge Requests

Once enabled, Duo automatically:

  • Summarizes changes in MR description
  • Highlights potential security issues
  • Suggests improvements in review comments
  • Explains complex code when asked

Use in MR comments:

1
2
3
/duo explain this function
/duo review for security
/duo suggest improvements

Option 2: GitLab CI/CD with AI

Build custom AI review into your pipeline.

Basic AI Review Pipeline

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# .gitlab-ci.yml
stages:
  - lint
  - test
  - security
  - ai-review

variables:
  OPENAI_API_KEY: $OPENAI_API_KEY

lint:
  stage: lint
  image: node:20
  script:
    - npm ci
    - npm run lint
    - npm run typecheck
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test
  coverage: '/Coverage: \d+\.\d+%/'
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

security-scan:
  stage: security
  image: semgrep/semgrep
  script:
    - semgrep ci --config auto --config p/security-audit
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  allow_failure: false

ai-review:
  stage: ai-review
  image: python:3.11
  script:
    - pip install openai requests
    - python scripts/ai_review.py
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  allow_failure: true  # Don't block on AI review

AI Review Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# scripts/ai_review.py
import os
import json
import requests
from openai import OpenAI

def get_mr_changes():
    """Get changed files from GitLab API"""
    project_id = os.environ["CI_PROJECT_ID"]
    mr_iid = os.environ["CI_MERGE_REQUEST_IID"]
    token = os.environ["CI_JOB_TOKEN"]

    url = f"https://gitlab.com/api/v4/projects/{project_id}/merge_requests/{mr_iid}/changes"
    headers = {"JOB-TOKEN": token}

    response = requests.get(url, headers=headers)
    return response.json()["changes"]

def analyze_changes(changes: list) -> list:
    """Use GPT-4 to analyze code changes"""
    client = OpenAI()

    all_issues = []
    for change in changes:
        if not change.get("diff"):
            continue

        response = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {
                    "role": "system",
                    "content": """Analyze this code diff for:
                    1. Security vulnerabilities
                    2. Logic errors
                    3. Performance issues

                    Return JSON array of issues with fields:
                    - line: line number in new file
                    - severity: critical/high/medium/low
                    - message: description of issue
                    - suggestion: how to fix"""
                },
                {
                    "role": "user",
                    "content": f"File: {change['new_path']}\n\n{change['diff']}"
                }
            ],
            response_format={"type": "json_object"}
        )

        result = json.loads(response.choices[0].message.content)
        for issue in result.get("issues", []):
            issue["file"] = change["new_path"]
            all_issues.append(issue)

    return all_issues

def post_mr_comment(issues: list):
    """Post review comments to MR"""
    project_id = os.environ["CI_PROJECT_ID"]
    mr_iid = os.environ["CI_MERGE_REQUEST_IID"]
    token = os.environ["GITLAB_TOKEN"]

    url = f"https://gitlab.com/api/v4/projects/{project_id}/merge_requests/{mr_iid}/notes"
    headers = {"PRIVATE-TOKEN": token}

    if not issues:
        body = "AI Review: No issues found."
    else:
        body = "## AI Code Review\n\n"
        for issue in issues:
            body += f"### {issue['file']}:{issue.get('line', '?')}\n"
            body += f"**{issue['severity'].upper()}**: {issue['message']}\n"
            if issue.get("suggestion"):
                body += f"\n> Suggestion: {issue['suggestion']}\n"
            body += "\n"

    requests.post(url, headers=headers, json={"body": body})

def main():
    changes = get_mr_changes()
    issues = analyze_changes(changes)
    post_mr_comment(issues)

    # Exit with error if critical issues found
    critical = [i for i in issues if i["severity"] == "critical"]
    if critical:
        print(f"Found {len(critical)} critical issues")
        exit(1)

if __name__ == "__main__":
    main()

Discussion Comments on Specific Lines

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def post_line_comment(project_id: str, mr_iid: str, issue: dict, token: str):
    """Post comment on specific line of code"""
    url = f"https://gitlab.com/api/v4/projects/{project_id}/merge_requests/{mr_iid}/discussions"
    headers = {"PRIVATE-TOKEN": token}

    payload = {
        "body": f"**{issue['severity'].upper()}**: {issue['message']}\n\n{issue.get('suggestion', '')}",
        "position": {
            "base_sha": os.environ["CI_MERGE_REQUEST_DIFF_BASE_SHA"],
            "start_sha": os.environ["CI_MERGE_REQUEST_DIFF_BASE_SHA"],
            "head_sha": os.environ["CI_COMMIT_SHA"],
            "position_type": "text",
            "new_path": issue["file"],
            "new_line": issue["line"]
        }
    }

    response = requests.post(url, headers=headers, json=payload)
    return response.status_code == 201

Option 3: Security Scanning

GitLab Ultimate includes SAST, DAST, and dependency scanning.

Enable All Scanners

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# .gitlab-ci.yml
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml

variables:
  SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
  SECRET_DETECTION_HISTORIC_SCAN: "true"

Custom SAST Configuration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# .gitlab-ci.yml
sast:
  stage: test
  variables:
    SAST_EXCLUDED_ANALYZERS: "spotbugs"
    SAST_ANALYZER_IMAGE_TAG: "3"
    SEARCH_MAX_DEPTH: 10
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

semgrep-sast:
  extends: .sast-analyzer
  variables:
    SEMGREP_RULES: "p/security-audit p/owasp-top-10"

Approval Rules Based on Security

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Require security team approval if vulnerabilities found
security-approval:
  stage: .post
  image: curlimages/curl
  script:
    - |
      VULNS=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
        "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/vulnerability_findings?pipeline_id=$CI_PIPELINE_ID" \
        | jq '. | length')

      if [ "$VULNS" -gt "0" ]; then
        echo "Found $VULNS vulnerabilities, requiring security approval"
        # Set MR to require security team approval
        curl --request PUT \
          --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
          "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID" \
          --data "labels=needs-security-review"
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Option 4: Third-Party Integrations

CodeRabbit for GitLab

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Install via GitLab OAuth integration
# Then configure in repo

# .coderabbit.yaml
reviews:
  auto_review:
    enabled: true
    drafts: false

  security:
    enabled: true
    severity: medium

ignore:
  paths:
    - "**/*.test.*"
    - "**/fixtures/**"

Semgrep CI Integration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# .gitlab-ci.yml
semgrep:
  image: semgrep/semgrep
  stage: security
  script:
    - semgrep ci
  variables:
    SEMGREP_APP_TOKEN: $SEMGREP_APP_TOKEN
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Snyk Integration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# .gitlab-ci.yml
snyk:
  image: snyk/snyk:node
  stage: security
  script:
    - snyk auth $SNYK_TOKEN
    - snyk test --severity-threshold=high
    - snyk monitor
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  allow_failure: false

Merge Request Approval Rules

Configure in GitLab UI

1
2
3
4
5
6
Settings → Merge Requests → Approval Rules

Rules:
1. "Any Approver" - 1 approval required
2. "Security Team" - Required when security label present
3. "Code Owners" - Required for protected files

Code Owners File

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# CODEOWNERS

# Default owners
*                       @team-leads

# Security-sensitive areas
/auth/                  @security-team
/api/                   @security-team @backend-team
*.env*                  @security-team

# Frontend
/src/components/        @frontend-team

# Infrastructure
/terraform/             @platform-team
/.gitlab-ci.yml         @platform-team

Performance Optimization

Caching

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.node-cache:
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - node_modules/
    policy: pull-push

lint:
  extends: .node-cache
  # ...

Parallel Jobs

1
2
3
4
5
6
test:
  stage: test
  parallel: 4
  script:
    - npm ci
    - npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL

Only Run Changed

1
2
3
4
5
6
7
test:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - "src/**/*"
        - "test/**/*"
        - "package*.json"

Complete Pipeline Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# .gitlab-ci.yml
stages:
  - lint
  - test
  - security
  - ai-review
  - deploy

default:
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/

variables:
  FF_USE_FASTZIP: "true"
  ARTIFACT_COMPRESSION_LEVEL: "fast"

# Fast checks
lint:
  stage: lint
  image: node:20-slim
  script:
    - npm ci --prefer-offline
    - npm run lint
    - npm run typecheck
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# Tests
test:
  stage: test
  image: node:20
  parallel: 4
  script:
    - npm ci
    - npm test -- --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL --coverage
  coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# Security
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml

semgrep:
  stage: security
  image: semgrep/semgrep
  script:
    - semgrep ci --config auto --config p/security-audit
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  allow_failure: false

# AI Review
ai-review:
  stage: ai-review
  image: python:3.11-slim
  before_script:
    - pip install --quiet openai requests
  script:
    - python scripts/ai_review.py
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  allow_failure: true

# Deploy to staging on merge
deploy-staging:
  stage: deploy
  script:
    - echo "Deploying to staging..."
  environment:
    name: staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

FAQ

Is GitLab Duo worth the cost of Ultimate?

For larger teams, yes. The security scanning alone often justifies the cost. For smaller teams, the Premium tier with third-party tools (Semgrep, CodeRabbit) provides similar capabilities at lower cost.

How do I migrate from GitHub Actions to GitLab CI?

Most concepts map directly: workflows become pipelines, jobs remain jobs, steps become script lines. The main differences are syntax and built-in features. GitLab has more native security scanning.

Can I use GitLab CI with self-hosted GitLab?

Yes, all features work on self-hosted. GitLab Duo requires connectivity to GitLab’s AI services, but can be configured with private endpoints on Ultimate.

How do I handle secrets in GitLab CI?

Use CI/CD Variables (Settings → CI/CD → Variables). Mark as “Masked” and “Protected” for sensitive values. Never hardcode secrets in gitlab-ci.yml.

Conclusion

Key Takeaways

  • GitLab Duo provides native AI code review on Premium/Ultimate
  • Custom CI pipelines work on any GitLab tier
  • Use GitLab’s built-in security templates for scanning
  • Third-party tools (CodeRabbit, Semgrep, Snyk) fill gaps
  • Configure CODEOWNERS for automatic reviewer assignment
  • Set up approval rules based on security findings
  • Cache dependencies and parallelize for speed
  • Make AI review informational, security checks blocking

AI Coding Security Insights.
Ship Vibe-Coded Apps Safely.

Effortlessly test and evaluate web application security using Vibe Eval agents.