Skip to content

Commit fd4de8d

Browse files
committed
feat(triggers): add GitLab, PagerDuty, and Zendesk webhook triggers
Add webhook trigger support for three integrations that previously had blocks but no triggers: - GitLab: push, merge request, issue, pipeline, comment, and all-events. Verifies the X-Gitlab-Token secret token; filters by object_kind. - PagerDuty: incident triggered/acknowledged/resolved/escalated/reassigned and all-events. Verifies X-PagerDuty-Signature (HMAC-SHA256 over raw body, comma-separated rotation); idempotency on event id. - Zendesk: ticket created/status changed/comment added/priority changed and all-events. Verifies X-Zendesk-Webhook-Signature (base64 HMAC-SHA256 over timestamp+body); idempotency on event id. Register GitLab's X-Gitlab-Event-UUID delivery header for webhook idempotency dedup.
1 parent 1248f8e commit fd4de8d

32 files changed

Lines changed: 1392 additions & 1 deletion

apps/sim/blocks/blocks/gitlab.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { GitLabIcon } from '@/components/icons'
22
import type { BlockConfig, BlockMeta } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
44
import type { GitLabResponse } from '@/tools/gitlab/types'
5+
import { getTrigger } from '@/triggers'
56

67
export const GitLabBlock: BlockConfig<GitLabResponse> = {
78
type: 'gitlab',
89
name: 'GitLab',
910
description: 'Interact with GitLab projects, issues, merge requests, and pipelines',
1011
authMode: AuthMode.ApiKey,
11-
triggerAllowed: false,
12+
triggerAllowed: true,
1213
longDescription:
1314
'Integrate GitLab into the workflow. Can manage projects, issues, merge requests, pipelines, and add comments. Supports all core GitLab DevOps operations.',
1415
docsLink: 'https://docs.sim.ai/integrations/gitlab',
@@ -437,6 +438,12 @@ Return ONLY the commit message - no explanations, no extra text.`,
437438
],
438439
},
439440
},
441+
...getTrigger('gitlab_push').subBlocks,
442+
...getTrigger('gitlab_merge_request').subBlocks,
443+
...getTrigger('gitlab_issue').subBlocks,
444+
...getTrigger('gitlab_pipeline').subBlocks,
445+
...getTrigger('gitlab_comment').subBlocks,
446+
...getTrigger('gitlab_webhook').subBlocks,
440447
],
441448
tools: {
442449
access: [
@@ -746,6 +753,18 @@ Return ONLY the commit message - no explanations, no extra text.`,
746753
// Success indicator
747754
success: { type: 'boolean', description: 'Operation success status' },
748755
},
756+
757+
triggers: {
758+
enabled: true,
759+
available: [
760+
'gitlab_push',
761+
'gitlab_merge_request',
762+
'gitlab_issue',
763+
'gitlab_pipeline',
764+
'gitlab_comment',
765+
'gitlab_webhook',
766+
],
767+
},
749768
}
750769

751770
export const GitLabBlockMeta = {

apps/sim/blocks/blocks/pagerduty.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { PagerDutyIcon } from '@/components/icons'
22
import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types'
3+
import { getTrigger } from '@/triggers'
34

45
export const PagerDutyBlock: BlockConfig = {
56
type: 'pagerduty',
67
name: 'PagerDuty',
78
description: 'Manage incidents and on-call schedules with PagerDuty',
9+
triggerAllowed: true,
810
longDescription:
911
'Integrate PagerDuty into your workflow to list, create, and update incidents, add notes, list services, and check on-call schedules.',
1012
docsLink: 'https://docs.sim.ai/integrations/pagerduty',
@@ -315,6 +317,12 @@ export const PagerDutyBlock: BlockConfig = {
315317
generationType: 'timestamp',
316318
},
317319
},
320+
...getTrigger('pagerduty_incident_triggered').subBlocks,
321+
...getTrigger('pagerduty_incident_acknowledged').subBlocks,
322+
...getTrigger('pagerduty_incident_resolved').subBlocks,
323+
...getTrigger('pagerduty_incident_escalated').subBlocks,
324+
...getTrigger('pagerduty_incident_reassigned').subBlocks,
325+
...getTrigger('pagerduty_webhook').subBlocks,
318326
],
319327

320328
tools: {
@@ -481,6 +489,18 @@ export const PagerDutyBlock: BlockConfig = {
481489
description: 'Array of on-call entries (list_oncalls)',
482490
},
483491
},
492+
493+
triggers: {
494+
enabled: true,
495+
available: [
496+
'pagerduty_incident_triggered',
497+
'pagerduty_incident_acknowledged',
498+
'pagerduty_incident_resolved',
499+
'pagerduty_incident_escalated',
500+
'pagerduty_incident_reassigned',
501+
'pagerduty_webhook',
502+
],
503+
},
484504
}
485505

486506
export const PagerDutyBlockMeta = {

apps/sim/blocks/blocks/zendesk.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { ZendeskIcon } from '@/components/icons'
22
import type { BlockConfig, BlockMeta } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
4+
import { getTrigger } from '@/triggers'
45

56
export const ZendeskBlock: BlockConfig = {
67
type: 'zendesk',
78
name: 'Zendesk',
89
description: 'Manage support tickets, users, and organizations in Zendesk',
10+
triggerAllowed: true,
911
longDescription:
1012
'Integrate Zendesk into the workflow. Can get tickets, get ticket, create ticket, create tickets bulk, update ticket, update tickets bulk, delete ticket, merge tickets, get users, get user, get current user, search users, create user, create users bulk, update user, update users bulk, delete user, get organizations, get organization, autocomplete organizations, create organization, create organizations bulk, update organization, delete organization, search, search count.',
1113
docsLink: 'https://docs.sim.ai/integrations/zendesk',
@@ -529,6 +531,11 @@ Return ONLY the search query - no explanations.`,
529531
},
530532
mode: 'advanced',
531533
},
534+
...getTrigger('zendesk_ticket_created').subBlocks,
535+
...getTrigger('zendesk_ticket_status_changed').subBlocks,
536+
...getTrigger('zendesk_ticket_comment_added').subBlocks,
537+
...getTrigger('zendesk_ticket_priority_changed').subBlocks,
538+
...getTrigger('zendesk_webhook').subBlocks,
532539
],
533540
tools: {
534541
access: [
@@ -695,6 +702,17 @@ Return ONLY the search query - no explanations.`,
695702
// Metadata (shared across all operations)
696703
metadata: { type: 'json', description: 'Operation metadata including operation type' },
697704
},
705+
706+
triggers: {
707+
enabled: true,
708+
available: [
709+
'zendesk_ticket_created',
710+
'zendesk_ticket_status_changed',
711+
'zendesk_ticket_comment_added',
712+
'zendesk_ticket_priority_changed',
713+
'zendesk_webhook',
714+
],
715+
},
698716
}
699717

700718
export const ZendeskBlockMeta = {

apps/sim/lib/core/idempotency/service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ export class IdempotencyService {
493493
normalizedHeaders?.['x-webhook-id'] ||
494494
normalizedHeaders?.['x-shopify-webhook-id'] ||
495495
normalizedHeaders?.['x-github-delivery'] ||
496+
normalizedHeaders?.['x-gitlab-event-uuid'] ||
496497
normalizedHeaders?.['x-event-id'] ||
497498
normalizedHeaders?.['x-teams-notification-id'] ||
498499
normalizedHeaders?.['svix-id'] ||
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { createLogger } from '@sim/logger'
2+
import { safeCompare } from '@sim/security/compare'
3+
import { NextResponse } from 'next/server'
4+
import type {
5+
AuthContext,
6+
EventMatchContext,
7+
FormatInputContext,
8+
FormatInputResult,
9+
WebhookProviderHandler,
10+
} from '@/lib/webhooks/providers/types'
11+
12+
const logger = createLogger('WebhookProvider:GitLab')
13+
14+
function asRecord(value: unknown): Record<string, unknown> {
15+
return (value as Record<string, unknown>) || {}
16+
}
17+
18+
export const gitlabHandler: WebhookProviderHandler = {
19+
/**
20+
* GitLab echoes the configured "Secret token" verbatim in the `X-Gitlab-Token`
21+
* header (plain equality, not an HMAC). Skip verification when no token is set.
22+
*/
23+
verifyAuth({ request, requestId, providerConfig }: AuthContext) {
24+
const secret = providerConfig.webhookSecret as string | undefined
25+
if (!secret) {
26+
return null
27+
}
28+
29+
const token = request.headers.get('X-Gitlab-Token')
30+
if (!token) {
31+
logger.warn(`[${requestId}] GitLab webhook missing X-Gitlab-Token header`)
32+
return new NextResponse('Unauthorized - Missing GitLab token', { status: 401 })
33+
}
34+
35+
if (!safeCompare(token, secret)) {
36+
logger.warn(`[${requestId}] GitLab token verification failed`)
37+
return new NextResponse('Unauthorized - Invalid GitLab token', { status: 401 })
38+
}
39+
40+
return null
41+
},
42+
43+
async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {
44+
const triggerId = providerConfig.triggerId as string | undefined
45+
if (!triggerId || triggerId === 'gitlab_webhook') return true
46+
47+
const objectKind = asRecord(body).object_kind as string | undefined
48+
49+
const { isGitLabEventMatch } = await import('@/triggers/gitlab/utils')
50+
if (!isGitLabEventMatch(triggerId, objectKind || '')) {
51+
logger.debug(
52+
`[${requestId}] GitLab event '${objectKind}' does not match trigger ${triggerId}, skipping`
53+
)
54+
return false
55+
}
56+
return true
57+
},
58+
59+
async formatInput({ body, headers }: FormatInputContext): Promise<FormatInputResult> {
60+
const b = asRecord(body)
61+
const eventType = headers['x-gitlab-event'] || ''
62+
const ref = (b.ref as string) || ''
63+
const branch = ref.replace('refs/heads/', '')
64+
return {
65+
input: { ...b, event_type: eventType, branch },
66+
}
67+
},
68+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import crypto from 'crypto'
2+
import { createLogger } from '@sim/logger'
3+
import { safeCompare } from '@sim/security/compare'
4+
import type {
5+
EventMatchContext,
6+
FormatInputContext,
7+
FormatInputResult,
8+
WebhookProviderHandler,
9+
} from '@/lib/webhooks/providers/types'
10+
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
11+
12+
const logger = createLogger('WebhookProvider:PagerDuty')
13+
14+
/**
15+
* PagerDuty V3 signs the raw body with HMAC-SHA256 and sends it in the
16+
* `X-PagerDuty-Signature` header as one or more comma-separated `v1=<hex>`
17+
* values (multiple appear during signing-secret rotation). The delivery is
18+
* valid when our computed signature matches any of them.
19+
*/
20+
function validatePagerDutySignature(secret: string, signature: string, body: string): boolean {
21+
if (!secret || !signature || !body) return false
22+
const computed = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
23+
return signature
24+
.split(',')
25+
.map((part) => part.trim())
26+
.filter((part) => part.startsWith('v1='))
27+
.some((part) => safeCompare(part.slice(3), computed))
28+
}
29+
30+
function asRecord(value: unknown): Record<string, unknown> {
31+
return (value as Record<string, unknown>) || {}
32+
}
33+
34+
function referenceSummary(
35+
value: unknown
36+
): { id?: unknown; summary?: unknown; html_url?: unknown } | null {
37+
if (!value || typeof value !== 'object') return null
38+
const ref = value as Record<string, unknown>
39+
return { id: ref.id, summary: ref.summary, html_url: ref.html_url }
40+
}
41+
42+
export const pagerdutyHandler: WebhookProviderHandler = {
43+
verifyAuth: createHmacVerifier({
44+
configKey: 'webhookSecret',
45+
headerName: 'X-PagerDuty-Signature',
46+
validateFn: validatePagerDutySignature,
47+
providerLabel: 'PagerDuty',
48+
}),
49+
50+
async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {
51+
const triggerId = providerConfig.triggerId as string | undefined
52+
if (!triggerId || triggerId === 'pagerduty_webhook') return true
53+
54+
const event = asRecord(asRecord(body).event)
55+
const eventType = event.event_type as string | undefined
56+
57+
const { isPagerDutyEventMatch } = await import('@/triggers/pagerduty/utils')
58+
if (!isPagerDutyEventMatch(triggerId, eventType || '')) {
59+
logger.debug(
60+
`[${requestId}] PagerDuty event '${eventType}' does not match trigger ${triggerId}, skipping`
61+
)
62+
return false
63+
}
64+
return true
65+
},
66+
67+
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
68+
const event = asRecord(asRecord(body).event)
69+
const data = asRecord(event.data)
70+
const priority = referenceSummary(data.priority)
71+
72+
return {
73+
input: {
74+
event_id: event.id,
75+
event_type: event.event_type,
76+
occurred_at: event.occurred_at,
77+
agent: event.agent ?? null,
78+
incident: {
79+
id: data.id,
80+
number: data.number,
81+
title: data.title,
82+
status: data.status,
83+
urgency: data.urgency,
84+
html_url: data.html_url,
85+
created_at: data.created_at,
86+
priority: priority?.summary ?? null,
87+
service: referenceSummary(data.service),
88+
escalation_policy: referenceSummary(data.escalation_policy),
89+
assignees: Array.isArray(data.assignees) ? data.assignees : [],
90+
},
91+
},
92+
}
93+
},
94+
95+
extractIdempotencyId(body: unknown) {
96+
const event = asRecord(asRecord(body).event)
97+
return (event.id as string | undefined) || null
98+
},
99+
}

apps/sim/lib/webhooks/providers/registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { fathomHandler } from '@/lib/webhooks/providers/fathom'
1313
import { firefliesHandler } from '@/lib/webhooks/providers/fireflies'
1414
import { genericHandler } from '@/lib/webhooks/providers/generic'
1515
import { githubHandler } from '@/lib/webhooks/providers/github'
16+
import { gitlabHandler } from '@/lib/webhooks/providers/gitlab'
1617
import { gmailHandler } from '@/lib/webhooks/providers/gmail'
1718
import { gongHandler } from '@/lib/webhooks/providers/gong'
1819
import { googleFormsHandler } from '@/lib/webhooks/providers/google-forms'
@@ -29,6 +30,7 @@ import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams'
2930
import { mondayHandler } from '@/lib/webhooks/providers/monday'
3031
import { notionHandler } from '@/lib/webhooks/providers/notion'
3132
import { outlookHandler } from '@/lib/webhooks/providers/outlook'
33+
import { pagerdutyHandler } from '@/lib/webhooks/providers/pagerduty'
3234
import { resendHandler } from '@/lib/webhooks/providers/resend'
3335
import { rssHandler } from '@/lib/webhooks/providers/rss'
3436
import { salesforceHandler } from '@/lib/webhooks/providers/salesforce'
@@ -46,6 +48,7 @@ import { verifyTokenAuth } from '@/lib/webhooks/providers/utils'
4648
import { vercelHandler } from '@/lib/webhooks/providers/vercel'
4749
import { webflowHandler } from '@/lib/webhooks/providers/webflow'
4850
import { whatsappHandler } from '@/lib/webhooks/providers/whatsapp'
51+
import { zendeskHandler } from '@/lib/webhooks/providers/zendesk'
4952
import { zoomHandler } from '@/lib/webhooks/providers/zoom'
5053

5154
const logger = createLogger('WebhookProviderRegistry')
@@ -64,6 +67,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
6467
generic: genericHandler,
6568
gmail: gmailHandler,
6669
github: githubHandler,
70+
gitlab: gitlabHandler,
6771
gong: gongHandler,
6872
google_forms: googleFormsHandler,
6973
fathom: fathomHandler,
@@ -81,6 +85,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
8185
'microsoft-teams': microsoftTeamsHandler,
8286
notion: notionHandler,
8387
outlook: outlookHandler,
88+
pagerduty: pagerdutyHandler,
8489
rss: rssHandler,
8590
salesforce: salesforceHandler,
8691
sendblue: sendblueHandler,
@@ -95,6 +100,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
95100
vercel: vercelHandler,
96101
webflow: webflowHandler,
97102
whatsapp: whatsappHandler,
103+
zendesk: zendeskHandler,
98104
zoom: zoomHandler,
99105
}
100106

0 commit comments

Comments
 (0)