Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion apps/sim/blocks/blocks/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { GitLabIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import type { GitLabResponse } from '@/tools/gitlab/types'
import { getTrigger } from '@/triggers'

export const GitLabBlock: BlockConfig<GitLabResponse> = {
type: 'gitlab',
name: 'GitLab',
description: 'Interact with GitLab projects, issues, merge requests, and pipelines',
authMode: AuthMode.ApiKey,
triggerAllowed: false,
triggerAllowed: true,
longDescription:
'Integrate GitLab into the workflow. Can manage projects, issues, merge requests, pipelines, and add comments. Supports all core GitLab DevOps operations.',
docsLink: 'https://docs.sim.ai/integrations/gitlab',
Expand Down Expand Up @@ -437,6 +438,12 @@ Return ONLY the commit message - no explanations, no extra text.`,
],
},
},
...getTrigger('gitlab_push').subBlocks,
...getTrigger('gitlab_merge_request').subBlocks,
...getTrigger('gitlab_issue').subBlocks,
...getTrigger('gitlab_pipeline').subBlocks,
...getTrigger('gitlab_comment').subBlocks,
...getTrigger('gitlab_webhook').subBlocks,
],
tools: {
access: [
Expand Down Expand Up @@ -746,6 +753,18 @@ Return ONLY the commit message - no explanations, no extra text.`,
// Success indicator
success: { type: 'boolean', description: 'Operation success status' },
},

triggers: {
enabled: true,
available: [
'gitlab_push',
'gitlab_merge_request',
'gitlab_issue',
'gitlab_pipeline',
'gitlab_comment',
'gitlab_webhook',
],
},
}

export const GitLabBlockMeta = {
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/blocks/blocks/pagerduty.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PagerDutyIcon } from '@/components/icons'
import { AuthMode, type BlockConfig, type BlockMeta, IntegrationType } from '@/blocks/types'
import { getTrigger } from '@/triggers'

export const PagerDutyBlock: BlockConfig = {
type: 'pagerduty',
name: 'PagerDuty',
description: 'Manage incidents and on-call schedules with PagerDuty',
triggerAllowed: true,
longDescription:
'Integrate PagerDuty into your workflow to list, create, and update incidents, add notes, list services, and check on-call schedules.',
docsLink: 'https://docs.sim.ai/integrations/pagerduty',
Expand Down Expand Up @@ -315,6 +317,12 @@ export const PagerDutyBlock: BlockConfig = {
generationType: 'timestamp',
},
},
...getTrigger('pagerduty_incident_triggered').subBlocks,
...getTrigger('pagerduty_incident_acknowledged').subBlocks,
...getTrigger('pagerduty_incident_resolved').subBlocks,
...getTrigger('pagerduty_incident_escalated').subBlocks,
...getTrigger('pagerduty_incident_reassigned').subBlocks,
...getTrigger('pagerduty_webhook').subBlocks,
],

tools: {
Expand Down Expand Up @@ -481,6 +489,18 @@ export const PagerDutyBlock: BlockConfig = {
description: 'Array of on-call entries (list_oncalls)',
},
},

triggers: {
enabled: true,
available: [
'pagerduty_incident_triggered',
'pagerduty_incident_acknowledged',
'pagerduty_incident_resolved',
'pagerduty_incident_escalated',
'pagerduty_incident_reassigned',
'pagerduty_webhook',
],
},
}

export const PagerDutyBlockMeta = {
Expand Down
18 changes: 18 additions & 0 deletions apps/sim/blocks/blocks/zendesk.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ZendeskIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { getTrigger } from '@/triggers'

export const ZendeskBlock: BlockConfig = {
type: 'zendesk',
name: 'Zendesk',
description: 'Manage support tickets, users, and organizations in Zendesk',
triggerAllowed: true,
longDescription:
'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.',
docsLink: 'https://docs.sim.ai/integrations/zendesk',
Expand Down Expand Up @@ -529,6 +531,11 @@ Return ONLY the search query - no explanations.`,
},
mode: 'advanced',
},
...getTrigger('zendesk_ticket_created').subBlocks,
...getTrigger('zendesk_ticket_status_changed').subBlocks,
...getTrigger('zendesk_ticket_comment_added').subBlocks,
...getTrigger('zendesk_ticket_priority_changed').subBlocks,
...getTrigger('zendesk_webhook').subBlocks,
],
tools: {
access: [
Expand Down Expand Up @@ -695,6 +702,17 @@ Return ONLY the search query - no explanations.`,
// Metadata (shared across all operations)
metadata: { type: 'json', description: 'Operation metadata including operation type' },
},

triggers: {
enabled: true,
available: [
'zendesk_ticket_created',
'zendesk_ticket_status_changed',
'zendesk_ticket_comment_added',
'zendesk_ticket_priority_changed',
'zendesk_webhook',
],
},
}

export const ZendeskBlockMeta = {
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/core/idempotency/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ export class IdempotencyService {
normalizedHeaders?.['x-webhook-id'] ||
normalizedHeaders?.['x-shopify-webhook-id'] ||
normalizedHeaders?.['x-github-delivery'] ||
normalizedHeaders?.['x-gitlab-event-uuid'] ||
normalizedHeaders?.['x-event-id'] ||
normalizedHeaders?.['x-teams-notification-id'] ||
normalizedHeaders?.['svix-id'] ||
Expand Down
155 changes: 155 additions & 0 deletions apps/sim/lib/webhooks/providers/gitlab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { generateId } from '@sim/utils/id'
import { NextResponse } from 'next/server'
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
import type {
AuthContext,
DeleteSubscriptionContext,
EventMatchContext,
FormatInputContext,
FormatInputResult,
SubscriptionContext,
SubscriptionResult,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'

const logger = createLogger('WebhookProvider:GitLab')

const GITLAB_API_BASE = 'https://gitlab.com/api/v4'

function asRecord(value: unknown): Record<string, unknown> {
return (value as Record<string, unknown>) || {}
}

function gitlabProjectHooksUrl(projectId: string): string {
return `${GITLAB_API_BASE}/projects/${encodeURIComponent(projectId)}/hooks`
}

export const gitlabHandler: WebhookProviderHandler = {
/**
* GitLab echoes the configured "Secret token" verbatim in the `X-Gitlab-Token`
* header (plain equality, not an HMAC). Skip verification when no token is set.
*/
verifyAuth({ request, requestId, providerConfig }: AuthContext) {
const secret = providerConfig.webhookSecret as string | undefined
if (!secret) {
return null
}

const token = request.headers.get('X-Gitlab-Token')
if (!token) {
logger.warn(`[${requestId}] GitLab webhook missing X-Gitlab-Token header`)
return new NextResponse('Unauthorized - Missing GitLab token', { status: 401 })
}

if (!safeCompare(token, secret)) {
logger.warn(`[${requestId}] GitLab token verification failed`)
return new NextResponse('Unauthorized - Invalid GitLab token', { status: 401 })
}

return null
},

async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {
const triggerId = providerConfig.triggerId as string | undefined
if (!triggerId || triggerId === 'gitlab_webhook') return true

const objectKind = asRecord(body).object_kind as string | undefined

const { isGitLabEventMatch } = await import('@/triggers/gitlab/utils')
if (!isGitLabEventMatch(triggerId, objectKind || '')) {
logger.debug(
`[${requestId}] GitLab event '${objectKind}' does not match trigger ${triggerId}, skipping`
)
return false
}
return true
},

async formatInput({ body, headers }: FormatInputContext): Promise<FormatInputResult> {
const b = asRecord(body)
const eventType = headers['x-gitlab-event'] || ''
const ref = (b.ref as string) || ''
const branch = ref.replace('refs/heads/', '')
return {
input: { ...b, event_type: eventType, branch },
}
},

async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
const config = getProviderConfig(ctx.webhook)
const accessToken = config.accessToken as string | undefined
const projectId = config.projectId as string | undefined
const triggerId = config.triggerId as string | undefined

if (!accessToken)
throw new Error('GitLab Personal Access Token is required to create the webhook.')
if (!projectId) throw new Error('GitLab Project ID is required to create the webhook.')

const { getGitLabEventFlags } = await import('@/triggers/gitlab/utils')
const secretToken = generateId()
const res = await fetch(gitlabProjectHooksUrl(projectId), {
method: 'POST',
headers: { 'PRIVATE-TOKEN': accessToken, 'Content-Type': 'application/json' },
body: JSON.stringify({
url: getNotificationUrl(ctx.webhook),
token: secretToken,
enable_ssl_verification: true,
...getGitLabEventFlags(triggerId ?? 'gitlab_webhook'),
}),
})

if (!res.ok) {
const detail = await res.text().catch(() => '')
logger.error(`[${ctx.requestId}] Failed to create GitLab webhook (${res.status})`, { detail })
if (res.status === 401)
throw new Error(
'GitLab authentication failed. Verify your Personal Access Token has the api scope.'
)
if (res.status === 403)
throw new Error(
'GitLab access denied. You need the Maintainer or Owner role on the project.'
)
if (res.status === 404) throw new Error('GitLab project not found. Verify the Project ID.')
throw new Error(`Failed to create GitLab webhook: ${res.status}`)
}

const created = (await res.json().catch(() => ({}))) as { id?: number | string }
if (created.id === undefined || created.id === null) {
throw new Error('GitLab webhook created but no hook ID was returned.')
}

logger.info(`[${ctx.requestId}] Created GitLab webhook ${created.id} for project ${projectId}`)
return { providerConfigUpdates: { externalId: String(created.id), webhookSecret: secretToken } }
},

async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
const config = getProviderConfig(ctx.webhook)
const accessToken = config.accessToken as string | undefined
const projectId = config.projectId as string | undefined
const externalId = config.externalId as string | undefined

if (!accessToken || !projectId || !externalId) {
if (ctx.strict) throw new Error('Missing GitLab credentials or hook ID for webhook deletion.')
logger.warn(
`[${ctx.requestId}] Skipping GitLab webhook cleanup — missing token, project, or hook ID`
)
return
}

const res = await fetch(`${gitlabProjectHooksUrl(projectId)}/${externalId}`, {
method: 'DELETE',
headers: { 'PRIVATE-TOKEN': accessToken },
})

if (!res.ok && res.status !== 404) {
if (ctx.strict) throw new Error(`Failed to delete GitLab webhook: ${res.status}`)
logger.warn(
`[${ctx.requestId}] Failed to delete GitLab webhook ${externalId} (non-fatal): ${res.status}`
)
return
}
logger.info(`[${ctx.requestId}] Deleted GitLab webhook ${externalId}`)
},
}
Loading
Loading