Обработка GitHub webhooks: timing-safe HMAC и дедупликация по Delivery ID
GitHub подписывает каждый вебхук `X-Hub-Signature-256`. Inquir: timing-safe проверка, дедупликация по `X-GitHub-Delivery`, маршрутизация по `X-GitHub-Event`, запуск CI/CD-пайплайна.
Last updated: 2026-04-20
Answer first
Direct answer
Обработка GitHub webhooks: timing-safe HMAC и дедупликация по Delivery ID. Функция читает `X-Hub-Signature-256`, считает HMAC, timing-safe сравнивает, проверяет Delivery ID, отвечает 200, запускает CI-пайплайн.
When it fits
- CI/CD триггер занимает больше 2 секунд
- Нужна дедупликация при GitHub retry
- Automated issue labeling, PR review requests, and release deployments
Tradeoffs
- Синхронная обработка push-события — запуск сборки занимает время, GitHub получает таймаут и делает повтор.
- Единый обработчик для всех событий без маршрутизации по `X-GitHub-Event`: pull_request приходит туда же, где push-обработчик запускает деплой.
Нагрузка и где ломается
Как ломаются GitHub webhook-обработчики
- 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 повторяет доставку при 5xx или таймауте. Без дедупликации по Delivery ID пайплайн запускается дважды.
Где костыли не спасают
Где теряются гарантии
Синхронная обработка push-события — запуск сборки занимает время, GitHub получает таймаут и делает повтор.
Единый обработчик для всех событий без маршрутизации по `X-GitHub-Event`: pull_request приходит туда же, где push-обработчик запускает деплой.
Как помогает Inquir
Надёжный GitHub webhook на Inquir
Функция читает `X-Hub-Signature-256`, считает HMAC, timing-safe сравнивает, проверяет Delivery ID, отвечает 200, запускает CI-пайплайн.
Что получаете
Ключевые части GitHub webhook-обработчика
timing-safe HMAC
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) — не string equality.
Delivery ID дедупликация
`X-GitHub-Delivery` → `INSERT … ON CONFLICT DO NOTHING` до запуска пайплайна.
Маршрутизация по событию
`X-GitHub-Event: push`, `pull_request`, `check_run` — разная логика для каждого типа.
Async CI/CD
`global.durable.startNew('build', undefined, { delivery, event, payload })` → ответить 200. Оркестрация запускает сборку.
Что делать дальше
Как обработать GitHub webhook
Проверить HMAC
HMAC-SHA256 raw body с `GITHUB_WEBHOOK_SECRET`; timing-safe сравнение с `X-Hub-Signature-256`.
Дедуплицировать
Записать `X-GitHub-Delivery` в БД; конфликт — ответить 200 без работы.
Запустить оркестрацию
`global.durable.startNew('process', undefined, { event, payload })` → `return { statusCode: 200 }`.
Пример кода
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' }; }
Когда подходит
Когда нужен async пайплайн
Когда это уместно
- CI/CD триггер занимает больше 2 секунд
- Нужна дедупликация при GitHub retry
- Automated issue labeling, PR review requests, and release deployments
Когда лучше не трогать
- Простой статус-коллектор без side-effects
FAQ
Вопросы и ответы
Как настроить GitHub webhook?
В Settings → Webhooks: URL шлюза Inquir, Content type: application/json, secret — значение `GITHUB_WEBHOOK_SECRET`.
Поддерживается GitHub App вместо PAT?
Да. Webhook secret тот же. Разница — в том, как вы вызываете GitHub API из пайплайна.