Обработка вебхуков GitHub: timing-safe HMAC и дедупликация доставок
GitHub подписывает каждый вебхук заголовком <code>X-Hub-Signature-256</code>. На Inquir паттерн выглядит так: timing-safe проверка подписи, дедупликация по <code>X-GitHub-Delivery</code>, маршрутизация по <code>X-GitHub-Event</code> и запуск CI/CD-пайплайна.
Обновлено: 2026-06-28
Кратко
Суть ответа
Обработка вебхуков GitHub: timing-safe HMAC и дедупликация доставок. Функция читает 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 с timing-safe сравнением — стандарт, но легко ошибиться
- Десятки типов событий в одном endpoint — логика маршрутизации быстро разрастается
- Тяжёлая работа (индексация, CI) в окне webhook → таймаут доставки GitHub
GitHub ретраит неудачные доставки до 3 дней. Без быстрого ACK и идемпотентной обработки один и тот же push может дважды запустить CI или проиндексировать коммит.
Когда простых рецептов недостаточно
Что ломается при повторных доставках и долгом 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
Сравните hex-дайджесты через crypto.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex")) — не string equality и не Buffer.from без кодировки.
Маршрутизация по событию
X-GitHub-Event: push, pull_request, check_run — разная логика для каждого типа.
Delivery ID дедупликация
X-GitHub-Delivery → upsert до запуска пайплайна; конфликт — 200 без работы.
Async fan-out
global.durable.startNew() до return { statusCode: 200 } — CI/CD, индексация и уведомления вне HTTP-окна.
Что сделать дальше, по шагам
Как обработать 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 из пайплайна.