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

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

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

Когда для 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 из пайплайна.

Inquir Compute logoInquir Compute

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

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

Привет, это команда Inquir Compute. Спроси AI о продукте:

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