Process GitHub webhooks serverlessly for CI/CD and automation
GitHub webhooks fire on push, pull_request, release, issues, and more. Build a serverless processor that verifies HMAC-SHA256, routes events by type, and triggers async pipelines for CI/CD, repo indexing, and issue automation.
Last updated: 2026-04-20
Answer first
Direct answer
Process GitHub webhooks serverlessly for CI/CD and automation. One function verifies the signature and routes by event type to dedicated pipeline jobs. Push events trigger indexing; pull_request events trigger review automation; release events trigger deployment pipelines.
When it fits
- CI/CD triggers on push and PR events
- Repo indexing, documentation sync, and search index updates on push
- Automated issue labeling, PR review requests, and release deployments
Tradeoffs
- Running repo indexing, vector embedding, or CI job dispatch synchronously before returning means any latency in those systems makes GitHub retry your endpoint.
- Handling all event types in one handler creates a monolith that is hard to test, hard to deploy, and easy to break.
Workload and what breaks
GitHub webhook challenges
- HMAC-SHA256 with timing-safe comparison — standard but easy to get wrong
- Dozens of event types in one endpoint — routing logic grows fast
- Heavy work (indexing, CI triggers) inside the webhook window causes GitHub delivery timeouts
GitHub retries failed webhook deliveries up to 3 days. Without a fast ACK and idempotent event handling, you can end up triggering the same CI job or indexing the same commit multiple times.
Where shortcuts fail
Why inline GitHub event processing is fragile
Running repo indexing, vector embedding, or CI job dispatch synchronously before returning means any latency in those systems makes GitHub retry your endpoint.
Handling all event types in one handler creates a monolith that is hard to test, hard to deploy, and easy to break.
How Inquir helps
Verify, route, fan-out — the GitHub webhook pattern
One function verifies the signature and routes by event type to dedicated pipeline jobs. Push events trigger indexing; pull_request events trigger review automation; release events trigger deployment pipelines.
What you get
GitHub webhook handling features
HMAC-SHA256 timing-safe verification
Use crypto.timingSafeEqual—never string comparison—to prevent timing attacks on signature validation.
Event type routing
x-github-event header identifies the event. Route to different pipeline functions per event type.
Delivery ID deduplication
Use x-github-delivery header as idempotency key to skip already-processed deliveries on retry.
Async fan-out
One push event can trigger parallel pipelines: index docs, run linter, update search, notify Slack.
What to do next
GitHub webhook flow
Verify HMAC-SHA256 timing-safely
Extract x-hub-signature-256, compute expected HMAC, compare with timingSafeEqual.
Route by event type, return 200
Switch on x-github-event header. Trigger appropriate pipeline. Return fast—before pipelines complete.
Process in pipelines
Each pipeline step runs independently with retries. Deduplication keys prevent double-processing on retry.
Code example
GitHub push and PR webhook handler
Verify HMAC, route by event type, trigger async pipelines—all before returning 200 to GitHub.
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sigHeader = (event.headers['x-hub-signature-256'] ?? '').replace('sha256=', ''); const expected = createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET).update(body).digest('hex'); if (sigHeader.length !== expected.length || !timingSafeEqual(Buffer.from(sigHeader, 'hex'), Buffer.from(expected, 'hex'))) { return { statusCode: 401, body: 'invalid signature' }; } const eventType = event.headers['x-github-event']; const deliveryId = event.headers['x-github-delivery']; const payload = JSON.parse(body); const isNew = await db.webhookDeliveries.upsert(deliveryId); if (!isNew) return { statusCode: 200, body: 'duplicate' }; if (eventType === 'push') { await global.durable.startNew('index-repo', undefined, { repo: payload.repository.full_name, sha: payload.after }); } else if (eventType === 'pull_request' && payload.action === 'opened') { await global.durable.startNew('review-pr', undefined, { repo: payload.repository.full_name, pr: payload.number }); } else if (eventType === 'release' && payload.action === 'published') { await global.durable.startNew('deploy-release', undefined, { repo: payload.repository.full_name, tag: payload.release.tag_name }); } return { statusCode: 200, body: 'accepted' }; }
When it fits
Use this for GitHub automation
When this works
- CI/CD triggers on push and PR events
- Repo indexing, documentation sync, and search index updates on push
- Automated issue labeling, PR review requests, and release deployments
When to skip it
- Simple GitHub Actions workflows that do not need external serverless processing
FAQ
FAQ
How do I test locally?
Use gh webhook forward --repo owner/repo --events push --url http://localhost:PORT to forward real events to a local function.
What about pull_request review events?
GitHub sends many sub-actions (submitted, dismissed, edited). Always check payload.action to route correctly.