gitlab cicd agents
CI/CD Agent Patterns for GitLab
Last Updated: January 7, 2026
Overview
This guide covers patterns and best practices for integrating AI agents into GitLab CI/CD pipelines:
- Agent Execution Patterns - When and how to run agents in pipelines
- GitLab CI/CD Components - Reusable agent templates
- Pipeline Optimization - Performance and cost optimization
- Security & Compliance - Secure agent execution
- Real-World Examples - Production-ready patterns
Agent Execution Patterns
Pattern 1: Code Review Agent
Use Case: Automated code review on every MR
# .gitlab-ci.yml stages: - review code_review: stage: review image: node:18 before_script: - npm install -g @anthropic/claude-code script: # Get MR changes - MR_DIFF=$(glab mr diff $CI_MERGE_REQUEST_IID) # Run AI code review - | claude chat --prompt "Review this code diff for: - Security vulnerabilities (CRITICAL) - Code quality issues - Best practices violations - Performance concerns Diff: $MR_DIFF Provide: 1. Overall assessment 2. Specific issues with file:line references 3. Recommendations " --output json > review.json # Post results to MR - | SUMMARY=$(jq -r '.summary' review.json) glab mr note $CI_MERGE_REQUEST_IID --message "## AI Code Review $SUMMARY <details> <summary>Full Report</summary> $(jq -r '.detailed_report' review.json) </details>" artifacts: reports: codequality: review.json paths: - review.json expire_in: 30 days rules: - if: $CI_MERGE_REQUEST_ID
Pattern 2: Test Generation Agent
Use Case: Automatically generate tests for new code
test_generation: stage: test image: python:3.11 script: # Find files changed in MR - | CHANGED_FILES=$(git diff --name-only origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD | grep '\.py$') # Generate tests for each file - | for file in $CHANGED_FILES; do echo "Generating tests for $file" python -c " from openai import OpenAI import os client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) with open('$file') as f: code = f.read() response = client.chat.completions.create( model='gpt-4-turbo', messages=[ {'role': 'system', 'content': 'Generate comprehensive pytest tests'}, {'role': 'user', 'content': f'Generate tests for:\n\n{code}'} ] ) test_file = 'tests/test_' + os.path.basename('$file') with open(test_file, 'w') as f: f.write(response.choices[0].message.content) print(f'Generated {test_file}') " done # Run generated tests - pip install pytest - pytest tests/ -v # Commit tests if they pass - git config user.email "ai-agent@gitlab.com" - git config user.name "Test Generation Agent" - git add tests/ - git commit -m "test: add AI-generated tests" || echo "No new tests" - git push origin HEAD:$CI_COMMIT_REF_NAME rules: - if: $CI_MERGE_REQUEST_ID changes: - "src/**/*.py"
Pattern 3: Documentation Agent
Use Case: Generate/update documentation automatically
documentation_agent: stage: docs image: node:18 script: # Generate API documentation - | claude chat --prompt "Generate API documentation for the code in src/api/. Format as OpenAPI 3.1 specification. Include: - All endpoints with descriptions - Request/response schemas - Authentication requirements - Examples " --output yaml > docs/openapi.yml # Generate README sections - | claude chat --prompt "Update README.md based on recent changes. Changes: $(git diff origin/main...HEAD) Update: - Installation instructions if dependencies changed - Usage examples if API changed - Configuration if new env vars added " --output markdown > README.new.md # Update README if changed - | if ! diff -q README.md README.new.md > /dev/null; then mv README.new.md README.md git add README.md docs/ git commit -m "docs: update documentation" git push origin HEAD:$CI_COMMIT_REF_NAME fi rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH changes: - "src/**/*"
Pattern 4: Security Analysis Agent
Use Case: Deep security analysis beyond standard SAST
security_analysis: stage: security image: python:3.11 before_script: - pip install anthropic python-gitlab script: # Run GitLab SAST first - glab ci run sast # Get SAST results - SAST_RESULTS=$(glab ci artifact download --job=sast) # Deep analysis with AI - | python << EOF import anthropic import json import os client = anthropic.Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) # Load SAST results with open('gl-sast-report.json') as f: sast = json.load(f) # Get code context vulnerabilities = sast.get('vulnerabilities', []) for vuln in vulnerabilities: file_path = vuln['location']['file'] line = vuln['location']['start_line'] # Read file context with open(file_path) as f: lines = f.readlines() context = ''.join(lines[max(0, line-10):line+10]) # AI analysis response = client.messages.create( model='claude-sonnet-4.5', max_tokens=4096, messages=[{ 'role': 'user', 'content': f'''Analyze this security vulnerability: Type: {vuln['category']} Severity: {vuln['severity']} Description: {vuln['message']} Code context: {context} Provide: 1. Is this a false positive? (yes/no/maybe) 2. Detailed explanation 3. Remediation steps 4. Example secure code ''' }] ) vuln['ai_analysis'] = response.content[0].text # Save enhanced report with open('security-report-enhanced.json', 'w') as f: json.dump(sast, f, indent=2) # Check for critical issues critical = [v for v in vulnerabilities if v['severity'] == 'Critical' and 'false positive' not in v.get('ai_analysis', '').lower()] if critical: print(f"CRITICAL: {len(critical)} critical vulnerabilities found") exit(1) EOF artifacts: reports: sast: security-report-enhanced.json paths: - security-report-enhanced.json rules: - if: $CI_MERGE_REQUEST_ID - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Pattern 5: Pipeline Optimization Agent
Use Case: Analyze and optimize pipeline performance
pipeline_optimization: stage: .post image: python:3.11 script: # Get pipeline history - | python << EOF from anthropic import Anthropic import gitlab import os import json gl = gitlab.Gitlab('https://gitlab.com', private_token=os.getenv('GITLAB_TOKEN')) project = gl.projects.get(os.getenv('CI_PROJECT_ID')) # Get last 10 pipelines pipelines = project.pipelines.list(per_page=10) pipeline_data = [] for p in pipelines: pipeline = project.pipelines.get(p.id) jobs = pipeline.jobs.list() pipeline_data.append({ 'id': p.id, 'duration': p.duration, 'jobs': [{ 'name': j.name, 'duration': j.duration, 'status': j.status } for j in jobs] }) # AI analysis client = Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY')) response = client.messages.create( model='claude-sonnet-4.5', max_tokens=8192, messages=[{ 'role': 'user', 'content': f'''Analyze these pipeline runs and suggest optimizations: {json.dumps(pipeline_data, indent=2)} Provide: 1. Bottleneck jobs (slowest, most frequent failures) 2. Parallelization opportunities 3. Caching improvements 4. Job consolidation suggestions 5. Specific .gitlab-ci.yml changes ''' }] ) # Save recommendations with open('pipeline-optimization.md', 'w') as f: f.write('# Pipeline Optimization Recommendations\n\n') f.write(response.content[0].text) # Create issue with recommendations issue = project.issues.create({ 'title': 'Pipeline Optimization Recommendations', 'description': response.content[0].text, 'labels': ['pipeline', 'optimization', 'ai-generated'] }) print(f"Created issue: {issue.web_url}") EOF artifacts: paths: - pipeline-optimization.md rules: - if: $CI_PIPELINE_SOURCE == "schedule" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual
GitLab CI/CD Components for Agents
GitLab CI/CD Components allow creating reusable, versioned agent templates.
Component Structure
gitlab-components/
.gitlab-ci.yml
templates/
code-review.yml
test-generation.yml
security-analysis.yml
docs-generation.yml
README.md
Code Review Component
# gitlab-components/templates/code-review.yml spec: inputs: model: default: 'claude-sonnet-4.5' focus_areas: default: 'security,quality,performance' min_severity: default: 'medium' --- ai_code_review: stage: review image: node:18 before_script: - npm install -g @anthropic/claude-code script: - | claude chat \ --model "$[[ inputs.model ]]" \ --prompt "Review MR !$CI_MERGE_REQUEST_IID focusing on: $[[ inputs.focus_areas ]]" \ --output json > review.json # Filter by severity - | jq --arg min_sev "$[[ inputs.min_severity ]]" ' .issues |= map(select(.severity >= $min_sev)) ' review.json > review-filtered.json # Post to MR - glab mr note $CI_MERGE_REQUEST_IID --message "$(jq -r '.summary' review-filtered.json)" artifacts: reports: codequality: review-filtered.json rules: - if: $CI_MERGE_REQUEST_ID
Using Components
# .gitlab-ci.yml include: - component: gitlab.com/blueflyio/gitlab-components/code-review@1.0.0 inputs: model: claude-sonnet-4.5 focus_areas: security,performance min_severity: high
Test Generation Component
# gitlab-components/templates/test-generation.yml spec: inputs: test_framework: default: 'pytest' coverage_target: default: '80' languages: default: 'python,javascript' --- ai_test_generation: stage: test image: python:3.11 variables: TEST_FRAMEWORK: "$[[ inputs.test_framework ]]" COVERAGE_TARGET: "$[[ inputs.coverage_target ]]" script: - pip install openai-agents pytest pytest-cov # Find untested files - | python << EOF from openai_agents import Agent import subprocess import os # Run coverage to find gaps result = subprocess.run( ['pytest', '--cov=src', '--cov-report=json'], capture_output=True ) # Parse coverage report import json with open('coverage.json') as f: cov = json.load(f) # Find files below target target = int(os.getenv('COVERAGE_TARGET')) low_coverage = [ (f, data['summary']['percent_covered']) for f, data in cov['files'].items() if data['summary']['percent_covered'] < target ] # Generate tests for low-coverage files test_gen = Agent( name="test_generator", model="gpt-4-turbo" ) for file, coverage in low_coverage: with open(file) as f: code = f.read() result = test_gen.run( prompt=f"Generate {os.getenv('TEST_FRAMEWORK')} tests for:\n\n{code}", response_format={"type": "code"} ) test_file = f"tests/test_{os.path.basename(file)}" with open(test_file, 'w') as f: f.write(result.code) EOF # Run new tests - pytest tests/ --cov=src --cov-report=term --cov-report=html artifacts: paths: - htmlcov/ reports: coverage_report: coverage_format: cobertura path: coverage.xml
Security Analysis Component
# gitlab-components/templates/security-analysis.yml spec: inputs: enable_ai_analysis: default: 'true' block_on_critical: default: 'true' custom_rules: default: '' --- ai_security_analysis: stage: security needs: - job: sast artifacts: true script: - | if [ "$[[ inputs.enable_ai_analysis ]]" = "true" ]; then python analyze_security.py \ --sast-report gl-sast-report.json \ --custom-rules "$[[ inputs.custom_rules ]]" \ --output security-enhanced.json fi - | CRITICAL_COUNT=$(jq '[.vulnerabilities[] | select(.severity == "Critical")] | length' security-enhanced.json) if [ "$CRITICAL_COUNT" -gt 0 ] && [ "$[[ inputs.block_on_critical ]]" = "true" ]; then echo "BLOCKING: $CRITICAL_COUNT critical vulnerabilities found" exit 1 fi artifacts: reports: sast: security-enhanced.json
Advanced Patterns
Multi-Stage Agent Workflow
stages: - analyze - plan - implement - review - deploy # Stage 1: Analyze requirements analyze_requirements: stage: analyze script: - | claude chat --prompt "Analyze requirements in requirements.md and: 1. Identify ambiguities 2. Suggest clarifications 3. Estimate complexity " --output json > analysis.json artifacts: paths: - analysis.json # Stage 2: Create implementation plan create_plan: stage: plan needs: [analyze_requirements] script: - | glab duo agent run planner \ --input analysis.json \ --output plan.json artifacts: paths: - plan.json # Stage 3: Generate implementation implement: stage: implement needs: [create_plan] script: - | python << EOF from openai_agents import Agent import json with open('plan.json') as f: plan = json.load(f) implementer = Agent( name="implementer", model="gpt-4-turbo" ) for task in plan['tasks']: result = implementer.run( prompt=f"Implement: {task['description']}", context=task ) # Save generated code with open(task['output_file'], 'w') as f: f.write(result.code) EOF - git add src/ - git commit -m "feat: AI-generated implementation" artifacts: paths: - src/ # Stage 4: AI code review review_code: stage: review needs: [implement] script: - glab duo agent run security_analyst - glab duo agent run code_reviewer artifacts: reports: codequality: review-report.json # Stage 5: Deploy if approved deploy: stage: deploy needs: [review_code] script: - ./deploy.sh rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual
Conditional Agent Execution
# Run expensive AI agents only when needed smart_review: stage: review script: - | # Calculate change size LINES_CHANGED=$(git diff --stat origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD | tail -1 | awk '{print $4}') if [ "$LINES_CHANGED" -lt 100 ]; then echo "Small change, using fast review" claude chat --model claude-haiku-4.0 --prompt "Quick review of small change" elif [ "$LINES_CHANGED" -lt 500 ]; then echo "Medium change, using standard review" claude chat --model claude-sonnet-4.5 --prompt "Standard code review" else echo "Large change, using comprehensive review" glab duo flow run code_review --comprehensive fi rules: - if: $CI_MERGE_REQUEST_ID
Agent Result Caching
cached_analysis: stage: analyze script: - | # Generate cache key from relevant files CACHE_KEY=$(find src/ -type f -exec md5sum {} \; | md5sum | cut -d' ' -f1) # Check if analysis exists if glab api "projects/$CI_PROJECT_ID/repository/files/cache%2F$CACHE_KEY.json" > /dev/null 2>&1; then echo "Using cached analysis" glab api "projects/$CI_PROJECT_ID/repository/files/cache%2F$CACHE_KEY.json/raw" > analysis.json else echo "Running new analysis" claude chat --prompt "Analyze codebase" --output json > analysis.json # Cache result glab api --method POST "projects/$CI_PROJECT_ID/repository/files/cache%2F$CACHE_KEY.json" \ -f "branch=main" \ -f "content=$(base64 -w0 analysis.json)" \ -f "commit_message=Cache analysis $CACHE_KEY" fi artifacts: paths: - analysis.json
Performance Optimization
Token Usage Optimization
# Optimize prompts for token efficiency efficient_review: stage: review script: - | # Only send changed lines, not entire files git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD > changes.diff # Compress context python << EOF import anthropic with open('changes.diff') as f: diff = f.read() # Truncate large diffs if len(diff) > 50000: diff = diff[:50000] + "\n... (truncated)" client = anthropic.Anthropic() response = client.messages.create( model='claude-sonnet-4.5', max_tokens=2048, # Limit output messages=[{ 'role': 'user', 'content': f'Brief review:\n{diff}' }] ) print(response.content[0].text) EOF
Parallel Agent Execution
# Run multiple agents in parallel parallel_analysis: stage: analyze parallel: matrix: - AGENT: [security, quality, performance, documentation] script: - | case $AGENT in security) glab duo agent run security_analyst -o security.json ;; quality) glab duo agent run code_reviewer -o quality.json ;; performance) claude chat --prompt "Analyze performance" -o performance.json ;; documentation) claude chat --prompt "Check documentation" -o docs.json ;; esac artifacts: paths: - "*.json" # Aggregate results aggregate_results: stage: review needs: [parallel_analysis] script: - | python << EOF import json import glob results = {} for file in glob.glob('*.json'): with open(file) as f: results[file.replace('.json', '')] = json.load(f) # Generate summary with open('summary.json', 'w') as f: json.dump(results, f, indent=2) EOF artifacts: paths: - summary.json
Security & Compliance
Secure API Key Management
# Use OIDC tokens instead of long-lived keys secure_agent_execution: id_tokens: GITLAB_OIDC_TOKEN: aud: https://gitlab.com ANTHROPIC_OIDC_TOKEN: aud: https://api.anthropic.com script: - export ANTHROPIC_API_KEY=$ANTHROPIC_OIDC_TOKEN - claude chat --prompt "Secure execution"
Audit Logging
audited_agent: before_script: - export AGENT_EXECUTION_ID=$(uuidgen) - | echo "Starting agent execution: $AGENT_EXECUTION_ID" | \ glab api --method POST "projects/$CI_PROJECT_ID/issues" \ -f "title=Agent Execution Log" \ -f "description=Agent: code_reviewer, ID: $AGENT_EXECUTION_ID" script: - claude chat --prompt "Review code" > review.txt after_script: - | # Log completion TOKENS_USED=$(jq '.usage.total_tokens' review.json) COST=$(python -c "print($TOKENS_USED * 0.000003)") glab api --method POST "projects/$CI_PROJECT_ID/issues/comments" \ -f "body=Completed: $AGENT_EXECUTION_ID, Tokens: $TOKENS_USED, Cost: \$$COST"
Compliance Checks
compliance_gate: stage: compliance script: - | # Ensure agent adheres to policies python << EOF import json with open('agent-execution.json') as f: execution = json.load(f) # Check token usage limits if execution['tokens_used'] > 100000: print("ERROR: Token usage exceeds limit") exit(1) # Check for sensitive data exposure if 'api_key' in execution['output'].lower(): print("ERROR: Potential secret exposure") exit(1) # Check execution time if execution['duration'] > 600: print("WARNING: Execution exceeded time limit") EOF
Cost Management
Budget Controls
budget_check: before_script: - | # Check monthly budget MONTHLY_COST=$(glab api "projects/$CI_PROJECT_ID/variables/AGENT_COST_$(date +%Y-%m)" | jq -r '.value') BUDGET_LIMIT=1000 # $1000/month if (( $(echo "$MONTHLY_COST > $BUDGET_LIMIT" | bc -l) )); then echo "Budget exceeded: \$$MONTHLY_COST / \$$BUDGET_LIMIT" exit 1 fi script: - claude chat --prompt "Analysis"
Cost Tracking
track_costs: after_script: - | # Calculate cost TOKENS=$(jq '.usage.total_tokens' response.json) COST=$(python -c "print($TOKENS * 0.000015)") # $15/million tokens # Update monthly total CURRENT=$(glab api "projects/$CI_PROJECT_ID/variables/AGENT_COST_$(date +%Y-%m)" | jq -r '.value' || echo "0") NEW_TOTAL=$(python -c "print($CURRENT + $COST)") glab api --method PUT "projects/$CI_PROJECT_ID/variables/AGENT_COST_$(date +%Y-%m)" \ -f "value=$NEW_TOTAL"
Monitoring & Observability
OpenTelemetry Integration
traced_execution: before_script: - pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp script: - | python << EOF from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter # Setup tracing trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) otlp_exporter = OTLPSpanExporter( endpoint="https://otel-collector.gitlab.com" ) span_processor = BatchSpanProcessor(otlp_exporter) trace.get_tracer_provider().add_span_processor(span_processor) # Trace agent execution with tracer.start_as_current_span("agent_code_review"): # Execute agent from anthropic import Anthropic client = Anthropic() with tracer.start_as_current_span("api_call"): response = client.messages.create( model='claude-sonnet-4.5', messages=[{'role': 'user', 'content': 'Review code'}] ) print(response.content[0].text) EOF
Complete Production Example
Full featured CI/CD pipeline with agents:
# .gitlab-ci.yml workflow: rules: - if: $CI_MERGE_REQUEST_ID - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH stages: - validate - analyze - test - security - review - deploy # Budget check budget_gate: stage: .pre script: - ./scripts/check-budget.sh # Quick validation validate: stage: validate script: - npm run lint - npm run typecheck # AI code analysis analyze_code: stage: analyze needs: [validate] parallel: matrix: - ANALYSIS: [quality, complexity, documentation] script: - glab duo agent run analyzer --type $ANALYSIS -o $ANALYSIS.json artifacts: paths: - "*.json" # AI test generation generate_tests: stage: test needs: [analyze_code] script: - python agents/test_generator.py - pytest tests/ --cov=src --cov-report=json coverage: '/TOTAL.*\s+(\d+%)$/' artifacts: reports: coverage_report: coverage_format: cobertura path: coverage.xml # Security analysis security_scan: stage: security include: - template: Security/SAST.gitlab-ci.yml script: - glab duo agent run security_analyst --enhance-sast artifacts: reports: sast: gl-sast-report-enhanced.json # Comprehensive review ai_review: stage: review needs: [analyze_code, generate_tests, security_scan] script: - | # Aggregate all analysis results python << EOF import json results = { 'quality': json.load(open('quality.json')), 'complexity': json.load(open('complexity.json')), 'security': json.load(open('gl-sast-report-enhanced.json')), 'coverage': json.load(open('coverage.json')) } # Final review with all context from anthropic import Anthropic client = Anthropic() response = client.messages.create( model='claude-sonnet-4.5', max_tokens=8192, messages=[{ 'role': 'user', 'content': f'''Comprehensive MR review: Analysis: {json.dumps(results, indent=2)} Provide: 1. Overall assessment 2. Approval recommendation 3. Action items ''' }] ) with open('final-review.md', 'w') as f: f.write(response.content[0].text) EOF - glab mr note $CI_MERGE_REQUEST_IID --message "$(cat final-review.md)" artifacts: paths: - final-review.md # Deploy deploy_production: stage: deploy script: - ./deploy.sh environment: name: production rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual
Resources
GitLab Documentation
- CI/CD Components: https://docs.gitlab.com/ee/ci/components/
- CI/CD Variables: https://docs.gitlab.com/ee/ci/variables/
- GitLab Duo in CI/CD: https://docs.gitlab.com/ee/user/gitlab_duo/ci_cd.html
Best Practices
- Pipeline Efficiency: https://docs.gitlab.com/ee/ci/pipelines/pipeline_efficiency.html
- Security Best Practices: https://docs.gitlab.com/ee/ci/security/
- Cost Optimization: https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html
Next Steps
- Start Simple: Add single agent to existing pipeline
- Measure Impact: Track time savings and bug detection
- Iterate: Expand to more stages and agents
- Optimize: Reduce token usage and costs
- Scale: Create reusable components for team
Related Documentation: