Inquir Compute logoInquir Compute
Сценарий · Inquir Compute

Обработка 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

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

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

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' };
}

Когда нужен async пайплайн

Когда это уместно

  • 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 из пайплайна.

Inquir Compute logoInquir Compute

Самый простой способ запускать AI-агентов и backend-джобы без инфраструктуры.

Связаться info@inquir.org

© 2025 Inquir Compute. Все права защищены.