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
- Go to the project in GitLab
- Settings -> Webhooks
- 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:
- Go to the group
- Settings -> Webhooks
- Same configuration as above
Group webhooks
Group webhooks automatically apply to all projects in the group.
Generation of webhook secret
| Bash |
|---|
| # 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:
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
- In GitLab webhooks settings
- Click Test on the saved webhook
- Select event type
- Check the response and GitPulse logs
Ngrok for local development
| Bash |
|---|
| # 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. 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 |
|---|
| # 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 |
|---|
| # Check GitPulse logs
docker compose logs api | grep webhook
# Common causes:
# - Malformed JSON payload
# - Database connection error
# - Redis unavailable
|
Duplicate events
| Python |
|---|
| # 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 |
|---|
| # 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