Сценарий · Inquir Compute

Обработка вебхуков 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-обработчик запускает деплой.

Надёжный 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

1

Проверить HMAC

HMAC-SHA256 raw body с GITHUB_WEBHOOK_SECRET; timing-safe сравнение с X-Hub-Signature-256.

2

Дедуплицировать

Записать X-GitHub-Delivery в БД; конфликт — ответить 200 без работы.

3

Запустить оркестрацию

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.

webhooks/github.mjs
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 из пайплайна.