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.
Descarga abierta · sin registro · para GitLab, Node.js, Express
// 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
// 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 €.