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:
- Go to your repository on GitHub
- Navigate to Settings → Webhooks
- Click Add webhook
- Payload URL:
https://hooksdash-api.ibadsiddiqui.dev/api/h/github-dev - Content type:
application/json - Secret: Create a strong secret (save it for signature verification)
- Select which events to trigger the webhook
- Click Add webhook
For Organization Webhooks:
- Go to your organization on GitHub
- Navigate to Settings → Webhooks
- 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
| Event | Description |
|---|---|
push | Git push to a repository |
pull_request | Pull request opened, closed, merged, etc. |
issues | Issue opened, closed, labeled, etc. |
issue_comment | Comment added to an issue or pull request |
release | Release published, created, deleted |
create | Branch or tag created |
delete | Branch or tag deleted |
workflow_run | GitHub Actions workflow run completed |
deployment | Deployment created |
star | Repository 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 olderx-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
pushevents are sent - Click "Let me select individual events" to choose specific events