GitHub Integration

Learn how to integrate GitHub webhooks with HooksDash for testing repository events locally.

Setup

1. Create a HooksDash Endpoint

# Via CLI
hooksdash endpoints create --name "GitHub Development" --slug github-dev

# Your webhook URL:
https://hooksdash-api.ibadsiddiqui.dev/api/h/github-dev

2. Configure in GitHub

For Repository Webhooks:

  1. Go to your repository on GitHub
  2. Navigate to Settings → Webhooks
  3. Click Add webhook
  4. Payload URL: https://hooksdash-api.ibadsiddiqui.dev/api/h/github-dev
  5. Content type: application/json
  6. Secret: Create a strong secret (save it for signature verification)
  7. Select which events to trigger the webhook
  8. Click Add webhook

For Organization Webhooks:

  1. Go to your organization on GitHub
  2. Navigate to Settings → Webhooks
  3. Follow the same steps as above

Implementation

Basic Webhook Handler

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

const app = express();
app.use(express.json());

function verifyGitHubSignature(req) {
  const signature = req.headers['x-hub-signature-256'];
  const secret = process.env.GITHUB_WEBHOOK_SECRET;

  if (!signature || !secret) {
    return false;
  }

  const hmac = crypto.createHmac('sha256', secret);
  const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');

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

app.post('/webhook', (req, res) => {
  if (!verifyGitHubSignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  const event = req.headers['x-github-event'];
  const payload = req.body;

  switch (event) {
    case 'push':
      console.log('Push to', payload.ref);
      console.log('Commits:', payload.commits.length);
      // Trigger CI/CD pipeline
      break;

    case 'pull_request':
      console.log('PR', payload.action, ':', payload.pull_request.title);
      // Run tests, add labels, etc.
      break;

    case 'issues':
      console.log('Issue', payload.action, ':', payload.issue.title);
      // Auto-label, assign, etc.
      break;

    case 'release':
      console.log('Release', payload.action, ':', payload.release.tag_name);
      // Deploy, notify team, etc.
      break;

    default:
      console.log('Unhandled event:', event);
  }

  res.json({ received: true });
});

app.listen(3000);

TypeScript Implementation

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

interface GitHubWebhookRequest extends express.Request {
  body: any;
}

function verifyGitHubSignature(
  body: any,
  signature: string | undefined,
  secret: string
): boolean {
  if (!signature) {
    return false;
  }

  const hmac = crypto.createHmac('sha256', secret);
  const digest = 'sha256=' + hmac.update(JSON.stringify(body)).digest('hex');

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

app.post('/webhook', (req: GitHubWebhookRequest, res) => {
  const signature = req.headers['x-hub-signature-256'] as string;
  const secret = process.env.GITHUB_WEBHOOK_SECRET!;

  if (!verifyGitHubSignature(req.body, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  const event = req.headers['x-github-event'] as string;
  const payload = req.body;

  switch (event) {
    case 'push':
      handlePush(payload);
      break;
    case 'pull_request':
      handlePullRequest(payload);
      break;
    case 'issues':
      handleIssue(payload);
      break;
    default:
      console.log('Unhandled event:', event);
  }

  res.json({ received: true });
});

function handlePush(payload: any) {
  console.log('Push to', payload.ref);
  console.log('Commits:', payload.commits.length);
}

function handlePullRequest(payload: any) {
  console.log('PR', payload.action, ':', payload.pull_request.title);
}

function handleIssue(payload: any) {
  console.log('Issue', payload.action, ':', payload.issue.title);
}

app.listen(3000);

Common Event Types

EventDescription
pushGit push to a repository
pull_requestPull request opened, closed, merged, etc.
issuesIssue opened, closed, labeled, etc.
issue_commentComment added to an issue or pull request
releaseRelease published, created, deleted
createBranch or tag created
deleteBranch or tag deleted
workflow_runGitHub Actions workflow run completed
deploymentDeployment created
starRepository starred or unstarred

Testing Locally

1. Start Your Server

node server.js

2. Start HooksDash Tunnel

hooksdash forward --port 3000 --endpoint github-dev

3. Trigger Events

Test your webhook by:

  • Making a commit and pushing to the repository
  • Opening a pull request
  • Creating an issue
  • Or use GitHub's "Recent Deliveries" to redeliver past events

Example Use Cases

CI/CD Pipeline

app.post('/webhook', (req, res) => {
  const event = req.headers['x-github-event'];
  const payload = req.body;

  if (event === 'push' && payload.ref === 'refs/heads/main') {
    // Deploy to production
    console.log('Deploying to production...');
    deployToProduction(payload.after); // commit SHA
  }

  if (event === 'pull_request' && payload.action === 'opened') {
    // Run tests
    console.log('Running tests for PR #', payload.pull_request.number);
    runTests(payload.pull_request);
  }

  res.json({ received: true });
});

Auto-Labeling Issues

const { Octokit } = require('@octokit/rest');

const octokit = new Octokit({
  auth: process.env.GITHUB_TOKEN,
});

app.post('/webhook', async (req, res) => {
  const event = req.headers['x-github-event'];
  const payload = req.body;

  if (event === 'issues' && payload.action === 'opened') {
    const issue = payload.issue;
    const labels = [];

    // Auto-label based on content
    if (issue.title.toLowerCase().includes('bug')) {
      labels.push('bug');
    }
    if (issue.title.toLowerCase().includes('feature')) {
      labels.push('enhancement');
    }

    if (labels.length > 0) {
      await octokit.issues.addLabels({
        owner: payload.repository.owner.login,
        repo: payload.repository.name,
        issue_number: issue.number,
        labels,
      });
    }
  }

  res.json({ received: true });
});

Release Notifications

app.post('/webhook', async (req, res) => {
  const event = req.headers['x-github-event'];
  const payload = req.body;

  if (event === 'release' && payload.action === 'published') {
    const release = payload.release;

    // Send notification to Slack
    await sendToSlack({
      text: `New release: ${release.name}`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*New Release:* ${release.name}\n${release.body}`,
          },
        },
      ],
    });

    // Update documentation
    await updateDocs(release.tag_name);
  }

  res.json({ received: true });
});

Best Practices

1. Verify Signatures

Always verify webhook signatures to ensure they come from GitHub:

function verifyGitHubSignature(req) {
  const signature = req.headers['x-hub-signature-256'];
  const secret = process.env.GITHUB_WEBHOOK_SECRET;

  const hmac = crypto.createHmac('sha256', secret);
  const digest = 'sha256=' + hmac.update(JSON.stringify(req.body)).digest('hex');

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

2. Handle Webhook Retries

GitHub will retry failed webhooks. Implement idempotency:

const processedDeliveries = new Set();

app.post('/webhook', (req, res) => {
  const deliveryId = req.headers['x-github-delivery'];

  if (processedDeliveries.has(deliveryId)) {
    return res.json({ received: true });
  }

  processedDeliveries.add(deliveryId);
  // Process event...
});

3. Respond Quickly

GitHub expects a 2xx response within 10 seconds:

app.post('/webhook', async (req, res) => {
  // Verify signature
  if (!verifyGitHubSignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // Respond immediately
  res.json({ received: true });

  // Process asynchronously
  processGitHubEvent(req.headers['x-github-event'], req.body)
    .catch(console.error);
});

Troubleshooting

Signature Verification Failed

Issue: Invalid signature

Solution:

  • Verify your webhook secret is correct
  • Make sure you're using the raw request body for signature verification
  • Check that you're using x-hub-signature-256 (not the older x-hub-signature)

Webhook Not Received

Issue: Webhook sent from GitHub but not appearing in HooksDash

Solution:

  • Verify the webhook URL in GitHub settings
  • Check "Recent Deliveries" in GitHub for delivery status and error messages
  • Ensure the webhook is active (not disabled due to failures)

Events Not Being Sent

Issue: Only receiving ping events

Solution:

  • Check which events you selected in GitHub webhook settings
  • By default, only push events are sent
  • Click "Let me select individual events" to choose specific events

Resources