GitHub Webhooks to an Internal Service Using Skooh (HMAC + Event Routing)
Validate GitHub webhook signatures with HMAC-SHA256 and route events to different handlers.
Overview
GitHub webhooks power CI/CD pipelines, issue tracking, deployment automation, and more. This guide covers:
- Setting up Skooh to receive GitHub webhooks
- Validating `X-Hub-Signature-256` (HMAC-SHA256)
- Routing events by type
- Using replay for testing
Step 1: Create Your Skooh Endpoint
- Go to Skooh Hooks
- Create a new endpoint named "GitHub"
- Copy your URL: `https://api.skooh.io/hook/gh-abc123\`
Step 2: Configure GitHub
In your repository (or organization):
- Go to Settings → Webhooks → Add webhook
- Payload URL: Your Skooh URL
- Content type: `application/json`
- Secret: Generate a strong secret (save it!)
- Events: Choose specific events or "Send me everything"
Note: See GitHub's webhook documentation for current setup instructions.
Step 3: Run the Skooh Proxy
Using the Desktop App:
- Download the Skooh Desktop App
- Enter your API key and hook token
- Set forwarding URL (e.g., `http://localhost:8080/webhooks/github\`)
- Click Connect
Using Docker:
docker run -d \\
--name skooh-github \\
-e SKOOH_TOKEN=gh-abc123 \\
-e SKOOH_API_KEY=your_api_key \\
-e FORWARD_URL=http://ci-service:8080/webhooks/github \\
--restart unless-stopped \\
skooh/proxy
Validating X-Hub-Signature-256
GitHub signs webhooks with HMAC-SHA256. Always verify.
const crypto = require('crypto');
function verifyGitHubSignature(req, secret) {
const signature = req.headers['x-hub-signature-256'];
if (!signature) return false;
const hmac = crypto.createHmac('sha256', secret);
const digest = 'sha256=' + hmac.update(req.rawBody).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
Event Routing Pattern
GitHub sends different events to the same endpoint. Route them:
app.post('/webhooks/github',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Verify signature
if (!verifyGitHubSignature(req, process.env.GITHUB_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = req.headers['x-github-event'];
const delivery = req.headers['x-github-delivery'];
const payload = JSON.parse(req.body);
console.log(\`Received \${event} (delivery: \${delivery})\`);
// Route by event type
switch (event) {
case 'push':
await handlePush(payload);
break;
case 'pull_request':
await handlePullRequest(payload);
break;
case 'issues':
await handleIssue(payload);
break;
case 'release':
await handleRelease(payload);
break;
case 'workflow_run':
await handleWorkflowRun(payload);
break;
default:
console.log(\`Unhandled event: \${event}\`);
}
res.status(200).send('OK');
}
);
async function handlePush(payload) {
const { ref, commits, repository } = payload;
if (ref === 'refs/heads/main') {
console.log(\`Deploy triggered: \${commits.length} commits to main\`);
await triggerDeployment(repository.full_name);
}
}
async function handlePullRequest(payload) {
const { action, pull_request } = payload;
if (action === 'opened' || action === 'synchronize') {
console.log(\`Running CI for PR #\${pull_request.number}\`);
await runCIChecks(pull_request);
}
}
Sub-routing with Action Types
Many GitHub events include an `action` field:
async function handlePullRequest(payload) {
const { action, pull_request, sender } = payload;
switch (action) {
case 'opened':
await notifyTeam(\`New PR by \${sender.login}: \${pull_request.title}\`);
await runCIChecks(pull_request);
break;
case 'closed':
if (pull_request.merged) {
await triggerPostMergeWorkflow(pull_request);
}
break;
case 'review_requested':
await notifyReviewers(pull_request);
break;
}
}
Using Replay for Testing
Test Your CI Pipeline
- Make a real push to trigger a webhook
- Find it in the Skooh dashboard
- Click Replay to test your handler again
- No need to make another commit!
Debug Event Handling
- Filter by event type in the dashboard
- Find a problematic webhook
- Replay while watching your logs
- Fix the bug, replay again
Test Edge Cases
Replay webhooks for:
- Force pushes
- Deleted branches
- Large PRs with many commits
- Merge commits
Production Tips
- Use idempotency: Store `x-github-delivery` IDs to prevent duplicate processing
- Respond fast: GitHub expects a response within 10 seconds
- Process async: Queue heavy work, respond immediately
- Monitor: Set up alerts for signature failures
// Respond fast, process async
app.post('/webhooks/github', async (req, res) => {
// Verify immediately
if (!verifyGitHubSignature(req)) {
return res.status(401).send('Invalid signature');
}
// Queue for processing
await webhookQueue.add({
event: req.headers['x-github-event'],
payload: JSON.parse(req.body),
delivery: req.headers['x-github-delivery']
});
// Respond immediately
res.status(202).send('Accepted');
});
Conclusion
Skooh makes it easy to receive GitHub webhooks behind your firewall. With HMAC validation, event routing, and replay, you can build robust CI/CD pipelines and automation workflows.
Written by Skooh Team