Skooh.io Logo
Back to blog
IntegrationDeveloper TutorialMarch 12, 20258 min

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:

  1. Setting up Skooh to receive GitHub webhooks
  2. Validating `X-Hub-Signature-256` (HMAC-SHA256)
  3. Routing events by type
  4. Using replay for testing

Step 1: Create Your Skooh Endpoint

  1. Go to Skooh Hooks
  2. Create a new endpoint named "GitHub"
  3. Copy your URL: `https://api.skooh.io/hook/gh-abc123\`

Step 2: Configure GitHub

In your repository (or organization):

  1. Go to Settings → Webhooks → Add webhook
  2. Payload URL: Your Skooh URL
  3. Content type: `application/json`
  4. Secret: Generate a strong secret (save it!)
  5. 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:

  1. Download the Skooh Desktop App
  2. Enter your API key and hook token
  3. Set forwarding URL (e.g., `http://localhost:8080/webhooks/github\`)
  4. 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

  1. Make a real push to trigger a webhook
  2. Find it in the Skooh dashboard
  3. Click Replay to test your handler again
  4. No need to make another commit!

Debug Event Handling

  1. Filter by event type in the dashboard
  2. Find a problematic webhook
  3. Replay while watching your logs
  4. Fix the bug, replay again

Test Edge Cases

Replay webhooks for:

  • Force pushes
  • Deleted branches
  • Large PRs with many commits
  • Merge commits

Production Tips

  1. Use idempotency: Store `x-github-delivery` IDs to prevent duplicate processing
  2. Respond fast: GitHub expects a response within 10 seconds
  3. Process async: Queue heavy work, respond immediately
  4. 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.

Create your GitHub endpoint →

Written by Skooh Team

Share:

Ready to try Skooh?

Start receiving webhooks in your local environment in under 5 minutes.

Skooh | GitHub Webhooks to an Internal Service Using Skooh (HMAC + Event Routing)