diff --git a/.changeset/strange-gorillas-kick.md b/.changeset/strange-gorillas-kick.md new file mode 100644 index 00000000000..c8931555085 --- /dev/null +++ b/.changeset/strange-gorillas-kick.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/sdk": patch +"@trigger.dev/linear": patch +--- + +First release of `@trigger.dev/linear` integration. `io.runTask()` error handlers can now prevent further retries. diff --git a/.env.example b/.env.example index 295b41745c7..48170e8f365 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,8 @@ CLOUD_AIRTABLE_CLIENT_ID= CLOUD_AIRTABLE_CLIENT_SECRET= CLOUD_GITHUB_CLIENT_ID= CLOUD_GITHUB_CLIENT_SECRET= +CLOUD_LINEAR_CLIENT_ID= +CLOUD_LINEAR_CLIENT_SECRET= CLOUD_SLACK_APP_HOST= CLOUD_SLACK_CLIENT_ID= CLOUD_SLACK_CLIENT_SECRET= \ No newline at end of file diff --git a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts index 4369ad87472..b86c2e496ce 100644 --- a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts +++ b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts @@ -1,5 +1,6 @@ import { airtable } from "./integrations/airtable"; import { github } from "./integrations/github"; +import { linear } from "./integrations/linear"; import { openai } from "./integrations/openai"; import { plain } from "./integrations/plain"; import { resend } from "./integrations/resend"; @@ -33,6 +34,7 @@ export class IntegrationCatalog { export const integrationCatalog = new IntegrationCatalog({ airtable, github, + linear, openai, plain, resend, diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts new file mode 100644 index 00000000000..355d968833b --- /dev/null +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -0,0 +1,104 @@ +import type { HelpSample, Integration } from "../types"; + +function usageSample(hasApiKey: boolean): HelpSample { + return { + title: "Using the client", + code: ` +import { Linear } from "@trigger.dev/linear"; + +const linear = new Linear({ + id: "__SLUG__",${hasApiKey ? ",\n apiKey: process.env.LINEAR_API_KEY!" : ""} +}); + +client.defineJob({ + id: "linear-react-to-new-issue", + name: "Linear - React To New Issue", + version: "0.1.0", + integrations: { linear }, + trigger: linear.onIssueCreated(), + run: async (payload, io, ctx) => { + await io.linear.createComment("create-comment", { + issueId: payload.data.id, + body: "Thank's for opening this issue!" + }); + + await io.linear.createReaction("create-reaction", { + issueId: payload.data.id, + emoji: "+1" + }); + + return { payload, ctx }; + }, +}); + `, + }; +} + +export const linear: Integration = { + identifier: "linear", + name: "Linear", + packageName: "@trigger.dev/linear@latest", + authenticationMethods: { + oauth2: { + name: "OAuth", + type: "oauth2", + client: { + id: { + envName: "CLOUD_LINEAR_CLIENT_ID", + }, + secret: { + envName: "CLOUD_LINEAR_CLIENT_SECRET", + }, + }, + config: { + authorization: { + url: "https://linear.app/oauth/authorize", + scopeSeparator: ",", + }, + token: { + url: "https://api.linear.app/oauth/token", + metadata: {}, + }, + refresh: { + url: "https://linear.app/oauth/authorize", + }, + pkce: false, + }, + scopes: [ + { + name: "write", + description: + "Grants global write access to the user's account. Use a more targeted scope if you don't need full access.", + defaultChecked: true, + }, + + { + name: "issue:create", + description: "Grants access to create issues and attachments only.", + annotations: [{ label: "Issues" }], + }, + + { + name: "comments:create", + description: "Grants access to create new issue comments.", + annotations: [{ label: "Comments" }], + }, + + { + name: "admin", + description: + "Grants full access to admin-level endpoints. Don't use this unless you really need it.", + }, + ], + help: { + samples: [usageSample(false)], + }, + }, + apikey: { + type: "apikey", + help: { + samples: [usageSample(true)], + }, + }, + }, +}; diff --git a/docs/integrations/apis/linear.mdx b/docs/integrations/apis/linear.mdx new file mode 100644 index 00000000000..5c90f75cc42 --- /dev/null +++ b/docs/integrations/apis/linear.mdx @@ -0,0 +1,479 @@ +--- +title: Linear +description: "Streamline your project and issue tracking" +--- + + + +## Installation + +To get started with the Linear integration on Trigger.dev, you need to install the `@trigger.dev/linear` package. +You can do this using npm, pnpm, or yarn: + + + +```bash npm +npm install @trigger.dev/linear@latest +``` + +```bash pnpm +pnpm add @trigger.dev/linear@latest +``` + +```bash yarn +yarn add @trigger.dev/linear@latest +``` + + + +## Authentication + +To use the Linear API with Trigger.dev, you can either use OAuth or a Personal API Key. + +### OAuth + +```ts +import { Linear } from "@trigger.dev/linear"; + +//this will use OAuth +const linear = new Linear({ + id: "linear", +}); +``` + +### Personal API Key + +You can create a Personal API Key in your [Linear API Settings](https://linear.app/settings/api). + +```ts +import { Linear } from "@trigger.dev/linear"; + +//this will use the passed in API key (defined in your environment variables) +const linear = new Linear({ + id: "linear", + apiKey: process.env["LINEAR_API_KEY"], +}); +``` + +## Usage + +Include the Linear integration in your Trigger.dev job. + +```ts +client.defineJob({ + id: "linear-new-issue-autoresponder", + name: "Linear - New Issue Autoresponder", + version: "0.1.0", + integrations: { + //use the linear integration + linear, + }, + //trigger on issue created events + trigger: linear.onIssueCreated(), + run: async (payload, io, ctx) => { + //get new issue ID from the event payload + const newIssueId = payload.data.id; + + //comment + await io.linear.createComment("create-comment", { + issueId: newIssueId, + body: "Thank's for opening this issue!", + }); + + //react + await io.linear.createReaction("create-reaction", { + issueId: newIssueId, + emoji: "+1", + }); + + //store and display in the job run + return { payload, ctx }; + }, +}); +``` + +### Serialization helper + +Use the `serializeLinearOutput` helper instead of returning raw Linear SDK responses: + +```ts +import { Linear, serializeLinearOutput } from "@trigger.dev/linear"; +... +client.defineJob({ + id: "linear-sdk", + name: "Linear SDK", + version: "0.1.0", + integrations: { + linear, + }, + trigger: eventTrigger({ + name: "linear.sdk", + }), + run: async (payload, io, ctx) => { + //the official Linear SDK is exposed as `client` + const issues = await io.linear.runTask("first-two", async (client) => { + //these nodes contain values we can't serialize, e.g. functions + const { nodes } = await client.issues({ first: 2 }); + //we remove them with this little helper + return serializeLinearOutput(nodes); + }); + return issues; + }, +}); +``` + +### Pagination + +You can paginate responses three different ways: + +1. Via the raw Linear SDK exposed in `io.runTask()` +2. Iterating the same integration task with different params +3. Using the `getAll` helper exposed on the integration (**recommended!**) + +_When ordering results, make sure to use the `PaginationOrderBy` enum._ + +```ts +import { Linear, PaginationOrderBy, serializeLinearOutput } from "@trigger.dev/linear"; +... +client.defineJob({ + id: "linear-pagination", + name: "Linear Pagination", + version: "0.1.0", + integrations: { + linear, + }, + trigger: eventTrigger({ + name: "linear.paginate", + }), + run: async (payload, io, ctx) => { + //the same params will be used for all tasks + const params = { first: 5, orderBy: PaginationOrderBy.UpdatedAt }; + + //1. Linear SDK + const sdkIssues = await io.linear.runTask("all-issues-via-sdk", async (client) => { + const edges = await client.issues(params); + + //this will keep appending nodes until there are no more + while (edges.pageInfo.hasNextPage) { + await edges.fetchNext(); + } + + //use serialization helper to remove functions etc + return serializeLinearOutput(edges.nodes); + }); + + //2. Linear integration - no pagination helper + let edges = await io.linear.issues("get-issues", params); + let noHelper = edges.nodes; + + for (let i = 0; edges.pageInfo.hasNextPage; i++) { + edges = await io.linear.issues(`get-more-issues-${i}`, { + ...params, + after: edges.pageInfo.endCursor, + }); + noHelper = noHelper.concat(edges.nodes); + } + + //3. Linear integration - with the pagination helper + const withHelper = await io.linear.getAll(io.linear.issues, "get-all", params); + + return { + issueCounts: { + withSdk: sdkIssues.length, + noHelper: noHelper.length, + withHelper: withHelper.length, + }, + }; + }, +}); +``` + +## Triggers + +### Attachments + +| Function Name | Description | +| --------------------- | ---------------------------------------------- | +| `onAttachment` | When any action is performed on an attachment. | +| `onAttachmentCreated` | When an attachment is created. | +| `onAttachmentRemoved` | When an attachment is removed. | +| `onAttachmentUpdated` | When an attachment is updated. | + +### Comments + +| Function Name | Description | +| ------------------ | ------------------------------------------- | +| `onComment` | When any action is performed on an comment. | +| `onCommentCreated` | When an comment is created. | +| `onCommentRemoved` | When an comment is removed. | +| `onCommentUpdated` | When an comment is updated. | + +### Cycles + +| Function Name | Description | +| ---------------- | ----------------------------------------- | +| `onCycle` | When any action is performed on an cycle. | +| `onCycleCreated` | When an cycle is created. | +| `onCycleRemoved` | When an cycle is removed. | +| `onCycleUpdated` | When an cycle is updated. | + +### Issues + +| Function Name | Description | +| ---------------- | ----------------------------------------- | +| `onIssue` | When any action is performed on an issue. | +| `onIssueCreated` | When an issue is created. | +| `onIssueRemoved` | When an issue is removed. | +| `onIssueUpdated` | When an issue is updated. | + +### Issue Labels + +| Function Name | Description | +| --------------------- | ----------------------------------------------- | +| `onIssueLabel` | When any action is performed on an issue label. | +| `onIssueLabelCreated` | When an issue label is created. | +| `onIssueLabelRemoved` | When an issue label is removed. | +| `onIssueLabelUpdated` | When an issue label is updated. | + +### Issue SLAs + +| Function Name | Description | +| -------------------- | --------------------------------------------- | +| `onIssueSLA` | When any action is performed on an issue SLA. | +| `onIssueSLASet` | When an issue SLA is set. | +| `onIssueSLABreached` | When an issue SLA is breached. | +| `onIssueSLAHighRisk` | When an issue SLA is high risk. | + +### Projects + +| Function Name | Description | +| ------------------ | ------------------------------------------- | +| `onProject` | When any action is performed on an project. | +| `onProjectCreated` | When an project is created. | +| `onProjectRemoved` | When an project is removed. | +| `onProjectUpdated` | When an project is updated. | + +### Project Updates + +| Function Name | Description | +| ------------------------ | -------------------------------------------------- | +| `onProjectUpdate` | When any action is performed on an project update. | +| `onProjectUpdateCreated` | When an project update is created. | +| `onProjectUpdateRemoved` | When an project update is removed. | +| `onProjectUpdateUpdated` | When an project update is updated. | + +### Reactions + +| Function Name | Description | +| ------------------- | -------------------------------------------- | +| `onReaction` | When any action is performed on an reaction. | +| `onReactionCreated` | When an reaction is created. | +| `onReactionRemoved` | When an reaction is removed. | +| `onReactionUpdated` | When an reaction is updated. | + +## Tasks + +### Attachments + +| Function Name | Description | +| ------------------ | -------------------------- | +| `attachment` | Gets an attachment. | +| `attachments` | Gets multiple attachments. | +| `createAttachment` | Creates an attachment. | +| `deleteAttachment` | Deletes an attachment. | +| `updateAttachment` | Updates an attachment. | + +### Attachment Links + +| Function Name | Description | +| ------------------------- | ------------------------------------------ | +| `attachmentLinkFront` | Links a Front conversation to an issue. | +| `attachmentLinkIntercom` | Links a Intercom conversation to an issue. | +| `attachmentLinkJiraIssue` | Links a Jira issue to an issue. | +| `attachmentLinkSlack` | Links a Slack message to an issue. | +| `attachmentLinkURL` | Links any URL to an issue. | +| `attachmentLinkZendesk` | Links a Zendesk ticket to an issue. | + +### Comments + +| Function Name | Description | +| --------------- | ----------------------- | +| `comment` | Gets a comment. | +| `comments` | Gets multiple comments. | +| `createComment` | Creates a comment. | +| `deleteComment` | Deletes a comment. | +| `updateComment` | Updates a comment. | + +### Cycles + +| Function Name | Description | +| -------------- | ----------------- | +| `archiveCycle` | Archives a cycle. | +| `createCycle` | Creates a cycle. | +| `updateCycle` | Updates a cycle. | + +### Documents + +| Function Name | Description | +| ----------------- | ------------------------ | +| `document` | Gets a document. | +| `documents` | Gets multiple documents. | +| `createDocument` | Creates a document. | +| `searchDocuments` | Searches documents. | + +### Favorites + +| Function Name | Description | +| ---------------- | ------------------------ | +| `favorite` | Gets a favorite. | +| `favorites` | Gets multiple favorites. | +| `createFavorite` | Creates a favorite. | + +### Issues + +| Function Name | Description | +| -------------- | --------------------- | +| `issue` | Gets an issue. | +| `issues` | Gets multiple issues. | +| `archiveIssue` | Archives an issue. | +| `createIssue` | Creates an issue. | +| `deleteIssue` | Deletes an issue. | +| `searchIssues` | Searches issues. | +| `updateIssue` | Updates an issue. | + +### Issue Labels + +| Function Name | Description | +| ------------------ | --------------------------- | +| `issueLabel` | Gets an issue label. | +| `issueLabels` | Gets multiple issue labels. | +| `createIssueLabel` | Creates an issue label. | +| `deleteIssueLabel` | Deletes an issue label. | +| `updateIssueLabel` | Updates an issue label. | + +### Issue Relations + +| Function Name | Description | +| --------------------- | ------------------------------ | +| `issueRelation` | Gets an issue relation. | +| `issueRelations` | Gets multiple issue relations. | +| `createIssueRelation` | Creates an issue relation. | + +### Notifications + +| Function Name | Description | +| -------------------------------- | ------------------------------------ | +| `notification` | Gets a notification. | +| `notifications` | Gets multiple notifications. | +| `archiveNotification` | Archives a notification. | +| `createNotificationSubscription` | Creates a notification subscription. | + +### Organizations + +| Function Name | Description | +| ---------------------------------- | ------------------------------- | +| `organization` | Gets the viewer's organization. | +| `createOrganizationFromOnboarding` | Creates an organization. | +| `createOrganizationInvite` | Creates an organization invite. | + +### Projects + +| Function Name | Description | +| ---------------- | ----------------------- | +| `project` | Gets a project. | +| `projects` | Gets multiple projects. | +| `archiveProject` | Archives a project. | +| `createProject` | Creates a project. | +| `deleteProject` | Deletes a project. | +| `searchProjects` | Searches projects. | +| `updateProject` | Updates a project. | + +### Project Links + +| Function Name | Description | +| ------------------- | ---------------------------- | +| `projectLink` | Gets a project link. | +| `projectLinks` | Gets multiple project links. | +| `createProjectLink` | Creates a project link. | + +### Project Updates + +| Function Name | Description | +| --------------------- | ------------------------------ | +| `projectUpdate` | Gets a project update. | +| `projectUpdates` | Gets multiple project updates. | +| `createProjectUpdate` | Creates a project update. | +| `deleteProjectUpdate` | Deletes a project update. | +| `updateProjectUpdate` | Updates a project update. | + +### Reactions + +| Function Name | Description | +| ---------------- | ------------------- | +| `createReaction` | Creates a reaction. | +| `deleteReaction` | Deletes a reaction. | + +### Roadmaps + +| Function Name | Description | +| ---------------- | ------------------- | +| `archiveRoadmap` | Archives a roadmap. | +| `createRoadmap` | Creates a roadmap. | + +### Teams + +| Function Name | Description | +| ------------- | -------------------- | +| `team` | Gets a team. | +| `teams` | Gets multiple teams. | +| `createTeam` | Creates a team. | + +### Team Memberships + +| Function Name | Description | +| ---------------------- | ------------------------------- | +| `teamMembership` | Gets a team membership. | +| `teamMemberships` | Gets multiple team memberships. | +| `createTeamMembership` | Creates a team membership. | + +### Templates + +| Function Name | Description | +| ------------- | ------------------------ | +| `template` | Gets a template. | +| `templates` | Gets multiple templates. | + +### Users + +| Function Name | Description | +| ------------- | -------------------- | +| `user` | Gets a user. | +| `users` | Gets multiple users. | +| `updateUser` | Updates a user. | + +### Webhooks + +| Function Name | Description | +| --------------- | ----------------------- | +| `webhook` | Gets a webhook. | +| `webhooks` | Gets multiple webhooks. | +| `createWebhook` | Creates a webhook. | +| `deleteWebhook` | Deletes a webhook. | +| `updateWebhook` | Updates a webhook. | + +### Workflow States + +| Function Name | Description | +| ---------------------- | ------------------------------ | +| `workflowState` | Gets a workflow state. | +| `workflowStates` | Gets multiple workflow states. | +| `archiveWorkflowState` | Archives a workflow state. | +| `createWorkflowState` | Creates a workflow state. | + +### Misc + +| Function Name | Description | +| ------------------------ | -------------------------------------- | +| `createProjectMilestone` | Creates a project milestone. | +| `issuePriorityValues` | Gets issue priority values and labels. | +| `viewer` | Gets the currently authenticated user. | diff --git a/docs/integrations/introduction.mdx b/docs/integrations/introduction.mdx index 1496d2e69a2..30be9c1e365 100644 --- a/docs/integrations/introduction.mdx +++ b/docs/integrations/introduction.mdx @@ -33,6 +33,7 @@ Navigate the menu or select Integrations from the table below. | API | Description | Webhooks | Tasks | | --------------------------------------- | ---------------------------------------------------------------- | -------- | ----- | | [GitHub](/integrations/apis/github) | Subscribe to webhooks and perform actions | ✅ | ✅ | +| [Linear](/integrations/apis/linear) | Streamline project and issue tracking | ✅ | ✅ | | [OpenAI](/integrations/apis/openai) | Generate text and images. Including longer than 30s prompts | N/A | ✅ | | [Plain](/integrations/apis/plain) | Perform customer support using Plain | 🕘 | ✅ | | [Resend](/integrations/apis/resend) | Send emails using Resend | 🕘 | ✅ | diff --git a/docs/mint.json b/docs/mint.json index f8f2b7cf851..81c5dfb1717 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -244,6 +244,7 @@ "integrations/apis/github-tasks" ] }, + "integrations/apis/linear", "integrations/apis/openai", "integrations/apis/plain", "integrations/apis/resend", diff --git a/integrations/linear/README.md b/integrations/linear/README.md new file mode 100644 index 00000000000..13ffab69b4a --- /dev/null +++ b/integrations/linear/README.md @@ -0,0 +1,3 @@ + +# @trigger.dev/linear + \ No newline at end of file diff --git a/integrations/linear/package.json b/integrations/linear/package.json new file mode 100644 index 00000000000..c65a4a586f6 --- /dev/null +++ b/integrations/linear/package.json @@ -0,0 +1,36 @@ +{ + "name": "@trigger.dev/linear", + "version": "2.1.3", + "description": "Trigger.dev integration for @linear/sdk", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/index.js", + "dist/index.d.ts", + "dist/index.js.map" + ], + "devDependencies": { + "@types/node": "16.x", + "rimraf": "^3.0.2", + "tsup": "7.1.x", + "typescript": "4.9.4" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@linear/sdk": "^8.0.0", + "@trigger.dev/integration-kit": "workspace:^2.1.0", + "@trigger.dev/sdk": "workspace:^2.1.0", + "zod": "3.21.4" + }, + "engines": { + "node": ">=16.8.0" + } +} \ No newline at end of file diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts new file mode 100644 index 00000000000..18dff27b58b --- /dev/null +++ b/integrations/linear/src/events.ts @@ -0,0 +1,535 @@ +import { EventSpecification } from "@trigger.dev/sdk"; +import { + AttachmentEvent, + CommentEvent, + CycleEvent, + IssueEvent, + IssueLabelEvent, + IssueSLAEvent, + ProjectEvent, + ProjectUpdateEvent, + ReactionEvent, +} from "./schemas"; +import { GetLinearPayload } from "./types"; +import { + attachmentCreated, + attachmentRemoved, + attachmentUpdated, + commentCreated, + commentRemoved, + commentUpdated, + cycleCreated, + cycleRemoved, + cycleUpdated, + issueCreated, + issueRemoved, + issueUpdated, + issueLabelCreated, + issueLabelRemoved, + issueLabelUpdated, + projectCreated, + projectRemoved, + projectUpdated, + projectUpdateCreated, + projectUpdateRemoved, + projectUpdateUpdated, + reactionCreated, + reactionRemoved, + reactionUpdated, +} from "./payload-examples"; +import { onCommentProperties, onIssueProperties, updatedFromProperties } from "./utils"; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachment: EventSpecification> = { + name: "Attachment", + title: "On Attachment", + source: "linear.app", + icon: "linear", + examples: [attachmentCreated, attachmentRemoved, attachmentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Attachment ID", text: payload.data.id }, + ], +}; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachmentCreated: EventSpecification> = + { + name: "Attachment", + title: "On Attachment Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [attachmentCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], + }; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachmentRemoved: EventSpecification> = + { + name: "Attachment", + title: "On Attachment Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [attachmentRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], + }; + +/** **WARNING:** Still in alpha - use with caution! */ +export const onAttachmentUpdated: EventSpecification> = + { + name: "Attachment", + title: "On Attachment Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [attachmentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], + }; + +export const onComment: EventSpecification> = { + name: "Comment", + title: "On Comment", + source: "linear.app", + icon: "linear", + examples: [commentCreated, commentRemoved, commentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + ...onCommentProperties(payload), + ...updatedFromProperties(payload), + ], +}; + +export const onCommentCreated: EventSpecification> = { + name: "Comment", + title: "On Comment Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [commentCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onCommentProperties(payload), +}; + +export const onCommentRemoved: EventSpecification> = { + name: "Comment", + title: "On Comment Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [commentRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onCommentProperties(payload), +}; + +export const onCommentUpdated: EventSpecification> = { + name: "Comment", + title: "On Comment Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [commentUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)], +}; + +export const onCycle: EventSpecification> = { + name: "Cycle", + title: "On Cycle", + source: "linear.app", + icon: "linear", + examples: [cycleCreated, cycleRemoved, cycleUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Cycle ID", text: payload.data.id }, + ], +}; + +export const onCycleCreated: EventSpecification> = { + name: "Cycle", + title: "On Cycle Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [cycleCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], +}; + +export const onCycleRemoved: EventSpecification> = { + name: "Cycle", + title: "On Cycle Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [cycleRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], +}; + +export const onCycleUpdated: EventSpecification> = { + name: "Cycle", + title: "On Cycle Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [cycleUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], +}; + +export const onIssue: EventSpecification> = { + name: "Issue", + title: "On Issue", + source: "linear.app", + icon: "linear", + examples: [issueCreated, issueRemoved, issueUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + ...onIssueProperties(payload), + ...updatedFromProperties(payload), + ], +}; + +export const onIssueCreated: EventSpecification> = { + name: "Issue", + title: "On Issue Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [issueCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onIssueProperties(payload), +}; + +export const onIssueRemoved: EventSpecification> = { + name: "Issue", + title: "On Issue Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [issueRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => onIssueProperties(payload), +}; + +export const onIssueUpdated: EventSpecification> = { + name: "Issue", + title: "On Issue Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [issueUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)], +}; + +export const onIssueLabel: EventSpecification> = { + name: "IssueLabel", + title: "On IssueLabel", + source: "linear.app", + icon: "linear", + examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "IssueLabel ID", text: payload.data.id }, + ], +}; + +export const onIssueLabelCreated: EventSpecification> = + { + name: "IssueLabel", + title: "On IssueLabel Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [issueLabelCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], + }; + +export const onIssueLabelRemoved: EventSpecification> = + { + name: "IssueLabel", + title: "On IssueLabel Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [issueLabelRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], + }; + +export const onIssueLabelUpdated: EventSpecification> = + { + name: "IssueLabel", + title: "On IssueLabel Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [issueLabelUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], + }; + +// TODO: this needs to be tested +export const onIssueSLA: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "SLA action", text: payload.action }, + { label: "Issue ID", text: payload.issueData.id }, + ], +}; + +export const onIssueSLASet: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA Set", + source: "linear.app", + icon: "linear", + filter: { + action: ["set"], + }, + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }], +}; + +export const onIssueSLABreached: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA Breached", + source: "linear.app", + icon: "linear", + filter: { + action: ["breached"], + }, + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }], +}; + +export const onIssueSLAHighRisk: EventSpecification> = { + name: "IssueSLA", + title: "On Issue SLA High Risk", + source: "linear.app", + icon: "linear", + filter: { + action: ["highRisk"], + }, + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }], +}; + +export const onProject: EventSpecification> = { + name: "Project", + title: "On Project", + source: "linear.app", + icon: "linear", + examples: [projectCreated, projectRemoved, projectUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ...updatedFromProperties(payload), + ], +}; + +export const onProjectCreated: EventSpecification> = { + name: "Project", + title: "On Project Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [projectCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ], +}; + +export const onProjectRemoved: EventSpecification> = { + name: "Project", + title: "On Project Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [projectRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ], +}; + +export const onProjectUpdated: EventSpecification> = { + name: "Project", + title: "On Project Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [projectUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Project ID", text: payload.data.id }, + { label: "Project Name", text: payload.data.name, url: payload.url ?? undefined }, + ...updatedFromProperties(payload), + ], +}; + +export const onProjectUpdate: EventSpecification> = { + name: "ProjectUpdate", + title: "On ProjectUpdate", + source: "linear.app", + icon: "linear", + examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "ProjectUpdate ID", text: payload.data.id }, + ], +}; + +export const onProjectUpdateCreated: EventSpecification< + GetLinearPayload +> = { + name: "ProjectUpdate", + title: "On ProjectUpdate Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [projectUpdateCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], +}; + +export const onProjectUpdateRemoved: EventSpecification< + GetLinearPayload +> = { + name: "ProjectUpdate", + title: "On ProjectUpdate Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [projectUpdateRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], +}; + +export const onProjectUpdateUpdated: EventSpecification< + GetLinearPayload +> = { + name: "ProjectUpdate", + title: "On ProjectUpdate Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [projectUpdateUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], +}; + +export const onReaction: EventSpecification> = { + name: "Reaction", + title: "On Reaction", + source: "linear.app", + icon: "linear", + examples: [reactionCreated, reactionRemoved, reactionUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Reaction ID", text: payload.data.id }, + ], +}; + +export const onReactionCreated: EventSpecification> = { + name: "Reaction", + title: "On Reaction Created", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + examples: [reactionCreated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], +}; + +export const onReactionRemoved: EventSpecification> = { + name: "Reaction", + title: "On Reaction Removed", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + examples: [reactionRemoved], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], +}; + +export const onReactionUpdated: EventSpecification> = { + name: "Reaction", + title: "On Reaction Updated", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + examples: [reactionUpdated], + parsePayload: (payload) => payload as GetLinearPayload, + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], +}; diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts new file mode 100644 index 00000000000..b9f7d218b90 --- /dev/null +++ b/integrations/linear/src/index.ts @@ -0,0 +1,2029 @@ +import { + ConnectionAuth, + IO, + IOTask, + IntegrationTaskKey, + Json, + Prettify, + RunTaskErrorCallback, + RunTaskOptions, + TriggerIntegration, + retry, +} from "@trigger.dev/sdk"; +import { + Attachment, + AttachmentConnection, + AttachmentPayload, + Comment, + CommentConnection, + CommentPayload, + Connection, + CreateOrJoinOrganizationResponse, + CycleArchivePayload, + CyclePayload, + DeletePayload, + Document, + DocumentConnection, + DocumentPayload, + DocumentSearchPayload, + Favorite, + FavoriteConnection, + FavoritePayload, + FrontAttachmentPayload, + Issue, + IssueArchivePayload, + IssueConnection, + IssueLabel, + IssueLabelConnection, + IssueLabelPayload, + IssuePayload, + IssuePriorityValue, + IssueRelation, + IssueRelationConnection, + IssueRelationPayload, + IssueSearchPayload, + LinearClient, + LinearDocument as L, + LinearError, + Notification, + NotificationArchivePayload, + NotificationConnection, + NotificationSubscriptionPayload, + Organization, + OrganizationInvitePayload, + Project, + ProjectArchivePayload, + ProjectConnection, + ProjectLink, + ProjectLinkConnection, + ProjectLinkPayload, + ProjectMilestonePayload, + ProjectPayload, + ProjectSearchPayload, + ProjectUpdate, + ProjectUpdateConnection, + ProjectUpdatePayload, + RatelimitedLinearError, + ReactionPayload, + RoadmapArchivePayload, + RoadmapPayload, + Team, + TeamConnection, + TeamMembership, + TeamMembershipConnection, + TeamMembershipPayload, + TeamPayload, + Template, + User, + UserConnection, + UserPayload, + WorkflowState, + WorkflowStateArchivePayload, + WorkflowStateConnection, + WorkflowStatePayload, +} from "@linear/sdk"; + +import * as events from "./events"; +import { AwaitNested, LinearReturnType, SerializedLinearOutput } from "./types"; +import { Nullable, QueryVariables, queryProperties } from "./utils"; +import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; + +export type LinearIntegrationOptions = { + id: string; + apiKey?: string; +}; + +export type LinearRunTask = InstanceType["runTask"]; + +export class Linear implements TriggerIntegration { + private _options: LinearIntegrationOptions; + private _client?: LinearClient; + private _io?: IO; + private _connectionKey?: string; + + constructor(private options: LinearIntegrationOptions) { + if (Object.keys(options).includes("apiKey") && !options.apiKey) { + throw `Can't create Linear integration (${options.id}) as apiKey was undefined`; + } + + this._options = options; + } + + get authSource() { + return this._options.apiKey ? "LOCAL" : "HOSTED"; + } + + get id() { + return this._options.id; + } + + get metadata() { + return { id: "linear", name: "Linear" }; + } + + get source() { + return createWebhookEventSource(this); + } + + cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { + const linear = new Linear(this._options); + linear._io = io; + linear._connectionKey = connectionKey; + linear._client = this.createClient(auth); + return linear; + } + + createClient(auth?: ConnectionAuth) { + if (auth) { + return new LinearClient({ + accessToken: auth.accessToken, + }); + } + + if (this._options.apiKey) { + return new LinearClient({ + apiKey: this._options.apiKey, + }); + } + + throw new Error("No auth"); + } + + runTask | void>( + key: IntegrationTaskKey, + callback: (client: LinearClient, task: IOTask, io: IO) => Promise, + options?: RunTaskOptions, + errorCallback?: RunTaskErrorCallback + ): Promise { + if (!this._io) throw new Error("No IO"); + if (!this._connectionKey) throw new Error("No connection key"); + + return this._io.runTask( + key, + (task, io) => { + if (!this._client) throw new Error("No client"); + return callback(this._client, task, io); + }, + { + icon: "linear", + retry: retry.standardBackoff, + ...(options ?? {}), + connectionKey: this._connectionKey, + }, + errorCallback ?? onError + ); + } + + async getAll< + TTask extends ( + key: IntegrationTaskKey, + params: Partial> + ) => LinearReturnType>, + >( + task: TTask, + key: IntegrationTaskKey, + params: Nullable = {} + ): Promise>["nodes"]> { + const boundTask = task.bind(this); + + let edges = await boundTask(`${key}-0`, params); + let nodes = edges.nodes; + + for (let i = 1; edges.pageInfo.hasNextPage; i++) { + edges = await boundTask(`${key}-${i}`, { ...params, after: edges.pageInfo.endCursor }); + nodes = nodes.concat(edges.nodes); + } + + return nodes; + } + + attachment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.attachment(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + } + ); + } + + attachments( + key: IntegrationTaskKey, + params: L.AttachmentsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.attachments(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Attachments", + params, + properties: queryProperties(params), + } + ); + } + + createAttachment( + key: IntegrationTaskKey, + params: L.AttachmentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createAttachment(params); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Create Attachment", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Title", text: params.title }, + { label: "URL", text: params.url }, + ], + } + ); + } + + deleteAttachment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteAttachment(params.id), { + name: "Delete Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + }); + } + + updateAttachment( + key: IntegrationTaskKey, + params: { id: string; input: L.AttachmentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateAttachment(params.id, params.input); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Update Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + } + ); + } + + attachmentLinkDiscord( + key: IntegrationTaskKey, + params: { + channelId: string; + issueId: string; + messageId: string; + url: string; + variables?: Omit< + L.AttachmentLinkDiscordMutationVariables, + "channelId" | "issueId" | "messageId" | "url" + >; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkDiscord( + params.channelId, + params.issueId, + params.messageId, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Discord Message", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Channel ID", text: params.channelId }, + { label: "Message ID", text: params.messageId }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkFront( + key: IntegrationTaskKey, + params: { + conversationId: string; + issueId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkFront( + params.conversationId, + params.issueId, + params.variables + ); + return serializeLinearOutput(payload); + }, + { + name: "Link Front Conversation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Conversation ID", text: params.conversationId }, + ], + } + ); + } + + attachmentLinkIntercom( + key: IntegrationTaskKey, + params: { + conversationId: string; + issueId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkIntercom( + params.conversationId, + params.issueId, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Intercom Conversation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Conversation ID", text: params.conversationId }, + ], + } + ); + } + + attachmentLinkJiraIssue( + key: IntegrationTaskKey, + params: { + issueId: string; + jiraIssueId: string; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkJiraIssue(params.issueId, params.jiraIssueId); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Jira Issue", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Jira Issue ID", text: params.jiraIssueId }, + ], + } + ); + } + + attachmentLinkSlack( + key: IntegrationTaskKey, + params: { + channel: string; + issueId: string; + latest: string; + url: string; + variables?: Omit< + L.AttachmentLinkSlackMutationVariables, + "channel" | "issueId" | "latest" | "url" + >; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkSlack( + params.channel, + params.issueId, + params.latest, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Slack Message", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Channel", text: params.channel }, + { label: "Latest", text: params.latest }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkURL( + key: IntegrationTaskKey, + params: { + issueId: string; + url: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkURL( + params.issueId, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link URL", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkZendesk( + key: IntegrationTaskKey, + params: { + issueId: string; + ticketId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkZendesk( + params.issueId, + params.ticketId, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Zendesk Ticket", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Ticket ID", text: params.ticketId }, + ], + } + ); + } + + comment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.comment(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + comments( + key: IntegrationTaskKey, + params: L.CommentsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.comments(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Comments", + params, + properties: queryProperties(params), + } + ); + } + + createComment( + key: IntegrationTaskKey, + params: L.CommentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createComment(params); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Create Comment", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Body", text: params.body ?? "" }, + ], + } + ); + } + + deleteComment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteComment(params.id), { + name: "Delete Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + }); + } + + updateComment( + key: IntegrationTaskKey, + params: { id: string; input: L.CommentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateComment(params.id, params.input); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Update Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + archiveCycle( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveCycle(params.id); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + + createCycle( + key: IntegrationTaskKey, + params: L.CycleCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createCycle(params); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Create Cycle", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Start at", text: params.startsAt.toISOString() }, + { label: "Ends at", text: params.endsAt.toISOString() }, + ], + } + ); + } + + // deleteCycle() does not exist + + updateCycle( + key: IntegrationTaskKey, + params: { id: string; input: L.CycleUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateCycle(params.id, params.input); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Update Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + + document(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.document(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Document", + params, + properties: [{ label: "Document ID", text: params.id }], + } + ); + } + + documents( + key: IntegrationTaskKey, + params: L.DocumentsQueryVariables + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.documents(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Documents", + params, + properties: queryProperties(params), + } + ); + } + + createDocument( + key: IntegrationTaskKey, + params: L.DocumentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createDocument(params); + return serializeLinearOutput(await payload.document); + }, + { + name: "Create Document", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + searchDocuments( + key: IntegrationTaskKey, + params: { + term: string; + variables?: L.SearchDocumentsQueryVariables; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchDocuments(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Documents", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + favorite(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.favorite(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Favorite", + params, + properties: [{ label: "Favorite ID", text: params.id }], + } + ); + } + + favorites( + key: IntegrationTaskKey, + params: L.FavoritesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.favorites(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Favorites", + params, + properties: queryProperties(params), + } + ); + } + + createFavorite( + key: IntegrationTaskKey, + params: L.FavoriteCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createFavorite(params); + return serializeLinearOutput(await payload.favorite); + }, + { + name: "Create Favorite", + params, + } + ); + } + + issue(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issue(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issues( + key: IntegrationTaskKey, + params: L.IssuesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issues(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Issues", + params, + properties: queryProperties(params), + } + ); + } + + archiveIssue( + key: IntegrationTaskKey, + params: { + id: string; + variables?: Omit; + } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveIssue(params.id, params.variables); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + createIssue( + key: IntegrationTaskKey, + params: L.IssueCreateInput & { title: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssue(params); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Create Issue", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + deleteIssue( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteIssue(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + searchIssues( + key: IntegrationTaskKey, + params: { + term: string; + variables?: L.SearchIssuesQueryVariables; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchIssues(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Issues", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + updateIssue( + key: IntegrationTaskKey, + params: { id: string; input: L.IssueUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssue(params.id, params.input); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Update Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issueLabel(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueLabel(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueLabel", + params, + properties: [{ label: "IssueLabel ID", text: params.id }], + } + ); + } + + issueLabels( + key: IntegrationTaskKey, + params: L.IssueLabelsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueLabels(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueLabels", + params, + properties: queryProperties(params), + } + ); + } + + createIssueLabel( + key: IntegrationTaskKey, + params: L.IssueLabelCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueLabel(params); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Create IssueLabel", + params, + properties: [{ label: "Label name", text: params.name }], + } + ); + } + + deleteIssueLabel(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteIssueLabel(params.id), { + name: "Delete IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + }); + } + + updateIssueLabel( + key: IntegrationTaskKey, + params: { id: string; input: L.IssueLabelUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssueLabel(params.id, params.input); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Update IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + } + ); + } + + issuePriorityValues(key: IntegrationTaskKey): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issuePriorityValues; + return serializeLinearOutput(entity); + }, + { + name: "Get Issue Priority Values", + } + ); + } + + issueRelation(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueRelation(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueRelation", + params, + properties: [{ label: "IssueRelation ID", text: params.id }], + } + ); + } + + issueRelations( + key: IntegrationTaskKey, + params: L.IssueRelationsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueRelations(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueRelations", + params, + properties: queryProperties(params), + } + ); + } + + createIssueRelation( + key: IntegrationTaskKey, + params: L.IssueRelationCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueRelation(params); + return serializeLinearOutput(await payload.issueRelation); + }, + { + name: "Create IssueRelation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Related Issue ID", text: params.relatedIssueId }, + { label: "Relation Type", text: params.type }, + ], + } + ); + } + + notification(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.notification(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Notification", + params, + properties: [{ label: "Notification ID", text: params.id }], + } + ); + } + + notifications( + key: IntegrationTaskKey, + params: L.NotificationsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.notifications(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Notifications", + params, + properties: queryProperties(params), + } + ); + } + + archiveNotification( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveNotification(params.id); + return serializeLinearOutput(payload); + }, + { + name: "Archive Notification", + params, + properties: [{ label: "Notification ID", text: params.id }], + } + ); + } + + createNotificationSubscription( + key: IntegrationTaskKey, + params: { + input: L.NotificationSubscriptionCreateInput; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createNotificationSubscription(params.input); + return serializeLinearOutput(payload); + }, + { + name: "Create Notification Subscription", + params, + } + ); + } + + organization(key: IntegrationTaskKey): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.organization; + return serializeLinearOutput(entity); + }, + { + name: "Get Viewer's Organization", + } + ); + } + + createOrganizationFromOnboarding( + key: IntegrationTaskKey, + params: { + input: L.CreateOrganizationInput; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createOrganizationFromOnboarding( + params.input, + params.variables + ); + return serializeLinearOutput(await payload.organization); + }, + { + name: "Create Organization", + params, + properties: [ + { label: "Name", text: params.input.name }, + { label: "URL Key", text: params.input.urlKey }, + ], + } + ); + } + + /** WARNING: Causes internal server errors on Linear's side, regardless of input. */ + createOrganizationInvite( + key: IntegrationTaskKey, + params: { + input: L.OrganizationInviteCreateInput; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createOrganizationInvite(params.input); + return serializeLinearOutput(await payload.organizationInvite); + }, + { + name: "Create Organization Invite", + params, + properties: [{ label: "Invitee Email", text: params.input.email }], + } + ); + } + + project(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.project(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projects( + key: IntegrationTaskKey, + params: L.ProjectsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projects(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Projects", + params, + properties: queryProperties(params), + } + ); + } + + archiveProject( + key: IntegrationTaskKey, + params: { + id: string; + variables?: Omit; + } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveProject(params.id, params.variables); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + createProject( + key: IntegrationTaskKey, + params: L.ProjectCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProject(params); + return serializeLinearOutput(await payload.project); + }, + { + name: "Create Project", + params, + properties: [ + { label: "Team IDs", text: params.teamIds.join(", ") }, + { label: "Project name", text: params.name }, + ], + } + ); + } + + deleteProject( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteProject(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + searchProjects( + key: IntegrationTaskKey, + params: { + term: string; + variables?: L.SearchProjectsQueryVariables; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchProjects(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Projects", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + updateProject( + key: IntegrationTaskKey, + params: { id: string; input: L.ProjectUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProject(params.id, params.input); + return serializeLinearOutput(await payload.project); + }, + { + name: "Update Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projectLink(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectLink(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectLink", + params, + properties: [{ label: "ProjectLink ID", text: params.id }], + } + ); + } + + projectLinks( + key: IntegrationTaskKey, + params: L.ProjectLinksQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projectLinks(params); + return serializeLinearOutput(edges); + }, + { + name: "Get ProjectLinks", + params, + properties: queryProperties(params), + } + ); + } + + createProjectLink( + key: IntegrationTaskKey, + params: L.ProjectLinkCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectLink(params); + return serializeLinearOutput(await payload.projectLink); + }, + { + name: "Create ProjectLink", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Link Label", text: params.label }, + { label: "Link URL", text: params.url }, + ], + } + ); + } + + createProjectMilestone( + key: IntegrationTaskKey, + params: L.ProjectMilestoneCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectMilestone(params); + return serializeLinearOutput(await payload.projectMilestone); + }, + { + name: "Create ProjectMilestone", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Milestone Name", text: params.name }, + ], + } + ); + } + + projectUpdate(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectUpdate(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + projectUpdates( + key: IntegrationTaskKey, + params: L.ProjectUpdatesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projectUpdates(params); + return serializeLinearOutput(edges); + }, + { + name: "Get ProjectUpdates", + params, + properties: queryProperties(params), + } + ); + } + + createProjectUpdate( + key: IntegrationTaskKey, + params: L.ProjectUpdateCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectUpdate(params); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Create ProjectUpdate", + params, + properties: [{ label: "Project ID", text: params.projectId }], + } + ); + } + + deleteProjectUpdate(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteProjectUpdate(params.id), { + name: "Delete ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + }); + } + + updateProjectUpdate( + key: IntegrationTaskKey, + params: { id: string; input: L.ProjectUpdateUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProjectUpdate(params.id, params.input); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Update ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + createReaction( + key: IntegrationTaskKey, + params: L.ReactionCreateInput & { emoji: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createReaction(params); + return serializeLinearOutput(await payload.reaction); + }, + { + name: "Create Reaction", + params, + properties: [ + ...(params.commentId ? [{ label: "Comment ID", text: params.commentId }] : []), + ...(params.issueId ? [{ label: "Issue ID", text: params.issueId }] : []), + ...(params.projectUpdateId + ? [{ label: "ProjectUpdate ID", text: params.projectUpdateId }] + : []), + { label: "Emoji", text: params.emoji }, + ], + } + ); + } + + deleteReaction(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteReaction(params.id), { + name: "Delete Reaction", + params, + properties: [{ label: "Reaction ID", text: params.id }], + }); + } + + archiveRoadmap( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType> { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveRoadmap(params.id); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Roadmap", + params, + properties: [{ label: "Roadmap ID", text: params.id }], + } + ); + } + + createRoadmap( + key: IntegrationTaskKey, + params: L.RoadmapCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createRoadmap(params); + return serializeLinearOutput(await payload.roadmap); + }, + { + name: "Create Roadmap", + params, + properties: [{ label: "Roadmap Name", text: params.name }], + } + ); + } + + team(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.team(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Team", + params, + properties: [{ label: "Team ID", text: params.id }], + } + ); + } + + teams( + key: IntegrationTaskKey, + params: L.TeamsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.teams(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Teams", + params, + properties: queryProperties(params), + } + ); + } + + createTeam( + key: IntegrationTaskKey, + params: L.TeamCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createTeam(params); + return serializeLinearOutput(await payload.team); + }, + { + name: "Create Team", + params, + properties: [{ label: "Team Name", text: params.name }], + } + ); + } + + teamMembership( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.teamMembership(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get TeamMembership", + params, + properties: [{ label: "TeamMembership ID", text: params.id }], + } + ); + } + + teamMemberships( + key: IntegrationTaskKey, + params: L.TeamMembershipsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.teamMemberships(params); + return serializeLinearOutput(edges); + }, + { + name: "Get TeamMemberships", + params, + properties: queryProperties(params), + } + ); + } + + createTeamMembership( + key: IntegrationTaskKey, + params: L.TeamMembershipCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createTeamMembership(params); + return serializeLinearOutput(await payload.teamMembership); + }, + { + name: "Create TeamMembership", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "User ID", text: params.userId }, + ], + } + ); + } + + template(key: IntegrationTaskKey, params: { id: string }): LinearReturnType