Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(triggers): add Linear v2 triggers with automatic webhook registration#3991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
5cc54147351b43fd7c8564d1094ff6ea743File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,15 @@ | ||
| import crypto from 'crypto' | ||
| import { createLogger } from '@sim/logger' | ||
| import { safeCompare } from '@/lib/core/security/encryption' | ||
| import { generateId } from '@/lib/core/utils/uuid' | ||
| import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils' | ||
| import type { | ||
| DeleteSubscriptionContext, | ||
| EventMatchContext, | ||
| FormatInputContext, | ||
| FormatInputResult, | ||
| SubscriptionContext, | ||
| SubscriptionResult, | ||
| WebhookProviderHandler, | ||
| } from '@/lib/webhooks/providers/types' | ||
| import { createHmacVerifier } from '@/lib/webhooks/providers/utils' | ||
| @@ -60,6 +66,169 @@ export const linearHandler: WebhookProviderHandler = { | ||
| } | ||
| }, | ||
| async matchEvent({ body, requestId, providerConfig }: EventMatchContext) { | ||
| const triggerId = providerConfig.triggerId as string | undefined | ||
| if (triggerId && !triggerId.endsWith('_webhook') && !triggerId.endsWith('_webhook_v2')) { | ||
| const { isLinearEventMatch } = await import('@/triggers/linear/utils') | ||
| const obj = body as Record<string, unknown> | ||
| const action = obj.action as string | undefined | ||
| const type = obj.type as string | undefined | ||
| if (!isLinearEventMatch(triggerId, type || '', action)) { | ||
| logger.debug( | ||
| `[${requestId}] Linear event mismatch for trigger ${triggerId}. Type: ${type}, Action: ${action}. Skipping.` | ||
| ) | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| }, | ||
| async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> { | ||
| const config = getProviderConfig(ctx.webhook) | ||
| const triggerId = config.triggerId as string | undefined | ||
| if (!triggerId || !triggerId.endsWith('_v2')) { | ||
| return undefined | ||
| } | ||
| const apiKey = config.apiKey as string | undefined | ||
| if (!apiKey) { | ||
| logger.warn(`[${ctx.requestId}] Missing API key for Linear webhook ${ctx.webhook.id}`) | ||
| throw new Error( | ||
| 'Linear API key is required. Please provide a valid API key in the trigger configuration.' | ||
| ) | ||
| } | ||
| const { LINEAR_RESOURCE_TYPE_MAP } = await import('@/triggers/linear/utils') | ||
| const resourceTypes = LINEAR_RESOURCE_TYPE_MAP[triggerId] | ||
| if (!resourceTypes) { | ||
| logger.warn(`[${ctx.requestId}] Unknown Linear trigger ID: ${triggerId}`) | ||
| throw new Error(`Unknown Linear trigger type: ${triggerId}`) | ||
| } | ||
| const notificationUrl = getNotificationUrl(ctx.webhook) | ||
| const webhookSecret = generateId() | ||
| const teamId = config.teamId as string | undefined | ||
| const input: Record<string, unknown> = { | ||
| url: notificationUrl, | ||
| resourceTypes, | ||
| secret: webhookSecret, | ||
| enabled: true, | ||
| } | ||
| if (teamId) { | ||
| input.teamId = teamId | ||
| } else { | ||
| input.allPublicTeams = true | ||
| } | ||
| try { | ||
| const response = await fetch('https://api.linear.app/graphql', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: apiKey, | ||
| }, | ||
| body: JSON.stringify({ | ||
| query: `mutation WebhookCreate($input: WebhookCreateInput!) { | ||
| webhookCreate(input: $input) { | ||
| success | ||
| webhook { id enabled } | ||
| } | ||
| }`, | ||
| variables: { input }, | ||
| }), | ||
| }) | ||
| if (!response.ok) { | ||
| throw new Error( | ||
| `Linear API returned HTTP ${response.status}. Please verify your API key and try again.` | ||
| ) | ||
| } | ||
| const data = await response.json() | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const result = data?.data?.webhookCreate | ||
| if (!result?.success) { | ||
| const errors = data?.errors?.map((e: { message: string }) => e.message).join(', ') | ||
| logger.error(`[${ctx.requestId}] Failed to create Linear webhook`, { | ||
| errors, | ||
| webhookId: ctx.webhook.id, | ||
| }) | ||
| throw new Error(errors || 'Failed to create Linear webhook. Please verify your API key.') | ||
| } | ||
| const externalId = result.webhook?.id | ||
| logger.info( | ||
| `[${ctx.requestId}] Created Linear webhook ${externalId} for webhook ${ctx.webhook.id}` | ||
| ) | ||
| return { | ||
| providerConfigUpdates: { | ||
| externalId, | ||
| webhookSecret, | ||
| }, | ||
| } | ||
| } catch (error) { | ||
| if (error instanceof Error && error.message !== 'fetch failed') { | ||
| throw error | ||
| } | ||
| logger.error(`[${ctx.requestId}] Error creating Linear webhook`, { | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }) | ||
| throw new Error('Failed to create Linear webhook. Please verify your API key and try again.') | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| }, | ||
| async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> { | ||
| const config = getProviderConfig(ctx.webhook) | ||
| const externalId = config.externalId as string | undefined | ||
| const apiKey = config.apiKey as string | undefined | ||
| if (!externalId || !apiKey) { | ||
| return | ||
| } | ||
| try { | ||
| const response = await fetch('https://api.linear.app/graphql', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: apiKey, | ||
| }, | ||
| body: JSON.stringify({ | ||
| query: `mutation WebhookDelete($id: String!) { | ||
| webhookDelete(id: $id) { success } | ||
| }`, | ||
| variables: { id: externalId }, | ||
| }), | ||
| }) | ||
| if (!response.ok) { | ||
| logger.warn( | ||
| `[${ctx.requestId}] Linear API returned HTTP ${response.status} during webhook deletion for ${externalId}` | ||
| ) | ||
| return | ||
| } | ||
| const data = await response.json() | ||
| if (data?.data?.webhookDelete?.success) { | ||
| logger.info( | ||
| `[${ctx.requestId}] Deleted Linear webhook ${externalId} for webhook ${ctx.webhook.id}` | ||
| ) | ||
| } else { | ||
| logger.warn( | ||
| `[${ctx.requestId}] Linear webhook deletion returned unsuccessful for ${externalId}` | ||
| ) | ||
| } | ||
| } catch (error) { | ||
| logger.warn(`[${ctx.requestId}] Error deleting Linear webhook ${externalId} (non-fatal)`, { | ||
| error: error instanceof Error ? error.message : String(error), | ||
| }) | ||
| } | ||
| }, | ||
| extractIdempotencyId(body: unknown) { | ||
| const obj = body as Record<string, unknown> | ||
| const data = obj.data as Record<string, unknown> | undefined | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { LinearIcon } from '@/components/icons' | ||
| import { buildCommentOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils' | ||
| import type { TriggerConfig } from '@/triggers/types' | ||
| export const linearCommentCreatedV2Trigger: TriggerConfig = { | ||
| id: 'linear_comment_created_v2', | ||
| name: 'Linear Comment Created', | ||
| provider: 'linear', | ||
| description: 'Trigger workflow when a new comment is created in Linear', | ||
| version: '2.0.0', | ||
| icon: LinearIcon, | ||
| subBlocks: buildLinearV2SubBlocks({ | ||
| triggerId: 'linear_comment_created_v2', | ||
| eventType: 'Comment (create)', | ||
| }), | ||
| outputs: buildCommentOutputs(), | ||
| webhook: { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Linear-Event': 'Comment', | ||
| 'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', | ||
| 'Linear-Signature': 'sha256...', | ||
| 'User-Agent': 'Linear-Webhook', | ||
| }, | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { LinearIcon } from '@/components/icons' | ||
| import { buildCommentOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils' | ||
| import type { TriggerConfig } from '@/triggers/types' | ||
| export const linearCommentUpdatedV2Trigger: TriggerConfig = { | ||
| id: 'linear_comment_updated_v2', | ||
| name: 'Linear Comment Updated', | ||
| provider: 'linear', | ||
| description: 'Trigger workflow when a comment is updated in Linear', | ||
| version: '2.0.0', | ||
| icon: LinearIcon, | ||
| subBlocks: buildLinearV2SubBlocks({ | ||
| triggerId: 'linear_comment_updated_v2', | ||
| eventType: 'Comment (update)', | ||
| }), | ||
| outputs: buildCommentOutputs(), | ||
| webhook: { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Linear-Event': 'Comment', | ||
| 'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', | ||
| 'Linear-Signature': 'sha256...', | ||
| 'User-Agent': 'Linear-Webhook', | ||
| }, | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { LinearIcon } from '@/components/icons' | ||
| import { buildCustomerRequestOutputs, buildLinearV2SubBlocks } from '@/triggers/linear/utils' | ||
| import type { TriggerConfig } from '@/triggers/types' | ||
| export const linearCustomerRequestCreatedV2Trigger: TriggerConfig = { | ||
| id: 'linear_customer_request_created_v2', | ||
| name: 'Linear Customer Request Created', | ||
| provider: 'linear', | ||
| description: 'Trigger workflow when a new customer request is created in Linear', | ||
| version: '2.0.0', | ||
| icon: LinearIcon, | ||
| subBlocks: buildLinearV2SubBlocks({ | ||
| triggerId: 'linear_customer_request_created_v2', | ||
| eventType: 'Customer Requests', | ||
| }), | ||
| outputs: buildCustomerRequestOutputs(), | ||
| webhook: { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Linear-Event': 'CustomerNeed', | ||
| 'Linear-Delivery': 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', | ||
| 'Linear-Signature': 'sha256...', | ||
| 'User-Agent': 'Linear-Webhook', | ||
| }, | ||
| }, | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.