Skip to content

Webhooks

Guide to configuring GitLab webhooks for real-time notifications.

Overview

sequenceDiagram
    participant Dev as Developer
    participant GL as GitLab
    participant GP as GitPulse Webhook
    participant Q as Queue
    participant W as Worker

    Dev->>GL: git push
    GL->>GL: Process push
    GL->>GP: POST /webhooks/gitlab
    Note over GP: Verify signature
    GP->>Q: Enqueue event
    GP-->>GL: 200 OK (fast)
    W->>Q: Dequeue
    W->>W: Process & store

Configuration in GitLab

At the project level

  1. Go to the project in GitLab
  2. Settings -> Webhooks
  3. Complete the configuration:
Field The value
URL https://gitpulse.kpi.fei.tuke.sk/api/v1/webhooks/gitlab
Secret token Generated secret (see below)
Trigger Push, MR, Issues, Pipelines
SSL verification Yes Enabled

At the group level

For bulk configuration of all projects:

  1. Go to the group
  2. Settings -> Webhooks
  3. Same configuration as above

Group webhooks

Group webhooks automatically apply to all projects in the group.

Generation of webhook secret

Bash
1
2
3
4
5
6
# Generate a secure secret
WEBHOOK_SECRET=$(openssl rand -hex 32)
echo "GITLAB_WEBHOOK_SECRET=${WEBHOOK_SECRET}"

# Add to .env
echo "GITLAB_WEBHOOK_SECRET=${WEBHOOK_SECRET}" >> .env

Trigger events

Push Events

JSON
{
  "object_kind": "push",
  "event_name": "push",
  "before": "abc123...",
  "after": "def456...",
  "ref": "refs/heads/main",
  "checkout_sha": "def456...",
  "user_name": "John Doe",
  "user_email": "john@example.com",
  "project": {
    "id": 123,
    "name": "my-project",
    "path_with_namespace": "group/my-project"
  },
  "commits": [
    {
      "id": "def456...",
      "message": "feat: add feature X",
      "timestamp": "2024-01-15T10:30:00Z",
      "author": {
        "name": "John Doe",
        "email": "john@example.com"
      },
      "added": ["new_file.py"],
      "modified": ["existing.py"],
      "removed": []
    }
  ],
  "total_commits_count": 1
}

Processed metrics: - Number of commits - Author (mapping to team member) - Added/modified/deleted files - Time distribution

Merge Request Events

JSON
{
  "object_kind": "merge_request",
  "event_type": "merge_request",
  "user": {
    "id": 1,
    "username": "johndoe"
  },
  "object_attributes": {
    "iid": 42,
    "title": "Feature: User authentication",
    "description": "Implements OAuth2 login",
    "state": "merged",
    "source_branch": "feature/auth",
    "target_branch": "main",
    "author_id": 1,
    "assignee_id": 2,
    "created_at": "2024-01-10T10:00:00Z",
    "updated_at": "2024-01-15T14:30:00Z",
    "action": "merge"
  },
  "changes": {
    "state": {
      "previous": "opened",
      "current": "merged"
    }
  }
}

Processed metrics: - MR creation/merge rate - Time to merge - Review participation - Branch activity

Issue Events

JSON
{
  "object_kind": "issue",
  "event_type": "issue",
  "user": {
    "id": 1,
    "username": "johndoe"
  },
  "object_attributes": {
    "iid": 15,
    "title": "Bug: Login fails on mobile",
    "state": "closed",
    "action": "close",
    "labels": [
      {"title": "bug"},
      {"title": "priority:high"}
    ]
  }
}

Pipeline Events

JSON
{
  "object_kind": "pipeline",
  "object_attributes": {
    "id": 789,
    "status": "success",
    "duration": 245,
    "created_at": "2024-01-15T10:00:00Z",
    "finished_at": "2024-01-15T10:04:05Z"
  },
  "builds": [
    {
      "id": 1001,
      "stage": "build",
      "name": "compile",
      "status": "success",
      "duration": 60
    },
    {
      "id": 1002,
      "stage": "test",
      "name": "unit-tests",
      "status": "success",
      "duration": 120
    }
  ]
}

Webhook signature verification

GitPulse verifies each webhook using a secret token:

Python
# src/app/api/webhooks.py
import hmac
import hashlib

def verify_gitlab_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Verify GitLab webhook signature."""
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(f"sha256={expected}", signature)

GitLab sends a signature in the header:

Text Only
X-Gitlab-Token: <secret>

Webhook handler

Python
# src/app/api/webhooks.py
from fastapi import APIRouter, Request, HTTPException, Header
from app.config import settings
from app.workers.event_processor import process_gitlab_event

router = APIRouter()


@router.post("/webhooks/gitlab")
async def gitlab_webhook(
    request: Request,
    x_gitlab_token: str = Header(None),
    x_gitlab_event: str = Header(None),
):
    """Handle GitLab webhook events."""

    # 1. Verify token
    if x_gitlab_token != settings.GITLAB_WEBHOOK_SECRET:
        raise HTTPException(status_code=401, detail="Invalid token")

    # 2. Parse payload
    payload = await request.json()

    # 3. Enqueue for async processing
    job = process_gitlab_event.delay(
        event_type=x_gitlab_event,
        payload=payload
    )

    # 4. Return immediately
    return {"status": "accepted", "job_id": str(job.id)}

Testing webhooks

Manual test

Bash
# Simulate a push event
curl -X POST http://localhost:8000/api/v1/webhooks/gitlab \
  -H "Content-Type: application/json" \
  -H "X-Gitlab-Token: ${GITLAB_WEBHOOK_SECRET}" \
  -H "X-Gitlab-Event: Push Hook" \
  -d '{
    "object_kind": "push",
    "ref": "refs/heads/main",
    "commits": [{
      "id": "test123",
      "message": "Test commit",
      "author": {"name": "Test", "email": "test@example.com"}
    }]
  }'

GitLab webhook test

  1. In GitLab webhooks settings
  2. Click Test on the saved webhook
  3. Select event type
  4. Check the response and GitPulse logs

Ngrok for local development

Bash
1
2
3
4
5
# Start an ngrok tunnel
ngrok http 8000

# Use the generated URL in GitLab
# https://abc123.ngrok.io/api/v1/webhooks/gitlab

Troubleshooting

Webhook not delivered

Bash
1
2
3
4
5
6
7
8
# 1. Check GitLab webhook logs
# Project -> Settings -> Webhooks -> Recent deliveries

# 2. Verify GitPulse availability
curl -I https://gitpulse.kpi.fei.tuke.sk/api/v1/health

# 3. Check the firewall
sudo ufw status

401 Unauthorized

Bash
1
2
3
4
5
# Token mismatch
# 1. Verify GITLAB_WEBHOOK_SECRET in .env
# 2. Verify Secret token in GitLab webhook settings
# 3. Restart GitPulse
docker compose restart api

500 Server Error

Bash
1
2
3
4
5
6
7
# Check GitPulse logs
docker compose logs api | grep webhook

# Common causes:
# - Malformed JSON payload
# - Database connection error
# - Redis unavailable

Duplicate events

Python
1
2
3
4
5
6
# GitPulse uses an idempotency key
# An event with the same ID is processed only once

# If you see duplicates, check:
# 1. Whether webhooks are configured at both project and group level
# 2. Retry logic in GitLab

Webhook monitoring

Prometheus metrics

PromQL
# Webhook request rate
rate(gitlab_webhook_requests_total[5m])

# Error rate
sum(rate(gitlab_webhook_requests_total{status="error"}[5m])) /
sum(rate(gitlab_webhook_requests_total[5m]))

# Processing latency
histogram_quantile(0.99, 
  rate(gitlab_webhook_duration_seconds_bucket[5m])
)

Alerting

YAML
# monitoring/prometheus/alerts/webhooks.yml
groups:
  - name: webhooks
    rules:
      - alert: WebhookHighErrorRate
        expr: |
          sum(rate(gitlab_webhook_requests_total{status="error"}[5m])) /
          sum(rate(gitlab_webhook_requests_total[5m])) > 0.1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High webhook error rate"

Security

IP Whitelist

Nginx Configuration File
1
2
3
4
5
6
7
# Caddy/Nginx - allow only GitLab IPs
location /api/v1/webhooks/ {
    allow 10.0.0.0/8;      # GitLab server IP range
    deny all;

    proxy_pass http://api:8000;
}

Rate limiting

Python
# src/app/api/webhooks.py
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@router.post("/webhooks/gitlab")
@limiter.limit("100/minute")
async def gitlab_webhook(...):
    ...

Further reading