Обработка GitHub webhooks: timing-safe HMAC и дедупликация по Delivery ID
GitHub подписывает каждый вебхук `X-Hub-Signature-256`. Inquir: timing-safe проверка, дедупликация по `X-GitHub-Delivery`, маршрутизация по `X-GitHub-Event`, запуск CI/CD-пайплайна.
Обновлено: 2026-04-20
Кратко
Суть ответа
Обработка GitHub webhooks: timing-safe HMAC и дедупликация по Delivery ID. Функция читает `X-Hub-Signature-256`, считает HMAC, timing-safe сравнивает, проверяет Delivery ID, отвечает 200, запускает CI-пайплайн.
Когда подходит и когда нет
- CI/CD триггер занимает больше 2 секунд
- Нужна дедупликация при GitHub retry
- Automated issue labeling, PR review requests, and release deployments
На что обратить внимание
- Долгий CI/CD или деплой внутри одного HTTP-запроса упирается в таймаут 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 пайплайн запускается дважды.
Когда упрощённых рецептов мало
Что ломается в обработчиках GitHub webhooks при повторных доставках, долгом CI и смешении типов событий в одном коде
Долгий CI/CD или деплой внутри одного HTTP-запроса упирается в таймаут 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' }; }
Когда подходит и когда нет
Когда для GitHub webhooks нужен асинхронный пайплайн после быстрого ответа 200 OK
Когда это уместно
- CI/CD триггер занимает больше 2 секунд
- Нужна дедупликация при GitHub retry
- Automated issue labeling, PR review requests, and release deployments
Когда лучше не трогать
- Простой статус-коллектор без side-effects
Вопросы и ответы
Вопросы и ответы
Как настроить GitHub webhook?
В Settings → Webhooks: URL шлюза Inquir, Content type: application/json, secret — значение `GITHUB_WEBHOOK_SECRET`.
Поддерживается GitHub App вместо PAT?
Да. Webhook secret тот же. Разница — в том, как вы вызываете GitHub API из пайплайна.