← Volver al catálogo
WebReferenciaIntermedioGratis

Webhooks de GitLab

Referencia de código lista para usar para recibir y verificar webhooks de GitLab en Express o FastAPI, con comparación segura de tokens y manejo de eventos como push, merge request, issue y pipeline.

Descargar skill (.zip)

Descarga abierta · sin registro · para GitLab, Node.js, Express

// resultado_de_ejemplo

// Generated with: gitlab-webhooks skill // https://github.com/hookdeck/webhook-skills // // CULTIVA Deploy Bot — GitLab Webhook Server // Recibe eventos de gitlab.cultivaia.com y dispara acciones automáticas // Stack: Node.js 20 + Express 5 | Deploy: Railway (puerto 3000)

'use strict';

const express = require('express'); const crypto = require('crypto');

const app = express();

// ───────────────────────────────────────────── // Config (desde variables de entorno en Railway) // ───────────────────────────────────────────── const PORT = process.env.PORT || 3000; const GITLAB_WEBHOOK_TOKEN = process.env.GITLAB_WEBHOOK_TOKEN || ''; const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL || ''; const HULY_API_URL = process.env.HULY_API_URL || ''; const HULY_API_KEY = process.env.HULY_API_KEY || '';

// ───────────────────────────────────────────── // Verificación de token timing-safe // GitLab envía el token en texto plano (no HMAC) // → usamos crypto.timingSafeEqual para evitar timing attacks // ───────────────────────────────────────────── function verifyGitLabWebhook(tokenHeader, secret) { if (!tokenHeader || !secret) return false;

try { return crypto.timingSafeEqual( Buffer.from(tokenHeader), Buffer.from(secret) ); } catch { // Longitudes distintas lanzarían error → token inválido return false; } }

// ───────────────────────────────────────────── // Helpers de notificación (stub comentado; reemplazar con SDK real) // ───────────────────────────────────────────── async function notifySlack(message) { if (!SLACK_WEBHOOK_URL) return; // await fetch(SLACK_WEBHOOK_URL, { // method: 'POST', // headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ text: message }), // }); console.log('[Slack]', message); }

async function updateHulyBoard(projectRef, mrTitle, mrUrl) { if (!HULY_API_URL || !HULY_API_KEY) return; // await fetch(${HULY_API_URL}/tasks, { // method: 'PATCH', // headers: { Authorization: Bearer ${HULY_API_KEY}, 'Content-Type': 'application/json' }, // body: JSON.stringify({ ref: projectRef, status: 'done', link: mrUrl, title: mrTitle }), // }); console.log('[Huly] MR merged →', mrTitle, mrUrl); }

async function openFailureIssue(projectId, pipelineId, pipelineUrl) { // En producción: gitlab.cultivaia.com API v4 POST /projects/:id/issues console.log([GitLab API] Abriendo issue para pipeline #${pipelineId} en proyecto ${projectId}); console.log( → Ver logs: ${pipelineUrl}); }

// ───────────────────────────────────────────── // Handlers por evento // ─────────────────────────────────────────────

/** Push a cualquier rama */ async function handlePush(body) { const branch = body.ref?.replace('refs/heads/', ''); const commits = body.commits?.length ?? 0; const author = body.user_name; const repo = body.project?.name;

console.log([push] ${author} → ${repo}:${branch} (${commits} commit/s));

if (branch === 'main') { await notifySlack( :rocket: *Deploy en curso* — \${repo}`\n+ Rama: `${branch}` · ${commits} commit/s por *${author}*` ); } }

/** Merge Request (opened, merged, closed) */ async function handleMergeRequest(body) { const mr = body.object_attributes; const action = mr?.action; const title = mr?.title; const iid = mr?.iid; const url = mr?.url; const repo = body.project?.name;

console.log([merge_request] !${iid} "${title}" → ${action});

if (action === 'merge') { await notifySlack( :white_check_mark: *MR merged* en \${repo}`\n+ !${iid} _${title}_\n${url} ); await updateHulyBoard(${repo}#${iid}, title, url); } else if (action === 'open') { await notifySlack( :eyes: *Nuevo MR* en `${repo}`\n!${iid} _${title}_\n${url}` ); } }

/** Issue (opened, closed, updated) */ async function handleIssue(body) { const issue = body.object_attributes; const action = issue?.action; const title = issue?.title; const iid = issue?.iid; const repo = body.project?.name;

console.log([issue] #${iid} "${title}" → ${action});

// Auto-asignación al crear issue (round-robin simple) const devTeam = ['@luisa', '@miguel', '@sara']; if (action === 'open') { const assignee = devTeam[iid % devTeam.length]; console.log([issue] Auto-asignando #${iid} a ${assignee}); await notifySlack( :bug: *Issue abierto* en \${repo}` → asignado a ${assignee}\n+ #${iid} _${title}_` ); } }

/** Pipeline (running, success, failed, canceled) */ async function handlePipeline(body) { const pipe = body.object_attributes; const status = pipe?.status; const pipelineId = pipe?.id; const pipelineUrl = pipe?.url; const projectId = body.project?.id; const repo = body.project?.name;

console.log([pipeline] #${pipelineId} → ${status});

if (status === 'success') { await notifySlack( :white_check_mark: *Pipeline OK* — \${repo}` #${pipelineId} ); } else if (status === 'failed') { await notifySlack( :x: *Pipeline FALLIDO* — `${repo}` #${pipelineId}\n${pipelineUrl}` ); await openFailureIssue(projectId, pipelineId, pipelineUrl); } }

// ───────────────────────────────────────────── // Endpoint principal // ───────────────────────────────────────────── app.post( '/webhooks/gitlab', express.json(), async (req, res) => { const tokenHeader = req.headers['x-gitlab-token']; const event = req.headers['x-gitlab-event']; const eventUUID = req.headers['x-gitlab-event-uuid']; const instanceHost = req.headers['x-gitlab-instance'];

// 1. Verificar token
if (!verifyGitLabWebhook(tokenHeader, GITLAB_WEBHOOK_TOKEN)) {
  console.error(`[auth] Token inválido | UUID: ${eventUUID}`);
  return res.status(401).json({ error: 'Unauthorized' });
}

console.log(`\n── Evento recibido ──────────────────────────────`);
console.log(`  Event       : ${event}`);
console.log(`  UUID        : ${eventUUID}`);
console.log(`  GitLab host : ${instanceHost}`);

// 2. Enrutar por object_kind
const objectKind = req.body?.object_kind;

try {
  switch (objectKind) {
    case 'push':
    case 'tag_push':
      await handlePush(req.body);
      break;

    case 'merge_request':
      await handleMergeRequest(req.body);
      break;

    case 'issue':
      await handleIssue(req.body);
      break;

    case 'pipeline':
      await handlePipeline(req.body);
      break;

    default:
      console.log(`[skip] Evento no manejado: ${objectKind ?? event}`);
  }
} catch (err) {
  console.error('[error] Error procesando evento:', err.message);
  // 200 de todas formas: no queremos que GitLab reintente por un error interno nuestro
}

return res.status(200).json({ received: true, uuid: eventUUID });

} );

// ───────────────────────────────────────────── // Health check // ───────────────────────────────────────────── app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'cultiva-deploy-bot' }));

app.listen(PORT, () => { console.log(CULTIVA Deploy Bot escuchando en http://0.0.0.0:${PORT}); console.log( POST /webhooks/gitlab — recibe eventos de GitLab); console.log( GET /health — health check); });

module.exports = app; // para tests

// qué_hace

Proporciona snippets de verificacion de tokens y handlers completos para procesar eventos de GitLab via webhook.

// cómo_lo_hace

Usa comparacion de tokens timing-safe (crypto.timingSafeEqual / secrets.compare_digest) y enruta eventos por object_kind con ejemplos para Express y FastAPI.

// ejemplo_de_uso

Úsala cuando configuras webhooks de GitLab para disparar pipelines externos y necesitas validar que los eventos son auténticos. Ej.: el handler de FastAPI compara el token X-Gitlab-Token con comparación en tiempo constante antes de procesar el evento de merge request.

// plataformas

GitLabNode.jsExpressPythonFastAPI
Categoría
Web
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// opiniones_de_la_comunidad

Opiniones

Cargando opiniones…

// pase_cultiva_ia

Llévate todo el arsenal con el Pase

Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.

Pago único · IVA incluido · pago seguro con Stripe.

Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.