← Volver al catálogo
WebReferenciaIntermedioGratis

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).

Descargar skill (.zip)

Descarga abierta · sin registro · para GitHub, Node.js, Python

// resultado_de_ejemplo

// 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

GitHubNode.jsPythonExpressNext.jsFastAPI
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 €.