Files
2025-11-30 08:38:11 +08:00

20 KiB

GitLab Webhooks Reference

Overview

GitLab webhooks allow external services to be notified when certain events happen in GitLab. When triggered, GitLab sends an HTTP POST request with event data to the configured URL.

Webhook Configuration

Creating a Webhook

Via UI: Project Settings > Webhooks

Via API:

POST /projects/:id/hooks

Parameters:

  • url (required): The webhook URL
  • token: Secret token for request verification
  • push_events: Trigger on push events (default: true)
  • tag_push_events: Trigger on tag push events
  • issues_events: Trigger on issue events
  • confidential_issues_events: Trigger on confidential issue events
  • merge_requests_events: Trigger on merge request events
  • wiki_page_events: Trigger on wiki page events
  • deployment_events: Trigger on deployment events
  • job_events: Trigger on job events
  • pipeline_events: Trigger on pipeline events
  • releases_events: Trigger on release events
  • enable_ssl_verification: Verify SSL certificates (default: true)

Webhook Security

Secret Token: Include a secret token to verify requests:

import hmac
import hashlib

def verify_webhook(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Request Headers:

  • X-Gitlab-Token: Contains the secret token (if configured)
  • X-Gitlab-Event: Event type name
  • User-Agent: GitLab/{version}

Event Types

Push Events

Trigger: Code pushed to repository

Event Header: X-Gitlab-Event: Push Hook

Payload Structure:

{
  "object_kind": "push",
  "event_name": "push",
  "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
  "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  "ref": "refs/heads/main",
  "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  "user_id": 4,
  "user_name": "John Doe",
  "user_username": "jdoe",
  "user_email": "john@example.com",
  "user_avatar": "https://gitlab.com/uploads/user/avatar/4/avatar.jpg",
  "project_id": 15,
  "project": {
    "id": 15,
    "name": "My Project",
    "description": "Project description",
    "web_url": "https://gitlab.com/namespace/project",
    "avatar_url": null,
    "git_ssh_url": "git@gitlab.com:namespace/project.git",
    "git_http_url": "https://gitlab.com/namespace/project.git",
    "namespace": "Namespace",
    "visibility_level": 0,
    "path_with_namespace": "namespace/project",
    "default_branch": "main"
  },
  "commits": [
    {
      "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "message": "Fix bug in authentication",
      "title": "Fix bug in authentication",
      "timestamp": "2025-01-15T12:30:00+00:00",
      "url": "https://gitlab.com/namespace/project/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "author": {
        "name": "John Doe",
        "email": "john@example.com"
      },
      "added": ["new-file.txt"],
      "modified": ["existing-file.txt"],
      "removed": ["old-file.txt"]
    }
  ],
  "total_commits_count": 1,
  "repository": {
    "name": "My Project",
    "url": "git@gitlab.com:namespace/project.git",
    "description": "Project description",
    "homepage": "https://gitlab.com/namespace/project"
  }
}

Tag Push Events

Trigger: Tag created or deleted

Event Header: X-Gitlab-Event: Tag Push Hook

Payload Structure:

{
  "object_kind": "tag_push",
  "event_name": "tag_push",
  "before": "0000000000000000000000000000000000000000",
  "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
  "ref": "refs/tags/v1.0.0",
  "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
  "user_id": 4,
  "user_name": "John Doe",
  "user_avatar": "https://gitlab.com/uploads/user/avatar/4/avatar.jpg",
  "project_id": 15,
  "project": { /* project details */ },
  "commits": [ /* commit details */ ],
  "total_commits_count": 0,
  "repository": { /* repository details */ }
}

Note: before is all zeros for tag creation, after is all zeros for tag deletion.

Issue Events

Trigger: Issue created, updated, closed, or reopened

Event Header: X-Gitlab-Event: Issue Hook

Payload Structure:

{
  "object_kind": "issue",
  "event_type": "issue",
  "user": {
    "id": 4,
    "name": "John Doe",
    "username": "jdoe",
    "avatar_url": "https://gitlab.com/uploads/user/avatar/4/avatar.jpg",
    "email": "john@example.com"
  },
  "project": { /* project details */ },
  "object_attributes": {
    "id": 301,
    "title": "Bug in login feature",
    "assignee_ids": [5, 6],
    "assignee_id": 5,
    "author_id": 4,
    "project_id": 15,
    "created_at": "2025-01-15 12:30:00 UTC",
    "updated_at": "2025-01-15 13:00:00 UTC",
    "position": 0,
    "branch_name": null,
    "description": "Login page crashes when...",
    "milestone_id": 10,
    "state": "opened",
    "state_id": 1,
    "iid": 23,
    "url": "https://gitlab.com/namespace/project/-/issues/23",
    "action": "open",
    "labels": [
      {
        "id": 100,
        "title": "bug",
        "color": "#FF0000",
        "project_id": 15,
        "created_at": "2024-01-01 00:00:00 UTC",
        "updated_at": "2024-01-01 00:00:00 UTC"
      }
    ]
  },
  "assignees": [
    {
      "id": 5,
      "name": "Jane Smith",
      "username": "jsmith",
      "avatar_url": "https://gitlab.com/uploads/user/avatar/5/avatar.jpg"
    }
  ],
  "labels": [ /* label details */ ],
  "changes": {
    "updated_at": {
      "previous": "2025-01-15 12:30:00 UTC",
      "current": "2025-01-15 13:00:00 UTC"
    },
    "title": {
      "previous": "Old title",
      "current": "Bug in login feature"
    }
  }
}

Action Values: open, update, close, reopen

Merge Request Events

Trigger: MR created, updated, merged, or closed

Event Header: X-Gitlab-Event: Merge Request Hook

Payload Structure:

{
  "object_kind": "merge_request",
  "event_type": "merge_request",
  "user": { /* user details */ },
  "project": { /* project details */ },
  "object_attributes": {
    "id": 99,
    "iid": 1,
    "target_branch": "main",
    "source_branch": "feature-branch",
    "source_project_id": 15,
    "author_id": 4,
    "assignee_ids": [5],
    "assignee_id": 5,
    "reviewer_ids": [6, 7],
    "title": "Add new authentication feature",
    "created_at": "2025-01-15 12:00:00 UTC",
    "updated_at": "2025-01-15 14:00:00 UTC",
    "milestone_id": 10,
    "state": "opened",
    "state_id": 1,
    "merge_status": "can_be_merged",
    "target_project_id": 15,
    "description": "This MR adds...",
    "url": "https://gitlab.com/namespace/project/-/merge_requests/1",
    "source": { /* source project */ },
    "target": { /* target project */ },
    "last_commit": {
      "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "message": "Update authentication",
      "timestamp": "2025-01-15T13:30:00+00:00",
      "url": "https://gitlab.com/namespace/project/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
      "author": {
        "name": "John Doe",
        "email": "john@example.com"
      }
    },
    "work_in_progress": false,
    "draft": false,
    "action": "open",
    "assignee": { /* assignee details */ },
    "labels": [ /* label details */ ]
  },
  "labels": [ /* label details */ ],
  "changes": { /* changed attributes */ },
  "assignees": [ /* assignee details */ ],
  "reviewers": [ /* reviewer details */ ]
}

Action Values: open, update, close, reopen, merge, approved, unapproved

Pipeline Events

Trigger: Pipeline status changes

Event Header: X-Gitlab-Event: Pipeline Hook

Payload Structure:

{
  "object_kind": "pipeline",
  "object_attributes": {
    "id": 31,
    "ref": "main",
    "tag": false,
    "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
    "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
    "source": "push",
    "status": "success",
    "detailed_status": "passed",
    "stages": ["build", "test", "deploy"],
    "created_at": "2025-01-15 12:00:00 UTC",
    "finished_at": "2025-01-15 12:15:00 UTC",
    "duration": 900,
    "queued_duration": 10,
    "variables": [
      {
        "key": "ENVIRONMENT",
        "value": "production"
      }
    ]
  },
  "merge_request": {
    "id": 1,
    "iid": 1,
    "title": "Add feature",
    "source_branch": "feature-branch",
    "source_project_id": 15,
    "target_branch": "main",
    "target_project_id": 15,
    "state": "opened",
    "merge_status": "can_be_merged",
    "url": "https://gitlab.com/namespace/project/-/merge_requests/1"
  },
  "user": { /* user details */ },
  "project": { /* project details */ },
  "commit": {
    "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
    "message": "Add new feature",
    "title": "Add new feature",
    "timestamp": "2025-01-15T11:55:00+00:00",
    "url": "https://gitlab.com/namespace/project/-/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
    "author": {
      "name": "John Doe",
      "email": "john@example.com"
    }
  },
  "builds": [
    {
      "id": 380,
      "stage": "test",
      "name": "unit-tests",
      "status": "success",
      "created_at": "2025-01-15 12:00:00 UTC",
      "started_at": "2025-01-15 12:01:00 UTC",
      "finished_at": "2025-01-15 12:05:00 UTC",
      "duration": 240,
      "queued_duration": 60,
      "when": "on_success",
      "manual": false,
      "allow_failure": false,
      "user": { /* user details */ },
      "runner": {
        "id": 1,
        "description": "runner-1",
        "active": true,
        "runner_type": "project_type",
        "is_shared": false,
        "tags": ["docker", "linux"]
      },
      "artifacts_file": {
        "filename": "artifacts.zip",
        "size": 1024000
      },
      "environment": null
    }
  ]
}

Status Values: pending, running, success, failed, canceled, skipped

Job Events

Trigger: Job status changes

Event Header: X-Gitlab-Event: Job Hook

Payload Structure:

{
  "object_kind": "build",
  "ref": "main",
  "tag": false,
  "before_sha": "95790bf891e76fee5e1747ab589903a6a1f80f22",
  "sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  "build_id": 380,
  "build_name": "unit-tests",
  "build_stage": "test",
  "build_status": "success",
  "build_created_at": "2025-01-15 12:00:00 UTC",
  "build_started_at": "2025-01-15 12:01:00 UTC",
  "build_finished_at": "2025-01-15 12:05:00 UTC",
  "build_duration": 240,
  "build_queued_duration": 60,
  "build_allow_failure": false,
  "build_failure_reason": "unknown_failure",
  "pipeline_id": 31,
  "runner": {
    "id": 1,
    "description": "runner-1",
    "runner_type": "project_type",
    "active": true,
    "is_shared": false,
    "tags": ["docker", "linux"]
  },
  "project_id": 15,
  "project_name": "My Project",
  "user": { /* user details */ },
  "commit": { /* commit details */ },
  "repository": { /* repository details */ },
  "environment": {
    "name": "production",
    "action": "start",
    "deployment_tier": "production"
  }
}

Build Status Values: pending, running, success, failed, canceled

Deployment Events

Trigger: Deployment created or status changes

Event Header: X-Gitlab-Event: Deployment Hook

Payload Structure:

{
  "object_kind": "deployment",
  "status": "success",
  "status_changed_at": "2025-01-15 12:20:00 UTC",
  "deployment_id": 15,
  "deployable_id": 380,
  "deployable_url": "https://gitlab.com/namespace/project/-/jobs/380",
  "environment": "production",
  "environment_tier": "production",
  "environment_slug": "production",
  "environment_external_url": "https://prod.example.com",
  "project": { /* project details */ },
  "short_sha": "da156088",
  "user": { /* user details */ },
  "user_url": "https://gitlab.com/jdoe",
  "commit_url": "https://gitlab.com/namespace/project/-/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
  "commit_title": "Deploy to production"
}

Status Values: created, running, success, failed, canceled

Wiki Page Events

Trigger: Wiki page created, updated, or deleted

Event Header: X-Gitlab-Event: Wiki Page Hook

Payload Structure:

{
  "object_kind": "wiki_page",
  "user": { /* user details */ },
  "project": { /* project details */ },
  "wiki": {
    "web_url": "https://gitlab.com/namespace/project/-/wikis/home",
    "git_ssh_url": "git@gitlab.com:namespace/project.wiki.git",
    "git_http_url": "https://gitlab.com/namespace/project.wiki.git",
    "path_with_namespace": "namespace/project",
    "default_branch": "main"
  },
  "object_attributes": {
    "title": "Home",
    "content": "# Welcome\n\nThis is the home page...",
    "format": "markdown",
    "message": "Update home page",
    "slug": "home",
    "url": "https://gitlab.com/namespace/project/-/wikis/home",
    "action": "update"
  }
}

Action Values: create, update, delete

Release Events

Trigger: Release created, updated, or deleted

Event Header: X-Gitlab-Event: Release Hook

Payload Structure:

{
  "id": 1,
  "created_at": "2025-01-15 12:00:00 UTC",
  "description": "Release notes for v1.0.0",
  "name": "Version 1.0.0",
  "released_at": "2025-01-15 12:00:00 UTC",
  "tag": "v1.0.0",
  "object_kind": "release",
  "project": { /* project details */ },
  "url": "https://gitlab.com/namespace/project/-/releases/v1.0.0",
  "action": "create",
  "assets": {
    "count": 2,
    "links": [
      {
        "id": 1,
        "external": true,
        "link_type": "other",
        "name": "Binary",
        "url": "https://example.com/binary"
      }
    ],
    "sources": [
      {
        "format": "zip",
        "url": "https://gitlab.com/namespace/project/-/archive/v1.0.0/project-v1.0.0.zip"
      },
      {
        "format": "tar.gz",
        "url": "https://gitlab.com/namespace/project/-/archive/v1.0.0/project-v1.0.0.tar.gz"
      }
    ]
  },
  "commit": { /* commit details */ }
}

Action Values: create, update, delete

Testing Webhooks

Via GitLab UI

  1. Navigate to Project Settings > Webhooks
  2. Find your webhook
  3. Click "Test" dropdown
  4. Select event type to test
  5. View response in UI

Via API

curl -X POST "https://gitlab.com/api/v4/projects/:id/hooks/:hook_id/test/push_events" \
  --header "PRIVATE-TOKEN: <token>"

Local Testing

Use tools like:

  • ngrok: Create public URL for local development
  • webhook.site: Test webhook delivery
  • requestbin.com: Inspect webhook payloads
# Using ngrok
ngrok http 3000

# Update webhook URL to ngrok URL
# Trigger event in GitLab
# View webhook payload in ngrok dashboard

Webhook Best Practices

1. Security

  • Use HTTPS: Always use HTTPS URLs for webhooks
  • Verify SSL: Enable SSL verification in production
  • Secret Tokens: Use secret tokens to verify webhook authenticity
  • Validate Payloads: Verify request signatures
  • IP Allowlisting: Restrict webhook sources to GitLab IPs

2. Reliability

  • Return Quickly: Respond with 2xx status code quickly (< 10 seconds)
  • Async Processing: Queue webhook payloads for async processing
  • Idempotency: Handle duplicate webhook deliveries
  • Error Handling: Handle errors gracefully
  • Retry Logic: Implement retry logic for failed processing

3. Monitoring

  • Log Webhooks: Log all webhook deliveries
  • Track Failures: Monitor webhook failure rates
  • Alert on Issues: Set up alerts for webhook failures
  • Recent Deliveries: Check recent deliveries in GitLab UI

4. Performance

  • Rate Limiting: Handle rate limits on external APIs
  • Batch Processing: Batch similar webhook events
  • Caching: Cache frequently accessed data
  • Timeouts: Set appropriate timeouts

Example Webhook Handler

Python Flask Example

from flask import Flask, request, jsonify
import hmac
import hashlib

app = Flask(__name__)
WEBHOOK_SECRET = "your-secret-token"

def verify_signature(payload, signature):
    """Verify webhook signature"""
    if not signature:
        return False

    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    # Get signature from header
    signature = request.headers.get('X-Gitlab-Token')

    # Verify signature
    if not verify_signature(request.data, signature):
        return jsonify({'error': 'Invalid signature'}), 401

    # Get event type
    event_type = request.headers.get('X-Gitlab-Event')

    # Parse payload
    payload = request.json

    # Handle different event types
    if event_type == 'Push Hook':
        handle_push_event(payload)
    elif event_type == 'Merge Request Hook':
        handle_merge_request_event(payload)
    elif event_type == 'Pipeline Hook':
        handle_pipeline_event(payload)

    return jsonify({'status': 'received'}), 200

def handle_push_event(payload):
    """Handle push events"""
    ref = payload['ref']
    commits = payload['commits']
    print(f"Received push to {ref} with {len(commits)} commits")

def handle_merge_request_event(payload):
    """Handle merge request events"""
    action = payload['object_attributes']['action']
    iid = payload['object_attributes']['iid']
    print(f"Merge request {iid} was {action}")

def handle_pipeline_event(payload):
    """Handle pipeline events"""
    status = payload['object_attributes']['status']
    ref = payload['object_attributes']['ref']
    print(f"Pipeline on {ref} status: {status}")

if __name__ == '__main__':
    app.run(port=5000)

Node.js Express Example

const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');

const app = express();
const WEBHOOK_SECRET = 'your-secret-token';

// Verify webhook signature
function verifySignature(payload, signature) {
  if (!signature) return false;

  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-gitlab-token'];
  const eventType = req.headers['x-gitlab-event'];

  // Verify signature
  if (!verifySignature(req.body, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(req.body.toString());

  // Handle different event types
  switch (eventType) {
    case 'Push Hook':
      handlePushEvent(payload);
      break;
    case 'Merge Request Hook':
      handleMergeRequestEvent(payload);
      break;
    case 'Pipeline Hook':
      handlePipelineEvent(payload);
      break;
  }

  res.json({ status: 'received' });
});

function handlePushEvent(payload) {
  console.log(`Push to ${payload.ref} with ${payload.commits.length} commits`);
}

function handleMergeRequestEvent(payload) {
  const { action, iid } = payload.object_attributes;
  console.log(`Merge request ${iid} was ${action}`);
}

function handlePipelineEvent(payload) {
  const { status, ref } = payload.object_attributes;
  console.log(`Pipeline on ${ref} status: ${status}`);
}

app.listen(5000, () => {
  console.log('Webhook server listening on port 5000');
});

Troubleshooting

Common Issues

  1. Webhook Not Triggering

    • Verify event is enabled in webhook configuration
    • Check webhook URL is accessible from internet
    • Review Recent Deliveries in GitLab UI
    • Check firewall rules
  2. SSL Verification Failures

    • Ensure SSL certificate is valid
    • Check certificate chain is complete
    • Temporarily disable SSL verification for testing (not recommended for production)
  3. Timeouts

    • Reduce processing time in webhook handler
    • Return 2xx response quickly, process async
    • Check network connectivity
  4. Authentication Errors

    • Verify secret token matches
    • Check signature verification logic
    • Ensure token is transmitted correctly

Debugging Tips

  1. Use webhook testing services (webhook.site, requestbin.com)
  2. Check "Recent Deliveries" in GitLab webhook settings
  3. Enable detailed logging in webhook handler
  4. Test with curl/Postman using sample payloads
  5. Verify IP addresses if using allowlisting
  6. Check response status codes and headers

Additional Resources