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:
nicktrn
2023-09-21 23:14:54 +01:00
committed by GitHub
parent 91fc1e80f3
commit 15f17d27e0
54 changed files with 5433 additions and 9 deletions
+6
View File
@@ -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.
+2
View File
@@ -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)],
},
},
},
};
+479
View File
@@ -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. |
+1
View File
@@ -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 | 🕘 | ✅ |
+1
View File
@@ -244,6 +244,7 @@
"integrations/apis/github-tasks"
]
},
"integrations/apis/linear",
"integrations/apis/openai",
"integrations/apis/plain",
"integrations/apis/resend",
+3
View File
@@ -0,0 +1,3 @@
# @trigger.dev/linear
+36
View File
@@ -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"
}
}
+535
View File
@@ -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,
};
+444
View File
@@ -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>;
+28
View File
@@ -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]>;
};
+60
View File
@@ -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(", "),
},
];
};
+315
View File
@@ -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: {},
},
],
};
}
+33
View File
@@ -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"
]
}
+24
View File
@@ -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"],
},
]);
+18 -8
View File
@@ -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) {
+3 -1
View File
@@ -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"
+162
View File
@@ -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);
+6
View File
@@ -96,6 +96,12 @@
],
"@trigger.dev/airtable/*": [
"../../integrations/airtable/src/*"
],
"@trigger.dev/linear": [
"../../integrations/linear/src/index"
],
"@trigger.dev/linear/*": [
"../../integrations/linear/src/*"
]
}
}