GitHub Webhook Integration
This document covers how to configure and use the GitHub webhook integration in izri to automatically trigger repository analysis on push events.
Overview
When a GitHub push event is delivered to the webhook endpoint, izri:
- Validates the HMAC-SHA256 signature using the project's configured secret.
- Checks whether the push matches the configured trigger conditions (branch list, file patterns).
- Creates a
webhook_eventsrecord for audit/retry tracking. - If conditions match, creates a
project_analysesrecord and queues repository analysis.
Webhook endpoint
POST /api/webhooks/github/:projectId
| Parameter | Description |
|---|---|
projectId |
The izri project ULID (from the UI) |
Required headers (sent automatically by GitHub)
| Header | Description |
|---|---|
X-Hub-Signature-256 |
HMAC-SHA256 signature of the body |
X-GitHub-Event |
Event type (only push is processed) |
X-GitHub-Delivery |
Unique delivery UUID for idempotency |
Content-Type |
application/json |
Setting up a webhook in GitHub
- Navigate to your GitHub repository → Settings → Webhooks → Add webhook.
- Set Payload URL to:
https://<your-api-host>/api/webhooks/github/<projectId> - Set Content type to
application/json. - Enter the Secret generated by izri (see Configuring via tRPC below).
- Under Which events would you like to trigger this webhook?, choose Just the
pushevent (other events are acknowledged but not acted on). - Click Add webhook.
Configuring via tRPC
All webhook management is exposed through the webhooks tRPC router.
Create / update webhook config
const result = await trpc.webhooks.configure.mutate({
projectId: '01JXXXXXXXXXXXXXXXXXXXXXXX',
branches: ['main', 'develop'], // empty = all branches
filePatterns: ['src/**/*.ts'], // empty = any file
})
// result.secret ← copy this into your GitHub webhook settings (shown once only)
// result.configId
Get current config (secret excluded)
const config = await trpc.webhooks.getConfig.query({
projectId: '01JXXXXXXXXXXXXXXXXXXXXXXX',
})
// { active, branches, filePatterns, configId, createdAt, updatedAt }
Enable / disable webhook
await trpc.webhooks.setActive.mutate({
projectId: '01JXXXXXXXXXXXXXXXXXXXXXXX',
active: false,
})
Rotate secret
If you suspect your secret has been compromised, rotate it. After rotating, update the secret in the GitHub webhook settings.
const { secret } = await trpc.webhooks.rotateSecret.mutate({
projectId: '01JXXXXXXXXXXXXXXXXXXXXXXX',
})
// Update GitHub webhook settings with the new secret
List delivery events
const { events } = await trpc.webhooks.listEvents.query({
projectId: '01JXXXXXXXXXXXXXXXXXXXXXXX',
limit: 20,
// optional: status: 'FAILED' | 'PROCESSED' | 'PENDING' | 'SKIPPED' | 'ABANDONED'
})
Delete webhook config
await trpc.webhooks.delete.mutate({
projectId: '01JXXXXXXXXXXXXXXXXXXXXXXX',
})
Trigger conditions
Branch filter
| Config | Behaviour |
|---|---|
branches: [] |
Trigger on all branches |
branches: ['main', 'develop'] |
Trigger only on those branches |
File pattern filter
Patterns use * (single-directory wildcard) and ** (cross-directory wildcard).
| Pattern | Matches |
|---|---|
src/**/*.ts |
Any .ts file under src/ |
*.json |
Any .json file at the root |
packages/api/** |
Any file under packages/api/ |
| (empty array) | Any changed file — always triggers |
The filter is evaluated against the union of added, modified, and removed files across all commits in the push.
Security
- Signatures are validated using HMAC-SHA256 (Web Crypto API; no native addon required).
- Comparison uses a constant-time byte-by-byte XOR check to prevent timing attacks.
- Deliveries without a valid
X-Hub-Signature-256header are rejected with HTTP 401. - Duplicate deliveries (same
X-GitHub-Delivery) are idempotent — the second delivery is ignored.
Error handling and retry logic
Failed webhook events are retried up to 3 times with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1st | 1 minute |
| 2nd | 5 minutes |
| 3rd | 15 minutes |
After 3 failed attempts the event is marked ABANDONED.
The retry function retryFailedWebhookEvents() should be called by a periodic background job (e.g. every minute):
import { retryFailedWebhookEvents } from '@izri/trpc/services/webhookService'
// In your cron / background worker:
const { retried, abandoned } = await retryFailedWebhookEvents()
Event status reference
| Status | Description |
|---|---|
PENDING |
Received but not yet processed |
PROCESSED |
Successfully triggered repository analysis |
SKIPPED |
Trigger conditions not met (branch / file filter) |
FAILED |
Processing failed; will be retried |
ABANDONED |
Max retries exceeded |
Database tables
webhook_configs
Stores one configuration per project.
| Column | Type | Description |
|---|---|---|
id |
varchar(191) | ULID primary key |
project_id |
varchar(191) | References projects.id |
secret |
varchar(255) | HMAC secret (stored hashed in future) |
branches |
jsonb | Branch allowlist (string[]) |
file_patterns |
jsonb | Glob pattern allowlist (string[]) |
active |
boolean | Whether deliveries are processed |
created_at |
timestamp | |
updated_at |
timestamp |
webhook_events
Audit log of every delivery received.
| Column | Type | Description |
|---|---|---|
id |
varchar(191) | ULID primary key |
project_id |
varchar(191) | References projects.id |
delivery_id |
varchar(255) | GitHub's X-GitHub-Delivery UUID |
event_type |
varchar(64) | e.g. push |
payload |
jsonb | Full GitHub webhook payload |
status |
varchar(32) | See status reference above |
error |
text | Error message (if failed) |
attempts |
integer | Number of processing attempts |
next_retry_at |
timestamp | When to next retry (if FAILED) |
processed_at |
timestamp | When successfully processed |
created_at |
timestamp |