Skip to content

Commit 0da0481

Browse files
committed
feat(triggers): add Atlassian triggers for Jira, JSM, and Confluence
- Jira: add 9 new triggers (sprint created/started/closed, project created, version released, comment updated/deleted, worklog updated/deleted) - JSM: add 5 triggers from scratch (request created/updated/commented/resolved, generic webhook) - Confluence: add 7 new triggers (comment updated, attachment updated, page/blog restored, space removed, page permissions updated, user created) - Add JSM webhook provider handler with HMAC validation and changelog-based event matching - Add Atlassian webhook identifier to idempotency service for native dedup - Add extractIdempotencyId to Confluence handler - Fix Jira generic webhook to pass through full payload for non-issue events - Fix output schemas: add description (ADF), updateAuthor, note emailAddress as Jira Server only
1 parent dd9a1e3 commit 0da0481

36 files changed

+2306
-96
lines changed

apps/sim/blocks/blocks/confluence.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,17 +979,24 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
979979
...getTrigger('confluence_page_updated').subBlocks,
980980
...getTrigger('confluence_page_removed').subBlocks,
981981
...getTrigger('confluence_page_moved').subBlocks,
982+
...getTrigger('confluence_page_restored').subBlocks,
983+
...getTrigger('confluence_page_permissions_updated').subBlocks,
982984
...getTrigger('confluence_comment_created').subBlocks,
983985
...getTrigger('confluence_comment_removed').subBlocks,
986+
...getTrigger('confluence_comment_updated').subBlocks,
984987
...getTrigger('confluence_blog_created').subBlocks,
985988
...getTrigger('confluence_blog_updated').subBlocks,
986989
...getTrigger('confluence_blog_removed').subBlocks,
990+
...getTrigger('confluence_blog_restored').subBlocks,
987991
...getTrigger('confluence_attachment_created').subBlocks,
988992
...getTrigger('confluence_attachment_removed').subBlocks,
993+
...getTrigger('confluence_attachment_updated').subBlocks,
989994
...getTrigger('confluence_space_created').subBlocks,
990995
...getTrigger('confluence_space_updated').subBlocks,
996+
...getTrigger('confluence_space_removed').subBlocks,
991997
...getTrigger('confluence_label_added').subBlocks,
992998
...getTrigger('confluence_label_removed').subBlocks,
999+
...getTrigger('confluence_user_created').subBlocks,
9931000
...getTrigger('confluence_webhook').subBlocks,
9941001
],
9951002
triggers: {
@@ -999,17 +1006,24 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
9991006
'confluence_page_updated',
10001007
'confluence_page_removed',
10011008
'confluence_page_moved',
1009+
'confluence_page_restored',
1010+
'confluence_page_permissions_updated',
10021011
'confluence_comment_created',
10031012
'confluence_comment_removed',
1013+
'confluence_comment_updated',
10041014
'confluence_blog_created',
10051015
'confluence_blog_updated',
10061016
'confluence_blog_removed',
1017+
'confluence_blog_restored',
10071018
'confluence_attachment_created',
10081019
'confluence_attachment_removed',
1020+
'confluence_attachment_updated',
10091021
'confluence_space_created',
10101022
'confluence_space_updated',
1023+
'confluence_space_removed',
10111024
'confluence_label_added',
10121025
'confluence_label_removed',
1026+
'confluence_user_created',
10131027
'confluence_webhook',
10141028
],
10151029
},

apps/sim/blocks/blocks/jira.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,16 @@ Return ONLY the comment text - no explanations.`,
702702
...getTrigger('jira_issue_updated').subBlocks,
703703
...getTrigger('jira_issue_deleted').subBlocks,
704704
...getTrigger('jira_issue_commented').subBlocks,
705+
...getTrigger('jira_comment_updated').subBlocks,
706+
...getTrigger('jira_comment_deleted').subBlocks,
705707
...getTrigger('jira_worklog_created').subBlocks,
708+
...getTrigger('jira_worklog_updated').subBlocks,
709+
...getTrigger('jira_worklog_deleted').subBlocks,
710+
...getTrigger('jira_sprint_created').subBlocks,
711+
...getTrigger('jira_sprint_started').subBlocks,
712+
...getTrigger('jira_sprint_closed').subBlocks,
713+
...getTrigger('jira_project_created').subBlocks,
714+
...getTrigger('jira_version_released').subBlocks,
706715
...getTrigger('jira_webhook').subBlocks,
707716
],
708717
tools: {
@@ -1268,6 +1277,9 @@ Return ONLY the comment text - no explanations.`,
12681277
time_spent: { type: 'string', description: 'Time spent (for worklog events)' },
12691278
changelog: { type: 'json', description: 'Changelog object (for update events)' },
12701279
issue: { type: 'json', description: 'Complete issue object from webhook' },
1280+
sprint: { type: 'json', description: 'Sprint object (for sprint events)' },
1281+
project: { type: 'json', description: 'Project object (for project events)' },
1282+
version: { type: 'json', description: 'Version object (for version events)' },
12711283
jira: { type: 'json', description: 'Complete webhook payload' },
12721284
user: { type: 'json', description: 'User object who triggered the event' },
12731285
webhook: { type: 'json', description: 'Webhook metadata' },
@@ -1279,7 +1291,16 @@ Return ONLY the comment text - no explanations.`,
12791291
'jira_issue_updated',
12801292
'jira_issue_deleted',
12811293
'jira_issue_commented',
1294+
'jira_comment_updated',
1295+
'jira_comment_deleted',
12821296
'jira_worklog_created',
1297+
'jira_worklog_updated',
1298+
'jira_worklog_deleted',
1299+
'jira_sprint_created',
1300+
'jira_sprint_started',
1301+
'jira_sprint_closed',
1302+
'jira_project_created',
1303+
'jira_version_released',
12831304
'jira_webhook',
12841305
],
12851306
},

apps/sim/blocks/blocks/jira_service_management.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getScopesForService } from '@/lib/oauth/utils'
33
import type { BlockConfig } from '@/blocks/types'
44
import { AuthMode, IntegrationType } from '@/blocks/types'
55
import type { JsmResponse } from '@/tools/jsm/types'
6+
import { getTrigger } from '@/triggers'
67

78
export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
89
type: 'jira_service_management',
@@ -564,6 +565,11 @@ Return ONLY the comment text - no explanations.`,
564565
],
565566
},
566567
},
568+
...getTrigger('jsm_request_created').subBlocks,
569+
...getTrigger('jsm_request_updated').subBlocks,
570+
...getTrigger('jsm_request_commented').subBlocks,
571+
...getTrigger('jsm_request_resolved').subBlocks,
572+
...getTrigger('jsm_webhook').subBlocks,
567573
],
568574
tools: {
569575
access: [
@@ -1246,4 +1252,14 @@ Return ONLY the comment text - no explanations.`,
12461252
sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key' },
12471253
targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key' },
12481254
},
1255+
triggers: {
1256+
enabled: true,
1257+
available: [
1258+
'jsm_request_created',
1259+
'jsm_request_updated',
1260+
'jsm_request_commented',
1261+
'jsm_request_resolved',
1262+
'jsm_webhook',
1263+
],
1264+
},
12491265
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ export class IdempotencyService {
454454
normalizedHeaders?.['linear-delivery'] ||
455455
normalizedHeaders?.['greenhouse-event-id'] ||
456456
normalizedHeaders?.['x-zm-request-id'] ||
457+
normalizedHeaders?.['x-atlassian-webhook-identifier'] ||
457458
normalizedHeaders?.['idempotency-key']
458459

459460
if (webhookIdHeader) {

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const confluenceHandler: WebhookProviderHandler = {
2727
extractAttachmentData,
2828
extractSpaceData,
2929
extractLabelData,
30+
extractPagePermissionsData,
31+
extractUserData,
3032
} = await import('@/triggers/confluence/utils')
3133
const providerConfig = (webhook.providerConfig as Record<string, unknown>) || {}
3234
const triggerId = providerConfig.triggerId as string | undefined
@@ -45,6 +47,12 @@ export const confluenceHandler: WebhookProviderHandler = {
4547
if (triggerId?.startsWith('confluence_label_')) {
4648
return { input: extractLabelData(body) }
4749
}
50+
if (triggerId === 'confluence_page_permissions_updated') {
51+
return { input: extractPagePermissionsData(body) }
52+
}
53+
if (triggerId === 'confluence_user_created') {
54+
return { input: extractUserData(body) }
55+
}
4856
if (triggerId === 'confluence_webhook') {
4957
const b = body as Record<string, unknown>
5058
return {
@@ -59,12 +67,35 @@ export const confluenceHandler: WebhookProviderHandler = {
5967
space: b.space || null,
6068
label: b.label || null,
6169
content: b.content || null,
70+
user: b.user || null,
6271
},
6372
}
6473
}
6574
return { input: extractPageData(body) }
6675
},
6776

77+
extractIdempotencyId(body: unknown) {
78+
const obj = body as Record<string, unknown>
79+
const event = obj.event as string | undefined
80+
const timestamp = obj.timestamp ?? ''
81+
const page = obj.page as Record<string, unknown> | undefined
82+
const comment = obj.comment as Record<string, unknown> | undefined
83+
const attachment = obj.attachment as Record<string, unknown> | undefined
84+
const blog = (obj.blog || obj.blogpost) as Record<string, unknown> | undefined
85+
const space = obj.space as Record<string, unknown> | undefined
86+
const user = obj.user as Record<string, unknown> | undefined
87+
88+
const entityId =
89+
comment?.id || attachment?.id || blog?.id || page?.id || space?.id || user?.accountId
90+
if (event && entityId) {
91+
return `confluence:${event}:${entityId}:${timestamp}`
92+
}
93+
if (event && timestamp) {
94+
return `confluence:${event}:${timestamp}`
95+
}
96+
return null
97+
},
98+
6899
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
69100
const triggerId = providerConfig.triggerId as string | undefined
70101
const obj = body as Record<string, unknown>

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

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,8 @@ export function validateJiraSignature(secret: string, signature: string, body: s
3030
const providedSignature = signature.substring(7)
3131
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
3232
logger.debug('Jira signature comparison', {
33-
computedSignature: `${computedHash.substring(0, 10)}...`,
34-
providedSignature: `${providedSignature.substring(0, 10)}...`,
3533
computedLength: computedHash.length,
3634
providedLength: providedSignature.length,
37-
match: computedHash === providedSignature,
3835
})
3936
return safeCompare(computedHash, providedSignature)
4037
} catch (error) {
@@ -52,17 +49,64 @@ export const jiraHandler: WebhookProviderHandler = {
5249
}),
5350

5451
async formatInput({ body, webhook }: FormatInputContext): Promise<FormatInputResult> {
55-
const { extractIssueData, extractCommentData, extractWorklogData } = await import(
56-
'@/triggers/jira/utils'
57-
)
52+
const {
53+
extractIssueData,
54+
extractCommentData,
55+
extractWorklogData,
56+
extractSprintData,
57+
extractProjectData,
58+
extractVersionData,
59+
} = await import('@/triggers/jira/utils')
5860
const providerConfig = (webhook.providerConfig as Record<string, unknown>) || {}
5961
const triggerId = providerConfig.triggerId as string | undefined
60-
if (triggerId === 'jira_issue_commented') {
62+
63+
if (
64+
triggerId === 'jira_issue_commented' ||
65+
triggerId === 'jira_comment_updated' ||
66+
triggerId === 'jira_comment_deleted'
67+
) {
6168
return { input: extractCommentData(body) }
6269
}
63-
if (triggerId === 'jira_worklog_created') {
70+
if (
71+
triggerId === 'jira_worklog_created' ||
72+
triggerId === 'jira_worklog_updated' ||
73+
triggerId === 'jira_worklog_deleted'
74+
) {
6475
return { input: extractWorklogData(body) }
6576
}
77+
if (
78+
triggerId === 'jira_sprint_created' ||
79+
triggerId === 'jira_sprint_started' ||
80+
triggerId === 'jira_sprint_closed'
81+
) {
82+
return { input: extractSprintData(body) }
83+
}
84+
if (triggerId === 'jira_project_created') {
85+
return { input: extractProjectData(body) }
86+
}
87+
if (triggerId === 'jira_version_released') {
88+
return { input: extractVersionData(body) }
89+
}
90+
91+
if (!triggerId || triggerId === 'jira_webhook') {
92+
const obj = body as Record<string, unknown>
93+
return {
94+
input: {
95+
webhookEvent: obj.webhookEvent,
96+
timestamp: obj.timestamp,
97+
user: obj.user || null,
98+
issue_event_type_name: obj.issue_event_type_name,
99+
issue: obj.issue || {},
100+
changelog: obj.changelog,
101+
comment: obj.comment,
102+
worklog: obj.worklog,
103+
sprint: obj.sprint,
104+
project: obj.project,
105+
version: obj.version,
106+
},
107+
}
108+
}
109+
66110
return { input: extractIssueData(body) }
67111
},
68112

@@ -95,9 +139,16 @@ export const jiraHandler: WebhookProviderHandler = {
95139
extractIdempotencyId(body: unknown) {
96140
const obj = body as Record<string, unknown>
97141
const issue = obj.issue as Record<string, unknown> | undefined
142+
const comment = obj.comment as Record<string, unknown> | undefined
143+
const worklog = obj.worklog as Record<string, unknown> | undefined
98144
const project = obj.project as Record<string, unknown> | undefined
99-
if (obj.webhookEvent && (issue?.id || project?.id)) {
100-
return `${obj.webhookEvent}:${issue?.id || project?.id}`
145+
const sprint = obj.sprint as Record<string, unknown> | undefined
146+
const version = obj.version as Record<string, unknown> | undefined
147+
const entityId =
148+
comment?.id || worklog?.id || issue?.id || project?.id || sprint?.id || version?.id
149+
if (obj.webhookEvent && entityId) {
150+
const ts = obj.timestamp ?? ''
151+
return `${obj.webhookEvent}:${entityId}:${ts}`
101152
}
102153
return null
103154
},
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { createLogger } from '@sim/logger'
2+
import { validateJiraSignature } from '@/lib/webhooks/providers/jira'
3+
import type {
4+
EventMatchContext,
5+
FormatInputContext,
6+
FormatInputResult,
7+
WebhookProviderHandler,
8+
} from '@/lib/webhooks/providers/types'
9+
import { createHmacVerifier } from '@/lib/webhooks/providers/utils'
10+
11+
const logger = createLogger('WebhookProvider:JSM')
12+
13+
/**
14+
* Jira Service Management webhook handler.
15+
*
16+
* JSM uses the Jira webhook infrastructure. The handler reuses the same HMAC
17+
* signature validation as Jira and adds JSM-specific event matching logic
18+
* to route events to the correct trigger based on event type and changelog context.
19+
*/
20+
export const jsmHandler: WebhookProviderHandler = {
21+
verifyAuth: createHmacVerifier({
22+
configKey: 'webhookSecret',
23+
headerName: 'X-Hub-Signature',
24+
validateFn: validateJiraSignature,
25+
providerLabel: 'JSM',
26+
}),
27+
28+
async formatInput({ body, webhook }: FormatInputContext): Promise<FormatInputResult> {
29+
const { extractRequestData, extractCommentData } = await import('@/triggers/jsm/utils')
30+
const providerConfig = (webhook.providerConfig as Record<string, unknown>) || {}
31+
const triggerId = providerConfig.triggerId as string | undefined
32+
33+
if (triggerId === 'jsm_request_commented') {
34+
return { input: extractCommentData(body as Record<string, unknown>) }
35+
}
36+
37+
return { input: extractRequestData(body as Record<string, unknown>) }
38+
},
39+
40+
async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
41+
const triggerId = providerConfig.triggerId as string | undefined
42+
const obj = body as Record<string, unknown>
43+
44+
if (triggerId && triggerId !== 'jsm_webhook') {
45+
const webhookEvent = obj.webhookEvent as string | undefined
46+
const issueEventTypeName = obj.issue_event_type_name as string | undefined
47+
const changelog = obj.changelog as
48+
| { items?: Array<{ field?: string; toString?: string }> }
49+
| undefined
50+
51+
const { isJsmEventMatch } = await import('@/triggers/jsm/utils')
52+
if (!isJsmEventMatch(triggerId, webhookEvent || '', issueEventTypeName, changelog)) {
53+
logger.debug(
54+
`[${requestId}] JSM event mismatch for trigger ${triggerId}. Event: ${webhookEvent}. Skipping execution.`,
55+
{
56+
webhookId: webhook.id,
57+
workflowId: workflow.id,
58+
triggerId,
59+
receivedEvent: webhookEvent,
60+
}
61+
)
62+
return false
63+
}
64+
}
65+
66+
return true
67+
},
68+
69+
extractIdempotencyId(body: unknown) {
70+
const obj = body as Record<string, unknown>
71+
const issue = obj.issue as Record<string, unknown> | undefined
72+
const ts = obj.timestamp ?? ''
73+
if (obj.webhookEvent && issue?.id) {
74+
return `jsm:${obj.webhookEvent}:${issue.id}:${ts}`
75+
}
76+
if (obj.webhookEvent && ts) {
77+
return `jsm:${obj.webhookEvent}:${ts}`
78+
}
79+
return null
80+
},
81+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { hubspotHandler } from '@/lib/webhooks/providers/hubspot'
2020
import { imapHandler } from '@/lib/webhooks/providers/imap'
2121
import { intercomHandler } from '@/lib/webhooks/providers/intercom'
2222
import { jiraHandler } from '@/lib/webhooks/providers/jira'
23+
import { jsmHandler } from '@/lib/webhooks/providers/jsm'
2324
import { lemlistHandler } from '@/lib/webhooks/providers/lemlist'
2425
import { linearHandler } from '@/lib/webhooks/providers/linear'
2526
import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams'
@@ -65,6 +66,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
6566
imap: imapHandler,
6667
intercom: intercomHandler,
6768
jira: jiraHandler,
69+
jsm: jsmHandler,
6870
lemlist: lemlistHandler,
6971
linear: linearHandler,
7072
resend: resendHandler,

0 commit comments

Comments
 (0)