Going exponential with Linear (#478)
* Unleash GPT magic * Clean up after GPT * All the hooks * Provisional integration catalog entry * Sample webhook jobs * Attachments with alpha warnings * Remove some verbose logs * Fix IP restrictions * Remove tunnel * Revert "Remove tunnel" This reverts commit c5b69ce6524e3b40c26b66cdc56e087b8576b8c6. * Resolve event name clashes * Remove circular dependency * Use correct payload uuid * Schema fixes * Fix webhook event name * Start to Linearify catalog entry * Remove todo * More catalog updates * Make OAuth work * Rename webhook helper * Schema juggling * More discrimination * Add Issue SLA event * Simplify triggers * Handle rate limits * Fix Project schema * Payload examples * Improve event props * Remove redundant source metadata * One type to rule them all * Recursive WithoutFunctions type * Linear output serializer * Some tasks * Update catalog entry * Dynamic usage sample * Bump version * Remove tunnel * More tasks * Add optional skipRetrying on runTask errors * Fail fast on user errors * Entity getter tasks * Another couple of tasks * Token to apiKey * Sort tasks * Add filtered issue SLA triggers * Add docs * Type fixes * Job catalog examples * Serialization helper docs * Add changeset * Refactor webhooks * Enhance properties * Pagination helper and docs * Clean up imports * Change misc catalog job --------- Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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=
|
||||
@@ -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,
|
||||
|
||||
@@ -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)],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,479 @@
|
||||
---
|
||||
title: Linear
|
||||
description: "Streamline your project and issue tracking"
|
||||
---
|
||||
|
||||
<Snippet file="integration-getting-started.mdx" />
|
||||
|
||||
## 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:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm install @trigger.dev/linear@latest
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/linear@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn add @trigger.dev/linear@latest
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## 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. |
|
||||
@@ -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 | 🕘 | ✅ |
|
||||
|
||||
@@ -244,6 +244,7 @@
|
||||
"integrations/apis/github-tasks"
|
||||
]
|
||||
},
|
||||
"integrations/apis/linear",
|
||||
"integrations/apis/openai",
|
||||
"integrations/apis/plain",
|
||||
"integrations/apis/resend",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
# @trigger.dev/linear
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<GetLinearPayload<AttachmentEvent>> = {
|
||||
name: "Attachment",
|
||||
title: "On Attachment",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [attachmentCreated, attachmentRemoved, attachmentUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<AttachmentEvent>,
|
||||
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<GetLinearPayload<AttachmentEvent, "create">> =
|
||||
{
|
||||
name: "Attachment",
|
||||
title: "On Attachment Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [attachmentCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<AttachmentEvent, "create">,
|
||||
runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
/** **WARNING:** Still in alpha - use with caution! */
|
||||
export const onAttachmentRemoved: EventSpecification<GetLinearPayload<AttachmentEvent, "remove">> =
|
||||
{
|
||||
name: "Attachment",
|
||||
title: "On Attachment Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [attachmentRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<AttachmentEvent, "remove">,
|
||||
runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
/** **WARNING:** Still in alpha - use with caution! */
|
||||
export const onAttachmentUpdated: EventSpecification<GetLinearPayload<AttachmentEvent, "update">> =
|
||||
{
|
||||
name: "Attachment",
|
||||
title: "On Attachment Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [attachmentUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<AttachmentEvent, "update">,
|
||||
runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onComment: EventSpecification<GetLinearPayload<CommentEvent>> = {
|
||||
name: "Comment",
|
||||
title: "On Comment",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [commentCreated, commentRemoved, commentUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CommentEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
...onCommentProperties(payload),
|
||||
...updatedFromProperties(payload),
|
||||
],
|
||||
};
|
||||
|
||||
export const onCommentCreated: EventSpecification<GetLinearPayload<CommentEvent, "create">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [commentCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CommentEvent, "create">,
|
||||
runProperties: (payload) => onCommentProperties(payload),
|
||||
};
|
||||
|
||||
export const onCommentRemoved: EventSpecification<GetLinearPayload<CommentEvent, "remove">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [commentRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CommentEvent, "remove">,
|
||||
runProperties: (payload) => onCommentProperties(payload),
|
||||
};
|
||||
|
||||
export const onCommentUpdated: EventSpecification<GetLinearPayload<CommentEvent, "update">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [commentUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CommentEvent, "update">,
|
||||
runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)],
|
||||
};
|
||||
|
||||
export const onCycle: EventSpecification<GetLinearPayload<CycleEvent>> = {
|
||||
name: "Cycle",
|
||||
title: "On Cycle",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [cycleCreated, cycleRemoved, cycleUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CycleEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
{ label: "Cycle ID", text: payload.data.id },
|
||||
],
|
||||
};
|
||||
|
||||
export const onCycleCreated: EventSpecification<GetLinearPayload<CycleEvent, "create">> = {
|
||||
name: "Cycle",
|
||||
title: "On Cycle Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [cycleCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CycleEvent, "create">,
|
||||
runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onCycleRemoved: EventSpecification<GetLinearPayload<CycleEvent, "remove">> = {
|
||||
name: "Cycle",
|
||||
title: "On Cycle Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [cycleRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CycleEvent, "remove">,
|
||||
runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onCycleUpdated: EventSpecification<GetLinearPayload<CycleEvent, "update">> = {
|
||||
name: "Cycle",
|
||||
title: "On Cycle Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [cycleUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<CycleEvent, "update">,
|
||||
runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onIssue: EventSpecification<GetLinearPayload<IssueEvent>> = {
|
||||
name: "Issue",
|
||||
title: "On Issue",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [issueCreated, issueRemoved, issueUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
...onIssueProperties(payload),
|
||||
...updatedFromProperties(payload),
|
||||
],
|
||||
};
|
||||
|
||||
export const onIssueCreated: EventSpecification<GetLinearPayload<IssueEvent, "create">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [issueCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueEvent, "create">,
|
||||
runProperties: (payload) => onIssueProperties(payload),
|
||||
};
|
||||
|
||||
export const onIssueRemoved: EventSpecification<GetLinearPayload<IssueEvent, "remove">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [issueRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueEvent, "remove">,
|
||||
runProperties: (payload) => onIssueProperties(payload),
|
||||
};
|
||||
|
||||
export const onIssueUpdated: EventSpecification<GetLinearPayload<IssueEvent, "update">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [issueUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueEvent, "update">,
|
||||
runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)],
|
||||
};
|
||||
|
||||
export const onIssueLabel: EventSpecification<GetLinearPayload<IssueLabelEvent>> = {
|
||||
name: "IssueLabel",
|
||||
title: "On IssueLabel",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueLabelEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
{ label: "IssueLabel ID", text: payload.data.id },
|
||||
],
|
||||
};
|
||||
|
||||
export const onIssueLabelCreated: EventSpecification<GetLinearPayload<IssueLabelEvent, "create">> =
|
||||
{
|
||||
name: "IssueLabel",
|
||||
title: "On IssueLabel Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [issueLabelCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueLabelEvent, "create">,
|
||||
runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onIssueLabelRemoved: EventSpecification<GetLinearPayload<IssueLabelEvent, "remove">> =
|
||||
{
|
||||
name: "IssueLabel",
|
||||
title: "On IssueLabel Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [issueLabelRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueLabelEvent, "remove">,
|
||||
runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onIssueLabelUpdated: EventSpecification<GetLinearPayload<IssueLabelEvent, "update">> =
|
||||
{
|
||||
name: "IssueLabel",
|
||||
title: "On IssueLabel Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [issueLabelUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueLabelEvent, "update">,
|
||||
runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
// TODO: this needs to be tested
|
||||
export const onIssueSLA: EventSpecification<GetLinearPayload<IssueSLAEvent>> = {
|
||||
name: "IssueSLA",
|
||||
title: "On Issue SLA",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueSLAEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "SLA action", text: payload.action },
|
||||
{ label: "Issue ID", text: payload.issueData.id },
|
||||
],
|
||||
};
|
||||
|
||||
export const onIssueSLASet: EventSpecification<GetLinearPayload<IssueSLAEvent, "set">> = {
|
||||
name: "IssueSLA",
|
||||
title: "On Issue SLA Set",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["set"],
|
||||
},
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueSLAEvent, "set">,
|
||||
runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }],
|
||||
};
|
||||
|
||||
export const onIssueSLABreached: EventSpecification<GetLinearPayload<IssueSLAEvent, "breached">> = {
|
||||
name: "IssueSLA",
|
||||
title: "On Issue SLA Breached",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["breached"],
|
||||
},
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueSLAEvent, "breached">,
|
||||
runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }],
|
||||
};
|
||||
|
||||
export const onIssueSLAHighRisk: EventSpecification<GetLinearPayload<IssueSLAEvent, "highRisk">> = {
|
||||
name: "IssueSLA",
|
||||
title: "On Issue SLA High Risk",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["highRisk"],
|
||||
},
|
||||
parsePayload: (payload) => payload as GetLinearPayload<IssueSLAEvent, "highRisk">,
|
||||
runProperties: (payload) => [{ label: "Issue ID", text: payload.issueData.id }],
|
||||
};
|
||||
|
||||
export const onProject: EventSpecification<GetLinearPayload<ProjectEvent>> = {
|
||||
name: "Project",
|
||||
title: "On Project",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [projectCreated, projectRemoved, projectUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectEvent>,
|
||||
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<GetLinearPayload<ProjectEvent, "create">> = {
|
||||
name: "Project",
|
||||
title: "On Project Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [projectCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectEvent, "create">,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Project ID", text: payload.data.id },
|
||||
{ label: "Project Name", text: payload.data.name, url: payload.url ?? undefined },
|
||||
],
|
||||
};
|
||||
|
||||
export const onProjectRemoved: EventSpecification<GetLinearPayload<ProjectEvent, "remove">> = {
|
||||
name: "Project",
|
||||
title: "On Project Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [projectRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectEvent, "remove">,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Project ID", text: payload.data.id },
|
||||
{ label: "Project Name", text: payload.data.name, url: payload.url ?? undefined },
|
||||
],
|
||||
};
|
||||
|
||||
export const onProjectUpdated: EventSpecification<GetLinearPayload<ProjectEvent, "update">> = {
|
||||
name: "Project",
|
||||
title: "On Project Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [projectUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectEvent, "update">,
|
||||
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<GetLinearPayload<ProjectUpdateEvent>> = {
|
||||
name: "ProjectUpdate",
|
||||
title: "On ProjectUpdate",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectUpdateEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
{ label: "ProjectUpdate ID", text: payload.data.id },
|
||||
],
|
||||
};
|
||||
|
||||
export const onProjectUpdateCreated: EventSpecification<
|
||||
GetLinearPayload<ProjectUpdateEvent, "create">
|
||||
> = {
|
||||
name: "ProjectUpdate",
|
||||
title: "On ProjectUpdate Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [projectUpdateCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectUpdateEvent, "create">,
|
||||
runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onProjectUpdateRemoved: EventSpecification<
|
||||
GetLinearPayload<ProjectUpdateEvent, "remove">
|
||||
> = {
|
||||
name: "ProjectUpdate",
|
||||
title: "On ProjectUpdate Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [projectUpdateRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectUpdateEvent, "remove">,
|
||||
runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onProjectUpdateUpdated: EventSpecification<
|
||||
GetLinearPayload<ProjectUpdateEvent, "update">
|
||||
> = {
|
||||
name: "ProjectUpdate",
|
||||
title: "On ProjectUpdate Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [projectUpdateUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ProjectUpdateEvent, "update">,
|
||||
runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onReaction: EventSpecification<GetLinearPayload<ReactionEvent>> = {
|
||||
name: "Reaction",
|
||||
title: "On Reaction",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [reactionCreated, reactionRemoved, reactionUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ReactionEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
{ label: "Reaction ID", text: payload.data.id },
|
||||
],
|
||||
};
|
||||
|
||||
export const onReactionCreated: EventSpecification<GetLinearPayload<ReactionEvent, "create">> = {
|
||||
name: "Reaction",
|
||||
title: "On Reaction Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [reactionCreated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ReactionEvent, "create">,
|
||||
runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onReactionRemoved: EventSpecification<GetLinearPayload<ReactionEvent, "remove">> = {
|
||||
name: "Reaction",
|
||||
title: "On Reaction Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [reactionRemoved],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ReactionEvent, "remove">,
|
||||
runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }],
|
||||
};
|
||||
|
||||
export const onReactionUpdated: EventSpecification<GetLinearPayload<ReactionEvent, "update">> = {
|
||||
name: "Reaction",
|
||||
title: "On Reaction Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [reactionUpdated],
|
||||
parsePayload: (payload) => payload as GetLinearPayload<ReactionEvent, "update">,
|
||||
runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }],
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
|
||||
"url": "https://www.rickastley.co.uk/",
|
||||
"title": "My Personal Website",
|
||||
"source": {
|
||||
"type": "api",
|
||||
"imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
|
||||
},
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"metadata": {},
|
||||
"subtitle": "Rick Astley Official Site",
|
||||
"createdAt": "2023-09-17T10:51:34.932Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T10:51:34.932Z",
|
||||
"sourceType": "api",
|
||||
"groupBySource": false
|
||||
},
|
||||
"type": "Attachment",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T10:51:34.932Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:51:37.795Z"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
|
||||
"url": "https://www.rickastley.co.uk/",
|
||||
"title": "My Personal Website",
|
||||
"source": {
|
||||
"type": "api",
|
||||
"imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
|
||||
},
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"metadata": {},
|
||||
"subtitle": "Rick Astley Official Site",
|
||||
"createdAt": "2023-09-17T10:51:34.932Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T10:51:34.932Z",
|
||||
"sourceType": "api",
|
||||
"groupBySource": false
|
||||
},
|
||||
"type": "Attachment",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T10:51:34.932Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:51:37.795Z"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
|
||||
"url": "https://www.rickastley.co.uk/",
|
||||
"title": "Will Never Give You Up",
|
||||
"source": {
|
||||
"type": "api",
|
||||
"imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
|
||||
},
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"metadata": {},
|
||||
"subtitle": "Rick Astley Official Site",
|
||||
"createdAt": "2023-09-17T10:51:34.932Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T10:51:55.751Z",
|
||||
"sourceType": "api",
|
||||
"groupBySource": false
|
||||
},
|
||||
"type": "Attachment",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T10:52:15.457Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:52:15.938Z"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad",
|
||||
"url": "https://www.rickastley.co.uk/",
|
||||
"title": "Will Never Give You Up",
|
||||
"source": {
|
||||
"type": "api",
|
||||
"imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c"
|
||||
},
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"metadata": {},
|
||||
"subtitle": "Rick Astley Official Site",
|
||||
"createdAt": "2023-09-17T10:51:34.932Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T10:51:55.751Z",
|
||||
"sourceType": "api",
|
||||
"groupBySource": false
|
||||
},
|
||||
"type": "Attachment",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T10:51:55.751Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"title": "My Personal Website",
|
||||
"updatedAt": "2023-09-17T10:51:34.932Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:51:55.809Z"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd",
|
||||
"data": {
|
||||
"id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
|
||||
"body": "I just wanna tell you how I'm feeling",
|
||||
"issue": {
|
||||
"id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"title": "Welcome to Rick's World ❤️🔥"
|
||||
},
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"createdAt": "2023-09-17T10:57:11.751Z",
|
||||
"updatedAt": "2023-09-17T10:57:11.751Z",
|
||||
"reactionData": []
|
||||
},
|
||||
"type": "Comment",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T10:57:11.751Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:57:11.842Z"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd",
|
||||
"data": {
|
||||
"id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
|
||||
"body": "I just wanna tell you how I'm feeling",
|
||||
"issue": {
|
||||
"id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"title": "Welcome to Rick's World ❤️🔥"
|
||||
},
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"createdAt": "2023-09-17T10:57:11.751Z",
|
||||
"updatedAt": "2023-09-17T10:57:11.751Z",
|
||||
"reactionData": []
|
||||
},
|
||||
"type": "Comment",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T10:57:11.751Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:57:11.842Z"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
|
||||
"body": "Gotta make you understand",
|
||||
"issue": {
|
||||
"id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"title": "Welcome to Rick's World ❤️🔥"
|
||||
},
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"editedAt": "2023-09-17T10:57:24.386Z",
|
||||
"createdAt": "2023-09-17T10:57:11.751Z",
|
||||
"updatedAt": "2023-09-17T10:57:24.387Z",
|
||||
"reactionData": []
|
||||
},
|
||||
"type": "Comment",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T10:57:37.324Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:57:37.376Z"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd",
|
||||
"data": {
|
||||
"id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95",
|
||||
"body": "Gotta make you understand",
|
||||
"issue": {
|
||||
"id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"title": "Welcome to Rick's World ❤️🔥"
|
||||
},
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"editedAt": "2023-09-17T10:57:24.386Z",
|
||||
"createdAt": "2023-09-17T10:57:11.751Z",
|
||||
"updatedAt": "2023-09-17T10:57:24.387Z",
|
||||
"reactionData": []
|
||||
},
|
||||
"type": "Comment",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T10:57:24.387Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"body": "I just wanna tell you how I'm feeling",
|
||||
"editedAt": null,
|
||||
"updatedAt": "2023-09-17T10:57:11.751Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:57:24.446Z"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "cf1f0ba0-a769-402c-a5a4-308bca3ff53c",
|
||||
"endsAt": "2023-10-08T23:00:00.000Z",
|
||||
"number": 1,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"startsAt": "2023-09-24T23:00:00.000Z",
|
||||
"createdAt": "2023-09-17T11:00:19.616Z",
|
||||
"updatedAt": "2023-09-17T11:00:19.616Z",
|
||||
"scopeHistory": [],
|
||||
"issueCountHistory": [],
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": [],
|
||||
"uncompletedIssuesUponCloseIds": []
|
||||
},
|
||||
"type": "Cycle",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:00:19.616Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:00:19.696Z"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "cf1f0ba0-a769-402c-a5a4-308bca3ff53c",
|
||||
"endsAt": "2023-10-08T23:00:00.000Z",
|
||||
"number": 1,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"startsAt": "2023-09-24T23:00:00.000Z",
|
||||
"createdAt": "2023-09-17T11:00:19.616Z",
|
||||
"updatedAt": "2023-09-17T11:00:19.616Z",
|
||||
"scopeHistory": [],
|
||||
"issueCountHistory": [],
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": [],
|
||||
"uncompletedIssuesUponCloseIds": []
|
||||
},
|
||||
"type": "Cycle",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:00:19.616Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:00:19.696Z"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "fe7092dd-8a68-4a07-9477-b7c0a6f2136d",
|
||||
"endsAt": "2023-11-13T00:00:00.000Z",
|
||||
"number": 2,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"startsAt": "2023-10-15T23:00:00.000Z",
|
||||
"createdAt": "2023-09-17T11:00:19.616Z",
|
||||
"updatedAt": "2023-09-17T11:00:47.427Z",
|
||||
"scopeHistory": [],
|
||||
"issueCountHistory": [],
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": [],
|
||||
"uncompletedIssuesUponCloseIds": []
|
||||
},
|
||||
"type": "Cycle",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T11:01:08.772Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:01:08.807Z"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "fe7092dd-8a68-4a07-9477-b7c0a6f2136d",
|
||||
"endsAt": "2023-11-13T00:00:00.000Z",
|
||||
"number": 2,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"startsAt": "2023-10-15T23:00:00.000Z",
|
||||
"createdAt": "2023-09-17T11:00:19.616Z",
|
||||
"updatedAt": "2023-09-17T11:00:47.427Z",
|
||||
"scopeHistory": [],
|
||||
"issueCountHistory": [],
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": [],
|
||||
"uncompletedIssuesUponCloseIds": []
|
||||
},
|
||||
"type": "Cycle",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T11:00:47.427Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"endsAt": "2023-11-12T23:00:00.000Z",
|
||||
"updatedAt": "2023-09-17T11:00:47.414Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:00:47.463Z"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-ricks-world-👋",
|
||||
"data": {
|
||||
"id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"team": {
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"key": "TRI",
|
||||
"name": "Trigger.dev"
|
||||
},
|
||||
"state": {
|
||||
"id": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
|
||||
"name": "Todo",
|
||||
"type": "unstarted",
|
||||
"color": "#e2e2e2"
|
||||
},
|
||||
"title": "Welcome to Rick's World ❤️🔥",
|
||||
"labels": [],
|
||||
"number": 1,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"stateId": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
|
||||
"labelIds": [],
|
||||
"priority": 2,
|
||||
"createdAt": "2023-09-17T10:43:20.862Z",
|
||||
"sortOrder": -13701.52,
|
||||
"updatedAt": "2023-09-17T10:52:49.321Z",
|
||||
"boardOrder": 0,
|
||||
"description": "Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\n\n### **To start, type** `C` to **create your first issue.**\n\nCreate issues from any view using `C` or by clicking the `New issue` button.\n\n \n\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\n\nOur issue editor and comments support Markdown. You can also: \n\n* @mention a teammate\n* Drag & drop images or video (Loom & Youtube embed automatically)\n* Use emoji ✅",
|
||||
"priorityLabel": "High",
|
||||
"subscriberIds": [],
|
||||
"previousIdentifiers": []
|
||||
},
|
||||
"type": "Issue",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T10:52:49.321Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"title": "Welcome to Linear 👋",
|
||||
"updatedAt": "2023-09-17T10:52:16.952Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:52:49.762Z"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/issue/TRI-10/you-know-the-rules",
|
||||
"data": {
|
||||
"id": "4ceeeecb-3442-4972-b27a-4ac7198d4eac",
|
||||
"team": {
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"key": "TRI",
|
||||
"name": "Trigger.dev"
|
||||
},
|
||||
"state": {
|
||||
"id": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
|
||||
"name": "Backlog",
|
||||
"type": "backlog",
|
||||
"color": "#bec2c8"
|
||||
},
|
||||
"title": "You know the rules",
|
||||
"labels": [],
|
||||
"number": 10,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"stateId": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
|
||||
"labelIds": [],
|
||||
"priority": 0,
|
||||
"createdAt": "2023-09-17T11:02:48.688Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"sortOrder": -81,
|
||||
"updatedAt": "2023-09-17T11:02:48.688Z",
|
||||
"boardOrder": 0,
|
||||
"description": "And so do I",
|
||||
"priorityLabel": "No priority",
|
||||
"subscriberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"previousIdentifiers": []
|
||||
},
|
||||
"type": "Issue",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:02:48.688Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:02:48.800Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
|
||||
"name": "Give You Up",
|
||||
"color": "#26b5ce",
|
||||
"createdAt": "2023-09-17T11:08:22.426Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T11:08:22.426Z",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
|
||||
},
|
||||
"type": "IssueLabel",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:08:22.426Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:08:22.463Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
|
||||
"name": "Give You Up",
|
||||
"color": "#26b5ce",
|
||||
"createdAt": "2023-09-17T11:08:22.426Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T11:08:22.426Z",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
|
||||
},
|
||||
"type": "IssueLabel",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:08:22.426Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:08:22.463Z"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
|
||||
"name": "Let You Down",
|
||||
"color": "#f2994a",
|
||||
"createdAt": "2023-09-17T11:08:22.426Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T11:08:31.484Z",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
|
||||
},
|
||||
"type": "IssueLabel",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T11:08:35.517Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:08:35.556Z"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16",
|
||||
"name": "Let You Down",
|
||||
"color": "#f2994a",
|
||||
"createdAt": "2023-09-17T11:08:22.426Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"updatedAt": "2023-09-17T11:08:31.484Z",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534"
|
||||
},
|
||||
"type": "IssueLabel",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T11:08:31.484Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"name": "Give You Up",
|
||||
"color": "#26b5ce",
|
||||
"updatedAt": "2023-09-17T11:08:22.426Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:08:31.527Z"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "4ceeeecb-3442-4972-b27a-4ac7198d4eac",
|
||||
"team": {
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"key": "TRI",
|
||||
"name": "Trigger.dev"
|
||||
},
|
||||
"state": {
|
||||
"id": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
|
||||
"name": "Backlog",
|
||||
"type": "backlog",
|
||||
"color": "#bec2c8"
|
||||
},
|
||||
"title": "You know the rules",
|
||||
"labels": [],
|
||||
"number": 10,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"stateId": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b",
|
||||
"trashed": true,
|
||||
"labelIds": [],
|
||||
"priority": 0,
|
||||
"createdAt": "2023-09-17T11:02:48.688Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"sortOrder": -81,
|
||||
"updatedAt": "2023-09-17T11:02:48.689Z",
|
||||
"archivedAt": "2023-09-17T11:02:58.487Z",
|
||||
"boardOrder": 0,
|
||||
"description": "And so do I",
|
||||
"priorityLabel": "No priority",
|
||||
"subscriberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"previousIdentifiers": []
|
||||
},
|
||||
"type": "Issue",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T11:02:58.487Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:02:58.564Z"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-ricks-world-👋",
|
||||
"data": {
|
||||
"id": "ee4d5612-0752-441a-95cd-2a0de56689d3",
|
||||
"team": {
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"key": "TRI",
|
||||
"name": "Trigger.dev"
|
||||
},
|
||||
"state": {
|
||||
"id": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
|
||||
"name": "Todo",
|
||||
"type": "unstarted",
|
||||
"color": "#e2e2e2"
|
||||
},
|
||||
"title": "Welcome to Rick's World ❤️🔥",
|
||||
"labels": [],
|
||||
"number": 1,
|
||||
"teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"stateId": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2",
|
||||
"labelIds": [],
|
||||
"priority": 2,
|
||||
"createdAt": "2023-09-17T10:43:20.862Z",
|
||||
"sortOrder": -13701.52,
|
||||
"updatedAt": "2023-09-17T10:52:49.321Z",
|
||||
"boardOrder": 0,
|
||||
"description": "Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\n\n### **To start, type** `C` to **create your first issue.**\n\nCreate issues from any view using `C` or by clicking the `New issue` button.\n\n \n\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\n\nOur issue editor and comments support Markdown. You can also: \n\n* @mention a teammate\n* Drag & drop images or video (Loom & Youtube embed automatically)\n* Use emoji ✅",
|
||||
"priorityLabel": "High",
|
||||
"subscriberIds": [],
|
||||
"previousIdentifiers": []
|
||||
},
|
||||
"type": "Issue",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T10:52:49.321Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"title": "Welcome to Linear 👋",
|
||||
"updatedAt": "2023-09-17T10:52:16.952Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T10:52:49.762Z"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512",
|
||||
"data": {
|
||||
"id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
|
||||
"name": "Rick's World",
|
||||
"color": "#bec2c8",
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"slugId": "1e55e5e64512",
|
||||
"teamIds": [
|
||||
"0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
|
||||
],
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:24:43.171Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"sortOrder": 8161.55,
|
||||
"updatedAt": "2023-09-17T11:24:43.171Z",
|
||||
"milestones": [],
|
||||
"description": "We know the game and we're gonna play it",
|
||||
"scopeHistory": [],
|
||||
"slackNewIssue": true,
|
||||
"issueCountHistory": [],
|
||||
"slackIssueComments": true,
|
||||
"slackIssueStatuses": true,
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": []
|
||||
},
|
||||
"type": "Project",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:24:43.171Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:24:43.228Z"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512",
|
||||
"data": {
|
||||
"id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
|
||||
"name": "Rick's World",
|
||||
"color": "#bec2c8",
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"slugId": "1e55e5e64512",
|
||||
"teamIds": [
|
||||
"0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
|
||||
],
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:24:43.171Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"sortOrder": 8161.55,
|
||||
"updatedAt": "2023-09-17T11:24:43.171Z",
|
||||
"milestones": [],
|
||||
"description": "We know the game and we're gonna play it",
|
||||
"scopeHistory": [],
|
||||
"slackNewIssue": true,
|
||||
"issueCountHistory": [],
|
||||
"slackIssueComments": true,
|
||||
"slackIssueStatuses": true,
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": []
|
||||
},
|
||||
"type": "Project",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:24:43.171Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:24:43.228Z"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
|
||||
"name": "Rick's World",
|
||||
"color": "#bec2c8",
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"slugId": "1e55e5e64512",
|
||||
"teamIds": [
|
||||
"0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
|
||||
],
|
||||
"trashed": true,
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:24:43.171Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"sortOrder": 8161.55,
|
||||
"updatedAt": "2023-09-17T11:25:09.384Z",
|
||||
"archivedAt": "2023-09-17T11:25:16.188Z",
|
||||
"milestones": [],
|
||||
"targetDate": "2024-02-14T00:00:00.000Z",
|
||||
"description": "We know the game and we're gonna play it",
|
||||
"scopeHistory": [],
|
||||
"slackNewIssue": true,
|
||||
"issueCountHistory": [],
|
||||
"slackIssueComments": true,
|
||||
"slackIssueStatuses": true,
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": []
|
||||
},
|
||||
"type": "Project",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T11:25:16.188Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:25:16.358Z"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
|
||||
"body": "We're no strangers to love",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"health": "onTrack",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"project": {
|
||||
"id": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"name": "Rick's World"
|
||||
},
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:28:23.406Z",
|
||||
"projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"updatedAt": "2023-09-17T11:28:23.406Z",
|
||||
"infoSnapshot": {
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"teamsInfo": [
|
||||
{
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"issueInfo": {
|
||||
"triageCount": 0,
|
||||
"backlogCount": 0,
|
||||
"startedCount": 0,
|
||||
"canceledCount": 0,
|
||||
"completedCount": 0,
|
||||
"unstartedCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"milestonesInfo": []
|
||||
}
|
||||
},
|
||||
"type": "ProjectUpdate",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:28:23.406Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:28:23.479Z"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
|
||||
"body": "We're no strangers to love",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"health": "onTrack",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"project": {
|
||||
"id": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"name": "Rick's World"
|
||||
},
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:28:23.406Z",
|
||||
"projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"updatedAt": "2023-09-17T11:28:23.406Z",
|
||||
"infoSnapshot": {
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"teamsInfo": [
|
||||
{
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"issueInfo": {
|
||||
"triageCount": 0,
|
||||
"backlogCount": 0,
|
||||
"startedCount": 0,
|
||||
"canceledCount": 0,
|
||||
"completedCount": 0,
|
||||
"unstartedCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"milestonesInfo": []
|
||||
}
|
||||
},
|
||||
"type": "ProjectUpdate",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:28:23.406Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:28:23.479Z"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
|
||||
"body": "You know the rules and so do I",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"health": "onTrack",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"project": {
|
||||
"id": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"name": "Rick's World"
|
||||
},
|
||||
"editedAt": "2023-09-17T11:28:39.142Z",
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:28:23.406Z",
|
||||
"projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"updatedAt": "2023-09-17T11:28:39.142Z",
|
||||
"infoSnapshot": {
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"teamsInfo": [
|
||||
{
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"issueInfo": {
|
||||
"triageCount": 0,
|
||||
"backlogCount": 0,
|
||||
"startedCount": 0,
|
||||
"canceledCount": 0,
|
||||
"completedCount": 0,
|
||||
"unstartedCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"milestonesInfo": []
|
||||
}
|
||||
},
|
||||
"type": "ProjectUpdate",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T11:28:45.146Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:28:45.213Z"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4",
|
||||
"body": "You know the rules and so do I",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"health": "onTrack",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"project": {
|
||||
"id": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"name": "Rick's World"
|
||||
},
|
||||
"editedAt": "2023-09-17T11:28:39.142Z",
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:28:23.406Z",
|
||||
"projectId": "54dac280-389f-4339-8d5d-de347a56eb79",
|
||||
"updatedAt": "2023-09-17T11:28:39.142Z",
|
||||
"infoSnapshot": {
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"teamsInfo": [
|
||||
{
|
||||
"id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f",
|
||||
"issueInfo": {
|
||||
"triageCount": 0,
|
||||
"backlogCount": 0,
|
||||
"startedCount": 0,
|
||||
"canceledCount": 0,
|
||||
"completedCount": 0,
|
||||
"unstartedCount": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"milestonesInfo": []
|
||||
}
|
||||
},
|
||||
"type": "ProjectUpdate",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T11:28:39.142Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"body": "We're no strangers to love",
|
||||
"editedAt": null,
|
||||
"updatedAt": "2023-09-17T11:28:23.406Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:28:39.181Z"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512",
|
||||
"data": {
|
||||
"id": "708440e6-648c-40f9-be87-9e00bbd8d1ec",
|
||||
"name": "Rick's World",
|
||||
"color": "#bec2c8",
|
||||
"state": "backlog",
|
||||
"leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"slugId": "1e55e5e64512",
|
||||
"teamIds": [
|
||||
"0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f"
|
||||
],
|
||||
"roadmaps": [],
|
||||
"createdAt": "2023-09-17T11:24:43.171Z",
|
||||
"creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"memberIds": [
|
||||
"b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
],
|
||||
"sortOrder": 8161.55,
|
||||
"updatedAt": "2023-09-17T11:25:09.383Z",
|
||||
"milestones": [],
|
||||
"targetDate": "2024-02-14T00:00:00.000Z",
|
||||
"description": "We know the game and we're gonna play it",
|
||||
"scopeHistory": [],
|
||||
"slackNewIssue": true,
|
||||
"issueCountHistory": [],
|
||||
"slackIssueComments": true,
|
||||
"slackIssueStatuses": true,
|
||||
"completedScopeHistory": [],
|
||||
"inProgressScopeHistory": [],
|
||||
"completedIssueCountHistory": []
|
||||
},
|
||||
"type": "Project",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T11:25:09.383Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"updatedAt": "2023-09-17T11:24:43.171Z",
|
||||
"targetDate": null
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:25:09.419Z"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"emoji": "heart",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"comment": {
|
||||
"id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
|
||||
"body": "Never gonna",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
},
|
||||
"createdAt": "2023-09-17T11:31:27.394Z",
|
||||
"updatedAt": "2023-09-17T11:31:27.394Z"
|
||||
},
|
||||
"type": "Reaction",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:31:27.394Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:31:27.477Z"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"emoji": "heart",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"comment": {
|
||||
"id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
|
||||
"body": "Never gonna!",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
},
|
||||
"createdAt": "2023-09-17T11:31:27.394Z",
|
||||
"updatedAt": "2023-09-17T11:31:27.394Z"
|
||||
},
|
||||
"type": "Reaction",
|
||||
"action": "create",
|
||||
"createdAt": "2023-09-17T11:31:27.394Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:31:27.477Z"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"emoji": "heart",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"comment": {
|
||||
"id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
|
||||
"body": "Never gonna!",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
},
|
||||
"createdAt": "2023-09-17T11:31:27.394Z",
|
||||
"updatedAt": "2023-09-17T11:31:27.394Z"
|
||||
},
|
||||
"type": "Reaction",
|
||||
"action": "remove",
|
||||
"createdAt": "2023-09-17T11:32:07.003Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T11:32:07.088Z"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"data": {
|
||||
"id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944",
|
||||
"user": {
|
||||
"id": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"name": "Rick Astley"
|
||||
},
|
||||
"emoji": "heart_on_fire",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f",
|
||||
"comment": {
|
||||
"id": "3f44c353-a3a1-45a4-b5da-259818ae21dd",
|
||||
"body": "Never gonna!",
|
||||
"userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f"
|
||||
},
|
||||
"createdAt": "2023-09-17T12:31:27.394Z",
|
||||
"updatedAt": "2023-09-17T12:32:14.394Z"
|
||||
},
|
||||
"type": "Reaction",
|
||||
"action": "update",
|
||||
"createdAt": "2023-09-17T12:31:27.394Z",
|
||||
"webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f",
|
||||
"updatedFrom": {
|
||||
"emoji": "heart",
|
||||
"updatedAt": "2023-09-17T12:31:27.394Z"
|
||||
},
|
||||
"organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534",
|
||||
"webhookTimestamp": "2023-09-17T12:31:27.477Z"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { EventSpecificationExample } from "@trigger.dev/sdk";
|
||||
|
||||
import AttachmentCreated from "./AttachmentCreated.json"
|
||||
import AttachmentRemoved from "./AttachmentRemoved.json"
|
||||
import AttachmentUpdated from "./AttachmentUpdated.json"
|
||||
import CommentCreated from "./CommentCreated.json"
|
||||
import CommentRemoved from "./CommentRemoved.json"
|
||||
import CommentUpdated from "./CommentUpdated.json"
|
||||
import CycleCreated from "./CycleCreated.json"
|
||||
import CycleRemoved from "./CycleRemoved.json"
|
||||
import CycleUpdated from "./CycleUpdated.json"
|
||||
import IssueCreated from "./IssueCreated.json"
|
||||
import IssueRemoved from "./IssueRemoved.json"
|
||||
import IssueUpdated from "./IssueUpdated.json"
|
||||
import IssueLabelCreated from "./IssueLabelCreated.json"
|
||||
import IssueLabelRemoved from "./IssueLabelRemoved.json"
|
||||
import IssueLabelUpdated from "./IssueLabelUpdated.json"
|
||||
import ProjectCreated from "./ProjectCreated.json"
|
||||
import ProjectRemoved from "./ProjectRemoved.json"
|
||||
import ProjectUpdated from "./ProjectUpdated.json"
|
||||
import ProjectUpdateCreated from "./ProjectUpdateCreated.json"
|
||||
import ProjectUpdateRemoved from "./ProjectUpdateRemoved.json"
|
||||
import ProjectUpdateUpdated from "./ProjectUpdateUpdated.json"
|
||||
import ReactionCreated from "./ReactionCreated.json"
|
||||
import ReactionRemoved from "./ReactionRemoved.json"
|
||||
import ReactionUpdated from "./ReactionUpdated.json"
|
||||
|
||||
export const attachmentCreated: EventSpecificationExample = {
|
||||
id: "AttachmentCreated",
|
||||
name: "Attachment created",
|
||||
payload: AttachmentCreated,
|
||||
};
|
||||
export const attachmentRemoved: EventSpecificationExample = {
|
||||
id: "AttachmentRemoved",
|
||||
name: "Attachment removed",
|
||||
payload: AttachmentRemoved,
|
||||
};
|
||||
export const attachmentUpdated: EventSpecificationExample = {
|
||||
id: "AttachmentUpdated",
|
||||
name: "Attachment updated",
|
||||
payload: AttachmentUpdated,
|
||||
};
|
||||
|
||||
export const commentCreated: EventSpecificationExample = {
|
||||
id: "CommentCreated",
|
||||
name: "Comment created",
|
||||
payload: CommentCreated,
|
||||
};
|
||||
export const commentRemoved: EventSpecificationExample = {
|
||||
id: "CommentRemoved",
|
||||
name: "Comment removed",
|
||||
payload: CommentRemoved,
|
||||
};
|
||||
export const commentUpdated: EventSpecificationExample = {
|
||||
id: "CommentUpdated",
|
||||
name: "Comment updated",
|
||||
payload: CommentUpdated,
|
||||
};
|
||||
|
||||
export const cycleCreated: EventSpecificationExample = {
|
||||
id: "CycleCreated",
|
||||
name: "Cycle created",
|
||||
payload: CycleCreated,
|
||||
};
|
||||
export const cycleRemoved: EventSpecificationExample = {
|
||||
id: "CycleRemoved",
|
||||
name: "Cycle removed",
|
||||
payload: CycleRemoved,
|
||||
};
|
||||
export const cycleUpdated: EventSpecificationExample = {
|
||||
id: "CycleUpdated",
|
||||
name: "Cycle updated",
|
||||
payload: CycleUpdated,
|
||||
};
|
||||
|
||||
export const issueCreated: EventSpecificationExample = {
|
||||
id: "IssueCreated",
|
||||
name: "Issue created",
|
||||
payload: IssueCreated,
|
||||
};
|
||||
export const issueRemoved: EventSpecificationExample = {
|
||||
id: "IssueRemoved",
|
||||
name: "Issue removed",
|
||||
payload: IssueRemoved,
|
||||
};
|
||||
export const issueUpdated: EventSpecificationExample = {
|
||||
id: "IssueUpdated",
|
||||
name: "Issue updated",
|
||||
payload: IssueUpdated,
|
||||
};
|
||||
|
||||
export const issueLabelCreated: EventSpecificationExample = {
|
||||
id: "IssueLabelCreated",
|
||||
name: "IssueLabel created",
|
||||
payload: IssueLabelCreated,
|
||||
};
|
||||
export const issueLabelRemoved: EventSpecificationExample = {
|
||||
id: "IssueLabelRemoved",
|
||||
name: "IssueLabel removed",
|
||||
payload: IssueLabelRemoved,
|
||||
};
|
||||
export const issueLabelUpdated: EventSpecificationExample = {
|
||||
id: "IssueLabelUpdated",
|
||||
name: "IssueLabel updated",
|
||||
payload: IssueLabelUpdated,
|
||||
};
|
||||
|
||||
export const projectCreated: EventSpecificationExample = {
|
||||
id: "ProjectCreated",
|
||||
name: "Project created",
|
||||
payload: ProjectCreated,
|
||||
};
|
||||
export const projectRemoved: EventSpecificationExample = {
|
||||
id: "ProjectRemoved",
|
||||
name: "Project removed",
|
||||
payload: ProjectRemoved,
|
||||
};
|
||||
export const projectUpdated: EventSpecificationExample = {
|
||||
id: "ProjectUpdated",
|
||||
name: "Project updated",
|
||||
payload: ProjectUpdated,
|
||||
};
|
||||
|
||||
export const projectUpdateCreated: EventSpecificationExample = {
|
||||
id: "ProjectUpdateCreated",
|
||||
name: "ProjectUpdate created",
|
||||
payload: ProjectUpdateCreated,
|
||||
};
|
||||
export const projectUpdateRemoved: EventSpecificationExample = {
|
||||
id: "ProjectUpdateRemoved",
|
||||
name: "ProjectUpdate removed",
|
||||
payload: ProjectUpdateRemoved,
|
||||
};
|
||||
export const projectUpdateUpdated: EventSpecificationExample = {
|
||||
id: "ProjectUpdateUpdated",
|
||||
name: "ProjectUpdate updated",
|
||||
payload: ProjectUpdateUpdated,
|
||||
};
|
||||
|
||||
export const reactionCreated: EventSpecificationExample = {
|
||||
id: "ReactionCreated",
|
||||
name: "Reaction created",
|
||||
payload: ReactionCreated,
|
||||
};
|
||||
export const reactionRemoved: EventSpecificationExample = {
|
||||
id: "ReactionRemoved",
|
||||
name: "Reaction removed",
|
||||
payload: ReactionRemoved,
|
||||
};
|
||||
export const reactionUpdated: EventSpecificationExample = {
|
||||
id: "ReactionUpdated",
|
||||
name: "Reaction updated",
|
||||
payload: ReactionUpdated,
|
||||
};
|
||||
@@ -0,0 +1,444 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const WebhookResourceTypeSchema = z.union([
|
||||
z.literal("Attachment"),
|
||||
z.literal("Comment"),
|
||||
z.literal("Cycle"),
|
||||
z.literal("Issue"),
|
||||
z.literal("IssueLabel"),
|
||||
z.literal("IssueSLA"),
|
||||
z.literal("Project"),
|
||||
z.literal("ProjectUpdate"),
|
||||
z.literal("Reaction"),
|
||||
]);
|
||||
export type WebhookResourceType = z.infer<typeof WebhookResourceTypeSchema>;
|
||||
|
||||
export const WebhookChangeActionTypeSchema = z.union([
|
||||
z.literal("create"),
|
||||
z.literal("remove"),
|
||||
z.literal("update"),
|
||||
]);
|
||||
export type WebhookChangeActionType = z.infer<typeof WebhookChangeActionTypeSchema>;
|
||||
|
||||
export const WebhookSLAActionTypeSchema = z.union([
|
||||
z.literal("set"),
|
||||
z.literal("breached"),
|
||||
z.literal("highRisk"),
|
||||
]);
|
||||
export type WebhookSLAActionType = z.infer<typeof WebhookSLAActionTypeSchema>;
|
||||
|
||||
export const WebhookActionTypeSchema = WebhookChangeActionTypeSchema.or(WebhookSLAActionTypeSchema);
|
||||
export type WebhookActionType = z.infer<typeof WebhookActionTypeSchema>;
|
||||
|
||||
const IssueLabelDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
color: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
creatorId: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
// isGroup: z.boolean(), // missing
|
||||
name: z.string(),
|
||||
organizationId: z.string(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
teamId: z.string().optional().nullable(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const IssueDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
assignee: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
|
||||
assigneeId: z.string().optional().nullable(),
|
||||
autoArchivedAt: z.coerce.date().optional().nullable(),
|
||||
autoClosedAt: z.coerce.date().optional().nullable(),
|
||||
boardOrder: z.number(),
|
||||
canceledAt: z.coerce.date().optional().nullable(),
|
||||
completedAt: z.coerce.date().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
creatorId: z.string().optional().nullable(),
|
||||
cycleId: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
dueDate: z.coerce.date().optional().nullable(), // timeless
|
||||
estimate: z.number().optional().nullable(),
|
||||
favoriteId: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
labelIds: z.array(z.string()),
|
||||
labels: z.array(IssueLabelDataSchema.pick({ id: true, color: true, name: true })),
|
||||
number: z.number(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
previousIdentifiers: z.array(z.string()),
|
||||
priority: z.number(),
|
||||
priorityLabel: z.string(),
|
||||
projectId: z.string().optional().nullable(),
|
||||
sortOrder: z.number(),
|
||||
state: z.object({ id: z.string(), color: z.string(), name: z.string(), type: z.string() }),
|
||||
startedAt: z.coerce.date().optional().nullable(),
|
||||
stateId: z.string(),
|
||||
subIssueSortOrder: z.number().optional().nullable(),
|
||||
subscriberIds: z.array(z.string()),
|
||||
team: z.object({ id: z.string(), key: z.string(), name: z.string() }),
|
||||
teamId: z.string(),
|
||||
title: z.string(),
|
||||
trashed: z.boolean().optional().nullable(),
|
||||
triagedAt: z.coerce.date().optional().nullable(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
/** **WARNING:** Still in alpha - use with caution! */
|
||||
const AttachmentDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
creatorId: z.string().optional().nullable(),
|
||||
groupBySource: z.boolean(),
|
||||
id: z.string(),
|
||||
issueId: z.string(),
|
||||
metadata: z.object({}).passthrough(), // JSONObject
|
||||
source: z
|
||||
.object({
|
||||
type: z.string().nullable(),
|
||||
imageUrl: z.string().url().nullable(),
|
||||
})
|
||||
.passthrough()
|
||||
.partial()
|
||||
.nullable(), // JSONObject
|
||||
sourceType: z.string().optional().nullable(),
|
||||
subtitle: z.string().optional().nullable(),
|
||||
title: z.string(),
|
||||
updatedAt: z.coerce.date(),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
const CommentDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
body: z.string(),
|
||||
botActorId: z.string().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
editedAt: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
issue: IssueDataSchema.pick({ id: true, title: true }),
|
||||
issueId: z.string(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
reactionData: z.array(z.object({}).passthrough()), // JSONObject
|
||||
updatedAt: z.coerce.date(),
|
||||
userId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
const ReactionDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
comment: CommentDataSchema.pick({
|
||||
id: true,
|
||||
body: true,
|
||||
userId: true,
|
||||
})
|
||||
.optional()
|
||||
.nullable(), // missing from official schema
|
||||
createdAt: z.coerce.date(),
|
||||
emoji: z.string(),
|
||||
id: z.string(),
|
||||
updatedAt: z.coerce.date(),
|
||||
user: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
|
||||
userId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
const MilestoneDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
description: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
projectId: z.string().optional().nullable(),
|
||||
sortOrder: z.number(),
|
||||
targetDate: z.coerce.date().optional().nullable(), // timeless
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const RoadmapDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
color: z.string().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
creatorId: z.string(),
|
||||
description: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
organizationId: z.string(),
|
||||
ownerId: z.string(),
|
||||
slugId: z.string(),
|
||||
sortOrder: z.number(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const ProjectDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
autoArchivedAt: z.coerce.date().optional().nullable(),
|
||||
canceledAt: z.coerce.date().optional().nullable(),
|
||||
color: z.string(),
|
||||
completedAt: z.coerce.date().optional().nullable(),
|
||||
completedIssueCountHistory: z.array(z.number()),
|
||||
completedScopeHistory: z.array(z.number()),
|
||||
content: z.string().optional().nullable(),
|
||||
convertedFromIssueId: z.string().optional().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
creatorId: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string().optional().nullable(),
|
||||
id: z.string(),
|
||||
inProgressScopeHistory: z.array(z.number()),
|
||||
integrationsSettingsId: z.string().optional().nullable(),
|
||||
issueCountHistory: z.array(z.number()),
|
||||
leadId: z.string(),
|
||||
memberIds: z.array(z.string()),
|
||||
milestones: z.array(MilestoneDataSchema.pick({ id: true, name: true })), // at projectMilestones key in official schema
|
||||
name: z.string(),
|
||||
progress: z.number().optional().nullable(), // missing, should be NonNullable
|
||||
projectUpdateRemindersPausedUntilAt: z.coerce.date().optional().nullable(),
|
||||
roadmaps: z
|
||||
.array(RoadmapDataSchema.pick({ id: true, name: true }))
|
||||
.optional()
|
||||
.nullable(), // missing from official schema
|
||||
scope: z.number().optional().nullable(), // missing, should be NonNullable
|
||||
scopeHistory: z.array(z.number()),
|
||||
slackIssueComments: z.boolean(),
|
||||
slackIssueStatuses: z.boolean(),
|
||||
slackNewIssue: z.boolean(),
|
||||
slugId: z.string(),
|
||||
sortOrder: z.number(),
|
||||
startDate: z.coerce.date().optional().nullable(), // timeless
|
||||
startedAt: z.coerce.date().optional().nullable(),
|
||||
state: z.string(),
|
||||
targetDate: z.coerce.date().optional().nullable(), // timeless
|
||||
teamIds: z.array(z.string()),
|
||||
trashed: z.boolean().optional().nullable(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const ProjectUpdateDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
body: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
// diff: z.any().optional().nullable(), // missing, "stringified" JSON but typed as Record
|
||||
editedAt: z.coerce.date().optional().nullable(),
|
||||
health: z.string(),
|
||||
id: z.string(),
|
||||
infoSnapshot: z.object({}).passthrough().optional().nullable(), // JSONObject, marked as "internal"
|
||||
project: ProjectDataSchema.pick({ id: true, name: true }),
|
||||
projectId: z.string(),
|
||||
roadmaps: z
|
||||
.array(RoadmapDataSchema.pick({ id: true, name: true }))
|
||||
.optional()
|
||||
.nullable(), // missing from official schema
|
||||
updatedAt: z.coerce.date(),
|
||||
user: z.object({ id: z.string(), name: z.string() }),
|
||||
userId: z.string(),
|
||||
});
|
||||
|
||||
const CycleDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
autoArchivedAt: z.coerce.date().optional().nullable(),
|
||||
completedAt: z.coerce.date().optional().nullable(),
|
||||
completedIssueCountHistory: z.array(z.number()),
|
||||
completedScopeHistory: z.array(z.number()),
|
||||
createdAt: z.coerce.date(),
|
||||
description: z.string().optional().nullable(),
|
||||
endsAt: z.coerce.date(),
|
||||
id: z.string(),
|
||||
inProgressScopeHistory: z.array(z.number()),
|
||||
issueCountHistory: z.array(z.number()),
|
||||
name: z.string().optional().nullable(),
|
||||
number: z.number(),
|
||||
progress: z.number().optional().nullable(), // missing, should be NonNullable
|
||||
scopeHistory: z.array(z.number()),
|
||||
startsAt: z.coerce.date(),
|
||||
teamId: z.string(),
|
||||
uncompletedIssuesUponCloseIds: z.array(z.string()),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
export const WebhookPayloadBaseSchema = z.object({
|
||||
createdAt: z.coerce.date(),
|
||||
organizationId: z.string().optional().nullable(), // missing from official schema - workspace id?
|
||||
url: z.string().url().optional().nullable(),
|
||||
webhookId: z.string(),
|
||||
webhookTimestamp: z.coerce.date(),
|
||||
});
|
||||
|
||||
const CREATE = z.literal("create");
|
||||
const REMOVE = z.literal("remove");
|
||||
const UPDATE = z.literal("update");
|
||||
|
||||
/** **WARNING:** Still in alpha - use with caution! */
|
||||
export const AttachmentEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Attachment"),
|
||||
data: AttachmentDataSchema,
|
||||
});
|
||||
export const AttachmentEventSchema = z.discriminatedUnion("action", [
|
||||
AttachmentEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
AttachmentEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
AttachmentEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: AttachmentDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type AttachmentEvent = z.infer<typeof AttachmentEventSchema>;
|
||||
|
||||
export const CommentEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Comment"),
|
||||
data: CommentDataSchema,
|
||||
});
|
||||
export const CommentEventSchema = z.discriminatedUnion("action", [
|
||||
CommentEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
CommentEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
CommentEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: CommentDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type CommentEvent = z.infer<typeof CommentEventSchema>;
|
||||
|
||||
export const CycleEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Cycle"),
|
||||
data: CycleDataSchema,
|
||||
});
|
||||
export const CycleEventSchema = z.discriminatedUnion("action", [
|
||||
CycleEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
CycleEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
CycleEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: CycleDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type CycleEvent = z.infer<typeof CycleEventSchema>;
|
||||
|
||||
export const IssueEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Issue"),
|
||||
data: IssueDataSchema,
|
||||
});
|
||||
export const IssueEventSchema = z.discriminatedUnion("action", [
|
||||
IssueEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
IssueEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
IssueEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: IssueDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type IssueEvent = z.infer<typeof IssueEventSchema>;
|
||||
|
||||
export const IssueLabelEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("IssueLabel"),
|
||||
data: IssueLabelDataSchema,
|
||||
});
|
||||
export const IssueLabelEventSchema = z.discriminatedUnion("action", [
|
||||
IssueLabelEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
IssueLabelEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
IssueLabelEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: IssueLabelDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type IssueLabelEvent = z.infer<typeof IssueLabelEventSchema>;
|
||||
|
||||
// TODO: confirm this with real-world payload(s)
|
||||
export const IssueSLAEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("IssueSLA"),
|
||||
issueData: IssueDataSchema,
|
||||
});
|
||||
export const IssueSLAEventSchema = z.discriminatedUnion("action", [
|
||||
IssueSLAEventBaseSchema.extend({
|
||||
action: z.literal("set"),
|
||||
}),
|
||||
IssueSLAEventBaseSchema.extend({
|
||||
action: z.literal("highRisk"),
|
||||
}),
|
||||
IssueSLAEventBaseSchema.extend({
|
||||
action: z.literal("breached"),
|
||||
}),
|
||||
]);
|
||||
export type IssueSLAEvent = z.infer<typeof IssueSLAEventSchema>;
|
||||
export type IssueSLAEventBreached = Extract<IssueSLAEvent, { action: "breached" }>;
|
||||
|
||||
export const ProjectEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Project"),
|
||||
data: ProjectDataSchema,
|
||||
});
|
||||
export const ProjectEventSchema = z.discriminatedUnion("action", [
|
||||
ProjectEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
ProjectEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
ProjectEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: ProjectDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type ProjectEvent = z.infer<typeof ProjectEventSchema>;
|
||||
|
||||
export const ProjectUpdateEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("ProjectUpdate"),
|
||||
data: ProjectUpdateDataSchema,
|
||||
});
|
||||
export const ProjectUpdateEventSchema = z.discriminatedUnion("action", [
|
||||
ProjectUpdateEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
ProjectUpdateEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
ProjectUpdateEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: ProjectUpdateDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type ProjectUpdateEvent = z.infer<typeof ProjectUpdateEventSchema>;
|
||||
|
||||
export const ReactionEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Reaction"),
|
||||
data: ReactionDataSchema,
|
||||
});
|
||||
export const ReactionEventSchema = z.discriminatedUnion("action", [
|
||||
ReactionEventBaseSchema.extend({
|
||||
action: CREATE,
|
||||
}),
|
||||
ReactionEventBaseSchema.extend({
|
||||
action: REMOVE,
|
||||
}),
|
||||
ReactionEventBaseSchema.extend({
|
||||
action: UPDATE,
|
||||
updatedFrom: ReactionDataSchema.partial(),
|
||||
}),
|
||||
]);
|
||||
export type ReactionEvent = z.infer<typeof ReactionEventSchema>;
|
||||
|
||||
export const WebhookPayloadSchema = z.union([
|
||||
AttachmentEventSchema,
|
||||
CommentEventSchema,
|
||||
CycleEventSchema,
|
||||
IssueEventSchema,
|
||||
IssueLabelEventSchema,
|
||||
IssueSLAEventSchema,
|
||||
ProjectEventSchema,
|
||||
ProjectUpdateEventSchema,
|
||||
ReactionEventSchema,
|
||||
]);
|
||||
|
||||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Request } from "@linear/sdk";
|
||||
import { WebhookActionType, WebhookPayload } from "./schemas";
|
||||
|
||||
export type GetLinearPayload<
|
||||
TPayload extends WebhookPayload,
|
||||
TAction extends any = any,
|
||||
> = TAction extends WebhookActionType ? Extract<TPayload, { action: TAction }> : TPayload;
|
||||
|
||||
type FunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends Function ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type SerializedLinearOutput<T> = T extends object
|
||||
? T extends Array<infer U>
|
||||
? Array<SerializedLinearOutput<U>>
|
||||
: { [K in keyof T as Exclude<K, FunctionKeys<T> | `_${string}`>]: SerializedLinearOutput<T[K]> }
|
||||
: T;
|
||||
|
||||
export type LinearReturnType<
|
||||
TPayload extends Omit<Request, "_request">,
|
||||
K extends unknown = unknown,
|
||||
> = Promise<
|
||||
Awaited<SerializedLinearOutput<Awaited<K extends keyof TPayload ? TPayload[K] : TPayload>>>
|
||||
>;
|
||||
|
||||
export type AwaitNested<T extends object, K extends keyof T> = Omit<T, K> & {
|
||||
[key in K]: Awaited<T[K]>;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CommentEvent, IssueEvent, WebhookPayload } from "./schemas";
|
||||
import { GetLinearPayload } from "./types";
|
||||
import { LinearDocument as L } from "@linear/sdk";
|
||||
|
||||
export type QueryVariables = {
|
||||
after: string;
|
||||
before: string;
|
||||
first: number;
|
||||
includeArchived: boolean;
|
||||
last: number;
|
||||
orderBy: L.PaginationOrderBy;
|
||||
};
|
||||
|
||||
export type Nullable<T> = Partial<{
|
||||
[K in keyof T]: T[K] | null;
|
||||
}>;
|
||||
|
||||
export const onCommentProperties = (payload: GetLinearPayload<CommentEvent>) => {
|
||||
return [
|
||||
{ label: "Comment ID", text: payload.data.id },
|
||||
{ label: "Issue ID", text: payload.data.issueId },
|
||||
{ label: "Issue Title", text: payload.data.issue.title, url: payload.url ?? undefined },
|
||||
];
|
||||
};
|
||||
|
||||
export const onIssueProperties = (payload: GetLinearPayload<IssueEvent>) => {
|
||||
return [
|
||||
{ label: "Issue ID", text: payload.data.id },
|
||||
{
|
||||
label: "Issue",
|
||||
text: `[${payload.data.team.key}-${payload.data.number}] ${payload.data.title}`,
|
||||
url: payload.url ?? undefined,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const queryProperties = (query: Nullable<QueryVariables>) => {
|
||||
return [
|
||||
...(query.after ? [{ label: "After", text: query.after }] : []),
|
||||
...(query.before ? [{ label: "Before", text: query.before }] : []),
|
||||
...(query.first ? [{ label: "First", text: String(query.first) }] : []),
|
||||
...(query.last ? [{ label: "Last", text: String(query.last) }] : []),
|
||||
...(query.orderBy ? [{ label: "Order by", text: query.orderBy }] : []),
|
||||
...(query.includeArchived
|
||||
? [{ label: "Include archived", text: String(query.includeArchived) }]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
export const updatedFromProperties = (payload: WebhookPayload) => {
|
||||
if (payload.action !== "update") return [];
|
||||
return [
|
||||
{
|
||||
label: "Updated Keys",
|
||||
text: Object.keys(payload.updatedFrom)
|
||||
.filter((key) => !["editedAt", "updatedAt"].includes(key))
|
||||
.join(", "),
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
EventFilter,
|
||||
ExternalSource,
|
||||
ExternalSourceTrigger,
|
||||
HandlerEvent,
|
||||
IntegrationTaskKey,
|
||||
Logger,
|
||||
} from "@trigger.dev/sdk";
|
||||
import {
|
||||
LinearDocument as L,
|
||||
LinearWebhooks,
|
||||
LINEAR_WEBHOOK_SIGNATURE_HEADER,
|
||||
LINEAR_WEBHOOK_TS_FIELD,
|
||||
WebhookPayload,
|
||||
DeletePayload,
|
||||
Webhook,
|
||||
} from "@linear/sdk";
|
||||
import { z } from "zod";
|
||||
import * as events from "./events";
|
||||
import { Linear, LinearRunTask, serializeLinearOutput } from "./index";
|
||||
import { WebhookPayloadSchema } from "./schemas";
|
||||
import { LinearReturnType } from "./types";
|
||||
import { queryProperties } from "./utils";
|
||||
|
||||
export class Webhooks {
|
||||
runTask: LinearRunTask;
|
||||
|
||||
constructor(runTask: LinearRunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
webhook(key: IntegrationTaskKey, params: { id: string }): LinearReturnType<Webhook> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
return serializeLinearOutput(await client.webhook(params.id));
|
||||
},
|
||||
{
|
||||
name: "Get Webhook",
|
||||
params,
|
||||
properties: [{ label: "Webhook ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
webhooks(key: IntegrationTaskKey, params?: L.WebhooksQueryVariables): LinearReturnType<Webhook[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
let connections = await client.webhooks(params);
|
||||
const hooks = connections.nodes;
|
||||
while (connections.pageInfo.hasNextPage) {
|
||||
connections = await connections.fetchNext();
|
||||
hooks.push(...connections.nodes);
|
||||
}
|
||||
return serializeLinearOutput(hooks);
|
||||
},
|
||||
{
|
||||
name: "List Webhooks",
|
||||
params,
|
||||
properties: queryProperties(params ?? {}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: L.WebhookCreateInput
|
||||
): LinearReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
const payload = await client.createWebhook({ ...params, allPublicTeams: !params.teamId });
|
||||
return serializeLinearOutput({
|
||||
...payload,
|
||||
webhook: await payload.webhook,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Create Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Webhook URL", text: params.url },
|
||||
{ label: "Resource Types", text: params.resourceTypes.join(", ") },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteWebhook(key: IntegrationTaskKey, params: { id: string }): LinearReturnType<DeletePayload> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
return serializeLinearOutput(await client.deleteWebhook(params.id));
|
||||
},
|
||||
{
|
||||
name: "Delete Webhook",
|
||||
params,
|
||||
properties: [{ label: "Webhook ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: { id: string; input: L.WebhookUpdateInput }
|
||||
): LinearReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const payload = await client.updateWebhook(params.id, params.input);
|
||||
return serializeLinearOutput({
|
||||
...payload,
|
||||
webhook: await payload.webhook,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Update Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Webhook ID", text: params.id },
|
||||
...(params.input.url ? [{ label: "Webhook URL", text: params.input.url }] : []),
|
||||
...(params.input.resourceTypes
|
||||
? [{ label: "Resource Types", text: params.input.resourceTypes.join(", ") }]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type LinearEvents = (typeof events)[keyof typeof events];
|
||||
|
||||
export type TriggerParams = {
|
||||
teamId?: string;
|
||||
filter?: EventFilter;
|
||||
};
|
||||
|
||||
type CreateTriggersResult<TEventSpecification extends LinearEvents> = ExternalSourceTrigger<
|
||||
TEventSpecification,
|
||||
ReturnType<typeof createWebhookEventSource>
|
||||
>;
|
||||
|
||||
export function createTrigger<TEventSpecification extends LinearEvents>(
|
||||
source: ReturnType<typeof createWebhookEventSource>,
|
||||
event: TEventSpecification,
|
||||
params: TriggerParams
|
||||
): CreateTriggersResult<TEventSpecification> {
|
||||
return new ExternalSourceTrigger({
|
||||
event,
|
||||
params,
|
||||
source,
|
||||
options: {},
|
||||
});
|
||||
}
|
||||
|
||||
const WebhookRegistrationDataSchema = z.object({
|
||||
success: z.literal(true),
|
||||
webhook: z.object({
|
||||
id: z.string(),
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export function createWebhookEventSource(
|
||||
integration: Linear
|
||||
): ExternalSource<Linear, TriggerParams, "HTTP", {}> {
|
||||
return new ExternalSource("HTTP", {
|
||||
id: "linear.webhook",
|
||||
schema: z.object({
|
||||
teamId: z.string().optional(),
|
||||
}),
|
||||
version: "0.1.0",
|
||||
integration,
|
||||
key: (params) => `${params.teamId ? params.teamId : "all"}`,
|
||||
handler: webhookHandler,
|
||||
register: async (event, io, ctx) => {
|
||||
const { params, source: httpSource, options } = event;
|
||||
|
||||
// (key-specific) stored data, undefined if not registered yet
|
||||
const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data);
|
||||
|
||||
// set of events to register
|
||||
const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing]));
|
||||
const registeredOptions = {
|
||||
event: allEvents,
|
||||
};
|
||||
|
||||
// easily identify webhooks on linear
|
||||
const label = `trigger.${params.teamId ? params.teamId : "all"}`;
|
||||
|
||||
if (httpSource.active && webhookData.success) {
|
||||
const hasMissingOptions = Object.values(options).some(
|
||||
(option) => option.missing.length > 0
|
||||
);
|
||||
if (!hasMissingOptions) return;
|
||||
|
||||
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
|
||||
id: webhookData.data.webhook.id,
|
||||
input: {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
url: httpSource.url,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// check for existing hooks that match url
|
||||
const listResponse = await io.integration.webhooks("list-webhooks");
|
||||
const existingWebhook = listResponse.find((w) => w.url === httpSource.url);
|
||||
|
||||
if (existingWebhook) {
|
||||
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
|
||||
id: existingWebhook.id,
|
||||
input: {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
url: httpSource.url,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const createPayload = await io.integration.createWebhook("create-webhook", {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
teamId: params.teamId,
|
||||
url: httpSource.url,
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(createPayload),
|
||||
secret: (await createPayload.webhook)?.secret,
|
||||
options: registeredOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: Linear) {
|
||||
logger.debug("[@trigger.dev/linear] Handling webhook payload");
|
||||
|
||||
const { rawEvent: request, source } = event;
|
||||
|
||||
const LINEAR_IPS = ["35.231.147.226", "35.243.134.228"];
|
||||
|
||||
const clientIp =
|
||||
request.headers.get("cf-connecting-ip") ??
|
||||
(
|
||||
request.headers.get("x-real-ip") ??
|
||||
request.headers.get("x-forwarded-for") ??
|
||||
// default to allowing request if expected headers missing
|
||||
LINEAR_IPS[0]
|
||||
).split(",")[0];
|
||||
|
||||
if (!LINEAR_IPS.includes(clientIp)) {
|
||||
logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid.");
|
||||
throw Error("[@trigger.dev/linear] Invalid source IP.");
|
||||
}
|
||||
|
||||
const payloadUuid = request.headers.get("Linear-Delivery");
|
||||
const payloadEvent = request.headers.get("Linear-Event");
|
||||
|
||||
if (!payloadUuid || !payloadEvent) {
|
||||
logger.debug("[@trigger.dev/linear] Missing required Linear headers");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
logger.debug("[@trigger.dev/linear] No body found");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
const signature = request.headers.get(LINEAR_WEBHOOK_SIGNATURE_HEADER);
|
||||
|
||||
if (!signature) {
|
||||
logger.error("[@trigger.dev/linear] Error validating webhook signature, no signature found");
|
||||
throw Error("[@trigger.dev/linear] No signature found");
|
||||
}
|
||||
|
||||
const rawBody = await request.text();
|
||||
const body = JSON.parse(rawBody);
|
||||
const webhookHelper = new LinearWebhooks(source.secret);
|
||||
|
||||
if (!webhookHelper.verify(Buffer.from(rawBody), signature, body[LINEAR_WEBHOOK_TS_FIELD])) {
|
||||
logger.error("[@trigger.dev/linear] Error validating webhook signature, they don't match");
|
||||
throw Error("[@trigger.dev/linear] Invalid signature");
|
||||
}
|
||||
|
||||
const webhookPayload = WebhookPayloadSchema.parse(body);
|
||||
|
||||
return {
|
||||
events: [
|
||||
{
|
||||
id: payloadUuid,
|
||||
name: payloadEvent,
|
||||
source: "linear.app",
|
||||
payload: webhookPayload,
|
||||
context: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSources": false,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node16",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": [
|
||||
"es2019"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"target": "es2021"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"tsup.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
name: "main",
|
||||
entry: ["./src/index.ts"],
|
||||
outDir: "./dist",
|
||||
platform: "node",
|
||||
format: ["cjs"],
|
||||
legacyOutput: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
dts: true,
|
||||
treeshake: {
|
||||
preset: "smallest",
|
||||
},
|
||||
esbuildPlugins: [],
|
||||
external: ["http", "https", "util", "events", "tty", "os", "timers"],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -57,7 +57,11 @@ export type RunTaskErrorCallback = (
|
||||
error: unknown,
|
||||
task: IOTask,
|
||||
io: IO
|
||||
) => { retryAt: Date; error?: Error; jitter?: number } | Error | undefined | void;
|
||||
) =>
|
||||
| { retryAt?: Date; error?: Error; jitter?: number; skipRetrying?: boolean }
|
||||
| Error
|
||||
| undefined
|
||||
| void;
|
||||
|
||||
export class IO {
|
||||
private _id: string;
|
||||
@@ -627,6 +631,8 @@ export class IO {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let skipRetrying = false;
|
||||
|
||||
if (onError) {
|
||||
try {
|
||||
const onErrorResult = onError(error, task, this);
|
||||
@@ -635,13 +641,17 @@ export class IO {
|
||||
if (onErrorResult instanceof Error) {
|
||||
error = onErrorResult;
|
||||
} else {
|
||||
const parsedError = ErrorWithStackSchema.safeParse(onErrorResult.error);
|
||||
skipRetrying = !!onErrorResult.skipRetrying;
|
||||
|
||||
throw new RetryWithTaskError(
|
||||
parsedError.success ? parsedError.data : { message: "Unknown error" },
|
||||
task,
|
||||
onErrorResult.retryAt
|
||||
);
|
||||
if (onErrorResult.retryAt && !skipRetrying) {
|
||||
const parsedError = ErrorWithStackSchema.safeParse(onErrorResult.error);
|
||||
|
||||
throw new RetryWithTaskError(
|
||||
parsedError.success ? parsedError.data : { message: "Unknown error" },
|
||||
task,
|
||||
onErrorResult.retryAt
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (innerError) {
|
||||
@@ -655,7 +665,7 @@ export class IO {
|
||||
|
||||
const parsedError = ErrorWithStackSchema.safeParse(error);
|
||||
|
||||
if (options?.retry) {
|
||||
if (options?.retry && !skipRetrying) {
|
||||
const retryAt = calculateRetryAt(options.retry, task.attempts - 1);
|
||||
|
||||
if (retryAt) {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"dynamic-schedule": "nodemon --watch src/dynamic-schedule.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-schedule.ts",
|
||||
"dynamic-triggers": "nodemon --watch src/dynamic-triggers.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-triggers.ts",
|
||||
"background-fetch": "nodemon --watch src/background-fetch.ts -r tsconfig-paths/register -r dotenv/config src/background-fetch.ts",
|
||||
"linear": "nodemon --watch src/linear.ts -r tsconfig-paths/register -r dotenv/config src/linear.ts",
|
||||
"status": "nodemon --watch src/status.ts -r tsconfig-paths/register -r dotenv/config src/status.ts",
|
||||
"dev:trigger": "trigger-cli dev --port 8080"
|
||||
},
|
||||
@@ -39,7 +40,8 @@
|
||||
"@types/node": "20.4.2",
|
||||
"typescript": "5.1.6",
|
||||
"zod": "3.21.4",
|
||||
"@trigger.dev/airtable": "workspace:*"
|
||||
"@trigger.dev/airtable": "workspace:*",
|
||||
"@trigger.dev/linear": "workspace:*"
|
||||
},
|
||||
"trigger.dev": {
|
||||
"endpointId": "job-catalog"
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { createExpressServer } from "@trigger.dev/express";
|
||||
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
|
||||
import { Linear, PaginationOrderBy, serializeLinearOutput } from "@trigger.dev/linear";
|
||||
import { z } from "zod";
|
||||
|
||||
export const client = new TriggerClient({
|
||||
id: "job-catalog",
|
||||
apiKey: process.env["TRIGGER_API_KEY"],
|
||||
apiUrl: process.env["TRIGGER_API_URL"],
|
||||
verbose: false,
|
||||
ioLogLocalEnabled: true,
|
||||
});
|
||||
|
||||
const linear = new Linear({
|
||||
id: "linear",
|
||||
apiKey: process.env["LINEAR_API_KEY"],
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "linear-create-issue",
|
||||
name: "Linear Create Issue",
|
||||
version: "0.1.0",
|
||||
integrations: { linear },
|
||||
trigger: eventTrigger({
|
||||
name: "linear.create.issue",
|
||||
schema: z.object({
|
||||
teamId: z.string().optional(),
|
||||
issueTitle: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
run: async (payload, io, ctx) => {
|
||||
const firstTeam = await io.linear.runTask("get-first-team", async (client) => {
|
||||
const payload = await client.teams();
|
||||
//use helper to serialize raw client output
|
||||
return serializeLinearOutput(payload.nodes[0]);
|
||||
});
|
||||
|
||||
const issue = await io.linear.createIssue("create-issue", {
|
||||
//use optional teamId if passed - ID of first team otherwise
|
||||
teamId: payload.teamId ?? firstTeam.id,
|
||||
title: payload.issueTitle ?? "Shiny new issue",
|
||||
});
|
||||
|
||||
if (issue) {
|
||||
//some time to visually inspect and trigger the next job
|
||||
await io.wait("10 secs", 10);
|
||||
await io.linear.deleteIssue("delete-issue", { id: issue.id });
|
||||
} else {
|
||||
io.logger.error("Failed to create issue, nothing to delete.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "linear-new-issue-reply",
|
||||
name: "Linear New Issue Reply",
|
||||
version: "0.1.0",
|
||||
integrations: {
|
||||
linear,
|
||||
},
|
||||
//this should trigger when creating an issue in the job above
|
||||
trigger: linear.onIssueCreated(),
|
||||
run: async (payload, io, ctx) => {
|
||||
const newIssueId = payload.data.id;
|
||||
|
||||
await io.linear.createComment("create-comment", {
|
||||
issueId: newIssueId,
|
||||
body: "Thank's for opening this issue!",
|
||||
});
|
||||
|
||||
await io.linear.createReaction("create-reaction", {
|
||||
issueId: newIssueId,
|
||||
emoji: "+1",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
client.defineJob({
|
||||
id: "linear-test-misc",
|
||||
name: "Linear Test Misc",
|
||||
version: "0.1.0",
|
||||
integrations: { linear },
|
||||
//this should automatically trigger after the first example
|
||||
trigger: linear.onIssueRemoved(),
|
||||
run: async (payload, io, ctx) => {
|
||||
//info about the currently authenticated user
|
||||
await io.linear.viewer("get-viewer");
|
||||
await io.linear.organization("get-org");
|
||||
|
||||
//create a team
|
||||
const newTeam = await io.linear.createTeam("create-team", {
|
||||
name: `Rickastleydotdev-${Math.floor(Math.random() * 1000)}`,
|
||||
});
|
||||
|
||||
//get some entities
|
||||
const comments = await io.linear.comments("get-comments", { first: 2 });
|
||||
const issues = await io.linear.issues("get-issues", { first: 10 });
|
||||
const projects = await io.linear.projects("get-projects", { first: 5 });
|
||||
|
||||
return {
|
||||
deletedIssueId: payload.data.id,
|
||||
newTeamKey: newTeam?.key,
|
||||
comments: comments.nodes.length,
|
||||
issues: issues.nodes.length,
|
||||
projects: projects.nodes.length,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
createExpressServer(client);
|
||||
@@ -96,6 +96,12 @@
|
||||
],
|
||||
"@trigger.dev/airtable/*": [
|
||||
"../../integrations/airtable/src/*"
|
||||
],
|
||||
"@trigger.dev/linear": [
|
||||
"../../integrations/linear/src/index"
|
||||
],
|
||||
"@trigger.dev/linear/*": [
|
||||
"../../integrations/linear/src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user