Skip to content

Webhook Routing

How GitWire dispatches incoming GitHub webhooks to handler functions.

The Problem

The original webhook router was a single function in routes/webhooks.js with cyclomatic complexity 46. Every event type, action filter, and queue dispatch lived in one giant switch-case.

The Solution: Handler Registry

The router was refactored (v0.13.0) using the extract-dispatch-delegate pattern:

  1. Extract — each event type's logic moved to its own file in lib/webhookHandlers/
  2. Dispatch — a registry lookup replaces the switch-case
  3. Delegate — each handler focuses on a single event type

Registry Pattern

javascript
// lib/webhookHandlers/index.js
const handlers = {
  issues:               handleIssues,
  pull_request:         handlePullRequest,
  pull_request_review:  handlePullRequestReview,
  check_suite:          handleCheckSuite,
  workflow_run:         handleWorkflowRun,
  installation:         handleInstallation,
  installation_repositories: handleInstallation,
  push:                 handlePush,
  issue_comment:        handleIssueComment,
};

export async function routeWebhookToQueue(eventName, payload, deliveryId) {
  const handler = handlers[eventName];
  if (handler) {
    await handler(payload, deliveryId, ctx);
  } else {
    await ctx.webhookQueue.add("generic-event", jobData);
  }
}

The main dispatcher is now CC ~3 — just a lookup and try/catch.

Handler Files

lib/webhookHandlers/
├── index.js                    # Registry + dispatcher
├── handleIssues.js             # issues → triage queue
├── handlePullRequest.js        # pull_request → triage + phase4 + reconcile
├── handlePullRequestReview.js  # pull_request_review → phase2 (merge queue)
├── handleCheckSuite.js         # check_suite → phase2 check
├── handleWorkflowRun.js        # workflow_run → ci-heal + phase2 + rollback
├── handleInstallation.js       # installation → sync queue
├── handlePush.js               # push → config cache invalidation
├── handleIssueComment.js       # issue_comment → comment command router
└── commentCommands/
    ├── handleManualRun.js      # /gitwire run [pillar]
    ├── handleFixCommand.js     # /gitwire fix
    └── handleWaiverCommand.js  # /gitwire waive/unwaive

Shared Context

Each handler receives a shared ctx object instead of importing queues directly:

FieldSourcePurpose
triageQueueBullMQTriage jobs
ciHealQueueBullMQCI healing jobs
issueFixQueueBullMQIssue fix pipeline
phase2QueueBullMQMerge queue + error recovery
phase3QueueBullMQTrust + deps + policy
phase4QueueBullMQAI review + audit
redisioredisIdempotency keys, caching
dbpgDatabase queries
loggerpinoStructured logging
getInstallationClient@octokit/appPer-installation GitHub client
wrapOctokitgithubWrapper.jsCache + rate limit + sanitize wrapper

Why This Pattern

Before (monolith)After (registry)
CC 46CC ~3 (dispatcher), CC 3-8 (handlers)
548 lines in one file~60 lines per handler
Any change risks all eventsChanges isolated to one handler
Hard to test individual eventsEach handler testable independently

Webhook Worker | Data Flow | Comment Commands

Last validated: v0.13.0

Released under the MIT License.