GitHub Webhooks: Verificacion y Manejo de Eventos
Guia de referencia para recibir y verificar webhooks de GitHub con firma HMAC-SHA256, con ejemplos en Node y Python para los eventos mas comunes (push, pull_request, issues, release).
Descarga abierta · sin registro · para GitHub, Node.js, Python
// Generated with: github-webhooks skill // https://github.com/hookdeck/webhook-skills // Cliente: CultivaFlow SaaS — webhook GitHub para automatización CI/CD
'use strict';
require('dotenv').config(); const express = require('express'); const crypto = require('crypto');
const app = express();
// ───────────────────────────────────────────── // VERIFICACIÓN HMAC-SHA256 (timing-safe) // ─────────────────────────────────────────────
/**
- Verifica la firma del webhook de GitHub.
- @param {Buffer} rawBody Cuerpo raw de la petición (SIN parsear)
- @param {string} signatureHeader Valor de X-Hub-Signature-256
- @param {string} secret GITHUB_WEBHOOK_SECRET
- @returns {boolean} */ function verifyGitHubWebhook(rawBody, signatureHeader, secret) { if (!signatureHeader) return false;
const [algo, sig] = (signatureHeader || '').split('='); if (algo !== 'sha256' || !sig) return false;
const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex');
// timingSafeEqual previene timing-attacks: lanza si los buffers difieren en longitud try { return crypto.timingSafeEqual( Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex') ); } catch { return false; // longitudes distintas → firma inválida } }
// ───────────────────────────────────────────── // HANDLERS DE EVENTOS (async, no bloquean la respuesta) // ─────────────────────────────────────────────
async function handlePush(payload, deliveryId) { const { ref, head_commit, repository, pusher } = payload; const branch = ref.replace('refs/heads/', '');
console.log(JSON.stringify({ event: 'push', deliveryId, branch, commit: head_commit?.id?.slice(0, 7), message: head_commit?.message, pusher: pusher?.name, repo: repository?.full_name, }));
if (branch === 'main') { // 1. Disparar deploy en Railway await triggerRailwayDeploy({ branch, commitSha: head_commit?.id });
// 2. Notificar en Slack
await notifySlack({
text: `🚀 *CultivaFlow* deploy iniciado por \`${pusher?.name}\`\n` +
`Branch: \`${branch}\` · Commit: \`${head_commit?.id?.slice(0, 7)}\`\n` +
`> ${head_commit?.message}`,
});
} }
async function handlePullRequest(payload, deliveryId) { const { action, number, pull_request, repository } = payload; console.log(JSON.stringify({ event: 'pull_request', deliveryId, action, pr: number, title: pull_request?.title, author: pull_request?.user?.login, }));
if (action === 'opened' || action === 'ready_for_review') { // Asignar revisor automático según la carga de trabajo const reviewer = await pickReviewer(pull_request?.user?.login);
// Comentar en el PR con checklist de QA
await postPRComment(repository?.full_name, number, reviewer, `
Checklist de QA — CultivaFlow
Asignado a: @${reviewer}
- Tests unitarios pasan (`npm test`)
- Tests E2E pasan (`npm run test:e2e`)
- Sin regresiones en métricas de performance (Lighthouse ≥ 90)
- Variables de entorno documentadas si se añadieron
- Migraciones de BD reversibles
- Sin secrets/tokens hardcodeados `.trim()); } }
async function handleRelease(payload, deliveryId) { const { action, release, repository } = payload; console.log(JSON.stringify({ event: 'release', deliveryId, action, tag: release?.tag_name, name: release?.name, prerelease: release?.prerelease, }));
if (action === 'published' && !release?.prerelease) { // Actualizar changelog interno (Notion/Linear) await updateChangelog({ version: release?.tag_name, body: release?.body, url: release?.html_url, });
// Notificar clientes por email (SendGrid)
await sendReleaseEmail({
version: release?.tag_name,
highlights: release?.body,
});
} }
async function handleWorkflowRun(payload, deliveryId) { const { workflow_run } = payload; const { name, conclusion, html_url, head_branch, head_sha } = workflow_run;
console.log(JSON.stringify({ event: 'workflow_run', deliveryId, workflow: name, conclusion, branch: head_branch, }));
if (conclusion === 'failure') {
// Crear issue automático en Linear con contexto del fallo
await createLinearIssue({
title: [CI] Fallo en workflow "${name}" · ${head_branch},
description: ## Workflow fallido\n\n +
- **Workflow:** ${name}\n +
- **Branch:** \${head_branch}`\n+ - **Commit:** `${head_sha?.slice(0, 7)}`\n+ - **Logs:** ${html_url}\n\n+ Investigar y corregir antes del próximo deploy.`,
priority: 'urgent',
label: 'ci-failure',
});
}
}
// ───────────────────────────────────────────── // STUBS DE INTEGRACIONES EXTERNAS // ─────────────────────────────────────────────
async function triggerRailwayDeploy({ branch, commitSha }) {
// POST https://backboard.railway.app/graphql/v2 con mutation Deploy
console.log([Railway] Deploying branch=${branch} commit=${commitSha?.slice(0, 7)});
}
async function notifySlack({ text }) {
// POST process.env.SLACK_WEBHOOK_URL con { text }
console.log([Slack] ${text.replace(/\n/g, ' ').slice(0, 80)}…);
}
async function pickReviewer(author) { const pool = ['laura', 'marc', 'sofia']; // Excluir al autor; round-robin simplificado return pool.filter(r => r !== author)[0] ?? pool[0]; }
async function postPRComment(repoFullName, prNumber, reviewer, body) {
// POST https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments
console.log([GitHub] PR #${prNumber} → asignado a @${reviewer}, checklist posteado);
}
async function updateChangelog({ version, body, url }) {
console.log([Changelog] v${version} registrado. Notas: ${body?.slice(0, 60)}…);
}
async function sendReleaseEmail({ version, highlights }) {
console.log([SendGrid] Email de release v${version} enviado a lista de clientes);
}
async function createLinearIssue({ title, priority, label }) {
console.log([Linear] Issue creado — "${title}" (${priority}) [${label}]);
}
// ───────────────────────────────────────────── // RUTA WEBHOOK // ─────────────────────────────────────────────
app.post( '/webhooks/github', express.raw({ type: 'application/json' }), // body raw para verificación HMAC async (req, res) => { const signature = req.headers['x-hub-signature-256']; const event = req.headers['x-github-event']; const deliveryId = req.headers['x-github-delivery'];
// 1. Verificar firma ANTES de parsear
if (!verifyGitHubWebhook(req.body, signature, process.env.GITHUB_WEBHOOK_SECRET)) {
console.error(`[webhook] Firma inválida. delivery=${deliveryId}`);
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Responder 200 de inmediato (GitHub espera < 10 s)
res.status(200).json({ received: true, delivery: deliveryId });
// 3. Parsear y despachar de forma asíncrona
let payload;
try {
payload = JSON.parse(req.body.toString());
} catch (err) {
console.error(`[webhook] JSON inválido. delivery=${deliveryId}`, err);
return;
}
try {
switch (event) {
case 'ping':
console.log(`[webhook] ping recibido: "${payload.zen}"`);
break;
case 'push':
await handlePush(payload, deliveryId);
break;
case 'pull_request':
await handlePullRequest(payload, deliveryId);
break;
case 'release':
await handleRelease(payload, deliveryId);
break;
case 'workflow_run':
await handleWorkflowRun(payload, deliveryId);
break;
default:
console.log(`[webhook] Evento no manejado: ${event}. delivery=${deliveryId}`);
}
} catch (err) {
// El error no afecta la respuesta ya enviada; loguear para diagnóstico
console.error(`[webhook] Error procesando ${event}. delivery=${deliveryId}`, err);
}
} );
// ───────────────────────────────────────────── // HEALTH CHECK // ─────────────────────────────────────────────
app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'cultivaflow-webhooks' }));
// ───────────────────────────────────────────── // ARRANQUE // ─────────────────────────────────────────────
if (require.main === module) {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(CultivaFlow webhook server · http://localhost:${PORT});
console.log(Endpoint: POST http://localhost:${PORT}/webhooks/github);
});
}
module.exports = { app, verifyGitHubWebhook };
// qué_hace
Proporciona el patron de verificacion de firma y la estructura de eventos para integrar webhooks de GitHub en cualquier backend.
// cómo_lo_hace
Documenta la verificacion HMAC-SHA256 con comparacion timing-safe, las cabeceras clave y los tipos de evento, con snippets listos para Express, Next.js y FastAPI.
// ejemplo_de_uso
Para backends que reciben eventos de GitHub (push, PR, releases) y necesitan validar que son legítimos. Ej.: añadir verificación HMAC-SHA256 timing-safe a tu webhook de GitHub en Express para ignorar peticiones de terceros que falsifiquen el origen.
// 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 €.