Skip to content

CI/CD integration

A guide to integrating GitPulse with GitLab CI/CD pipelines.

Overview

graph LR
    subgraph "GitLab CI"
        PIPE["▶Pipeline"]
        BUILD["Build"]
        TEST["Test"]
        DEPLOY["Deploy"]
    end

    subgraph "GitPulse"
        WEBHOOK["Webhook"]
        METRICS["Metrics"]
        DASH["Dashboard"]
    end

    PIPE --> BUILD --> TEST --> DEPLOY
    BUILD --> |status| WEBHOOK
    TEST --> |results| WEBHOOK
    DEPLOY --> |status| WEBHOOK
    WEBHOOK --> METRICS
    METRICS --> DASH

Pipeline metrics

GitPulse tracks the following CI/CD metrics:

Metric Description Calculation
Pipeline Success Rate % of successful pipelines success / total
Mean Time to Recovery Time to fix crash Average time between failure and success
Pipeline Duration Pipeline duration Time from start to finish
Build Frequency Number of builds per day Count per day
Test Coverage Trend Development of test coverage Coverage from JUnit reports

GitLab CI configuration

Basic .gitlab-ci.yml

YAML
# .gitlab-ci.yml
stages:
  - build
  - test
  - analyze
  - deploy

variables:
  DOCKER_DRIVER: overlay2

# === BUILD ===
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main
    - merge_requests

# === TEST ===
test:
  stage: test
  image: python:3.12-slim
  script:
    - pip install -e ".[dev]"
    - pytest --junitxml=report.xml --cov=app --cov-report=xml
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      junit: report.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

# === ANALYZE ===
lint:
  stage: analyze
  image: python:3.12-slim
  script:
    - pip install ruff
    - ruff check . --output-format=gitlab > ruff-report.json
  artifacts:
    reports:
      codequality: ruff-report.json

security-scan:
  stage: analyze
  image: python:3.12-slim
  script:
    - pip install bandit safety
    - bandit -r src -f json -o bandit-report.json || true
    - safety check --json > safety-report.json || true
  artifacts:
    paths:
      - bandit-report.json
      - safety-report.json

# === DEPLOY ===
deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.gitpulse.kpi.fei.tuke.sk
  script:
    - echo "Deploying to staging..."
  only:
    - main

GitPulse-specific pipeline checks

YAML
# .gitlab-ci.yml (continued)

# === GITPULSE INTEGRATION ===
gitpulse-report:
  stage: analyze
  image: curlimages/curl:latest
  script:
    # Notify GitPulse about the pipeline
    - |
      curl -X POST "${GITPULSE_URL}/api/v1/pipelines/report" \
        -H "Authorization: Bearer ${GITPULSE_TOKEN}" \
        -H "Content-Type: application/json" \
        -d '{
          "project_id": "'${CI_PROJECT_ID}'",
          "pipeline_id": "'${CI_PIPELINE_ID}'",
          "status": "'${CI_JOB_STATUS}'",
          "commit_sha": "'${CI_COMMIT_SHA}'",
          "coverage": "'${CI_JOB_COVERAGE:-0}'"
        }'
  when: always  # Run even on failures

Pipeline Events

Webhook payload

GitLab sends pipeline events automatically:

JSON
{
  "object_kind": "pipeline",
  "object_attributes": {
    "id": 12345,
    "iid": 42,
    "ref": "main",
    "sha": "abc123def456...",
    "status": "success",
    "detailed_status": "passed",
    "stages": ["build", "test", "deploy"],
    "created_at": "2024-01-15T10:00:00Z",
    "finished_at": "2024-01-15T10:15:30Z",
    "duration": 930,
    "queued_duration": 10
  },
  "builds": [
    {
      "id": 100,
      "stage": "build",
      "name": "build",
      "status": "success",
      "duration": 120,
      "runner": {
        "description": "shared-runner-1"
      }
    },
    {
      "id": 101,
      "stage": "test",
      "name": "test",
      "status": "success",
      "duration": 300,
      "coverage": 85.5
    }
  ],
  "project": {
    "id": 123,
    "path_with_namespace": "kpi/tp-2024/team-alpha"
  }
}

GitPulse processing

Python
# src/app/services/pipeline_service.py
from dataclasses import dataclass
from datetime import datetime


@dataclass
class PipelineMetrics:
    success_rate: float
    avg_duration: float
    mttr: float  # Mean Time To Recovery


class PipelineService:
    async def process_pipeline_event(self, event: dict) -> None:
        """Process GitLab pipeline webhook."""
        attrs = event["object_attributes"]

        # Store pipeline record
        pipeline = await self.repository.create({
            "gitlab_id": attrs["id"],
            "project_id": event["project"]["id"],
            "status": attrs["status"],
            "duration": attrs["duration"],
            "started_at": attrs["created_at"],
            "finished_at": attrs["finished_at"],
            "stages": attrs["stages"],
        })

        # Store build records
        for build in event.get("builds", []):
            await self.build_repository.create({
                "pipeline_id": pipeline.id,
                "stage": build["stage"],
                "name": build["name"],
                "status": build["status"],
                "duration": build.get("duration"),
                "coverage": build.get("coverage"),
            })

        # Update metrics
        await self.update_team_metrics(pipeline)

Dashboard display

Pipeline health widget

graph TD
    subgraph "Pipeline Health (Team Alpha)"
        SR["Success Rate: 92%"]
        DUR["⏱Avg Duration: 8m 30s"]
        MTTR["MTTR: 45m"]
        FREQ["Builds/day: 12"]
    end

Trend graph

Python
# API endpoint for pipeline trends
@router.get("/teams/{team_id}/pipelines/trends")
async def get_pipeline_trends(
    team_id: int,
    days: int = 30,
    db: AsyncSession = Depends(get_db)
):
    """Get pipeline metrics trends."""
    service = PipelineService(db)

    return {
        "success_rate": await service.get_success_rate_trend(team_id, days),
        "duration": await service.get_duration_trend(team_id, days),
        "frequency": await service.get_build_frequency(team_id, days),
    }

CI/CD Best Practices for Students

Assessed aspects

GitPulse evaluates the following CI/CD practices:

Aspect Point rating
Pipeline exists Yes Basic
Automated tests Yes Basic
Code quality checks Yes Recommended
Security scanning Bonus
Coverage > 60% Bonus
Multi-stage pipeline Bonus

Example sample pipeline

YAML
# Ideal student pipeline
stages:
  - validate
  - build
  - test
  - quality
  - deploy

# Quick validation
validate:
  stage: validate
  script:
    - python -m py_compile src/**/*.py

# Build Docker image
build:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE .

# Unit tests with coverage
test:
  stage: test
  script:
    - pytest --cov=src --cov-report=term --cov-fail-under=60
  coverage: '/TOTAL.*\s+(\d+%)$/'

# Linting
lint:
  stage: quality
  script:
    - ruff check src/
    - ruff format --check src/

# Deploy to staging (main branch only)
deploy:
  stage: deploy
  script:
    - echo "Deploying..."
  environment:
    name: production
  only:
    - main

Troubleshooting

Pipeline status is not in GitPulse

Bash
1
2
3
4
5
6
7
8
9
# 1. Verify webhook configuration
# GitLab -> Settings -> Webhooks
# Trigger: Pipeline events

# 2. Check webhook logs
# GitLab -> Settings -> Webhooks -> Recent deliveries

# 3. GitPulse logs
docker compose logs api | grep pipeline

Coverage is not reported

YAML
1
2
3
4
5
6
7
8
# Make sure you have the correct coverage regex
test:
  script:
    - pytest --cov=app --cov-report=term
  coverage: '/TOTAL.*\s+(\d+%)$/'  # <- This regex must match the output

# Pytest output must contain:
# TOTAL    100    10    90%

Build failing in GitPulse but not in GitLab

Bash
1
2
3
4
5
# GitPulse may have stricter checks
# Check GitPulse validation:
curl "${GITPULSE_URL}/api/v1/pipelines/validate" \
  -H "Authorization: Bearer ${GITPULSE_TOKEN}" \
  -d '{"gitlab_ci_yml": "..."}'

Integration with rubric

Python
# Rubric criterion for CI/CD
ci_cd_criterion = RubricCriterion(
    name="CI/CD Pipeline",
    max_points=10,
    checks=[
        Check("pipeline_exists", points=2, description="Pipeline is configured"),
        Check("tests_automated", points=3, description="Automated tests"),
        Check("coverage_threshold", points=2, description="Coverage > 60%"),
        Check("linting", points=2, description="Code quality checks"),
        Check("security_scan", points=1, description="Security scanning"),
    ]
)

Further reading