@pipedream/linear_app 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # Overview
2
2
 
3
- With the Linear API, you can manage your workflow, track issues, and automate
4
- your development process. Here are some examples of what you can build:
3
+ Linear helps streamline software project management, bug tracking, and task coordination. By using the Linear (API key) API with Pipedream, you can automate routine tasks, sync issues across platforms, and trigger custom workflows. Think auto-assignment of tasks based on specific triggers or pushing updates to a Slack channel when an issue's status changes. These automations save time and ensure that your development team stays focused on coding rather than on administrative overhead.
5
4
 
6
- - A custom workflow management tool
7
- - A tool to track issues and feature requests
8
- - A development process automation tool
5
+ # Example Use Cases
6
+
7
+ - **Sync Issues with Google Sheets**: Use Pipedream to monitor new issues in Linear and automatically add them to a Google Sheets spreadsheet. This can help with reporting, auditing, and providing a high-level overview of tasks without needing to access Linear directly.
8
+
9
+ - **Automate Task Assignment Based on Labels**: When a new issue is created in Linear with a specific label (e.g., "urgent"), you can set up a Pipedream workflow to automatically assign it to a designated team member or escalate it by creating a high-priority notification in your team's messaging app, like Slack or Microsoft Teams.
10
+
11
+ - **Create GitHub Issues from Linear Tasks**: For development teams using both Linear and GitHub, a workflow can be set up to create a new GitHub issue whenever a Linear task reaches a certain stage or is tagged for development. This ensures that your code repository is always in sync with your project management tool.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipedream/linear_app",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Pipedream Linear_app Components",
5
5
  "main": "linear_app.app.mjs",
6
6
  "keywords": [
@@ -15,6 +15,6 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@linear/sdk": "^13.0.0",
18
- "@pipedream/platform": "^1.3.0"
18
+ "@pipedream/platform": "^3.0.3"
19
19
  }
20
20
  }
@@ -5,9 +5,9 @@ export default {
5
5
  ...common,
6
6
  key: "linear_app-comment-created-instant",
7
7
  name: "New Created Comment (Instant)",
8
- description: "Emit new event when a new comment is created. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)",
8
+ description: "Emit new event when a new comment is created. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)",
9
9
  type: "source",
10
- version: "0.1.5",
10
+ version: "0.1.7",
11
11
  dedupe: "unique",
12
12
  methods: {
13
13
  ...common.methods,
@@ -50,6 +50,11 @@ export default {
50
50
  },
51
51
  };
52
52
  },
53
+ getResource(comment) {
54
+ return this.linearApp.getComment({
55
+ commentId: comment.id,
56
+ });
57
+ },
53
58
  getMetadata(resource) {
54
59
  const {
55
60
  delivery,
@@ -1,6 +1,7 @@
1
1
  import linearApp from "../../linear_app.app.mjs";
2
2
  import constants from "../../common/constants.mjs";
3
3
  import utils from "../../common/utils.mjs";
4
+ import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform";
4
5
 
5
6
  export default {
6
7
  props: {
@@ -12,6 +13,7 @@ export default {
12
13
  linearApp,
13
14
  "teamId",
14
15
  ],
16
+ reloadProps: true,
15
17
  },
16
18
  projectId: {
17
19
  propDefinition: [
@@ -19,9 +21,30 @@ export default {
19
21
  "projectId",
20
22
  ],
21
23
  },
22
- http: "$.interface.http",
23
24
  db: "$.service.db",
24
25
  },
26
+ async additionalProps() {
27
+ const props = {};
28
+ let msg;
29
+ if (await this.isAdmin()) {
30
+ msg = "Admin role detected. Trigger will be set up as a webhook.";
31
+ props.http = "$.interface.http";
32
+ } else {
33
+ msg = "No admin role detected. Trigger will set up to use polling.";
34
+ props.timer = {
35
+ type: "$.interface.timer",
36
+ default: {
37
+ intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL,
38
+ },
39
+ };
40
+ }
41
+ props.alert = {
42
+ type: "alert",
43
+ alertType: "info",
44
+ content: `${msg} See the Linear [documentation](https://linear.app/docs/api-and-webhooks#webhooks) for details.`,
45
+ };
46
+ return props;
47
+ },
25
48
  methods: {
26
49
  setWebhookId(teamId, id) {
27
50
  this.db.set(`webhook-${teamId}`, id);
@@ -41,6 +64,9 @@ export default {
41
64
  useGraphQl() {
42
65
  return true;
43
66
  },
67
+ getResource() {
68
+ throw new Error("getResource is not implemented");
69
+ },
44
70
  getResourceTypes() {
45
71
  throw new Error("getResourceTypes is not implemented");
46
72
  },
@@ -59,11 +85,20 @@ export default {
59
85
  getLoadedProjectId() {
60
86
  throw new Error("Get loaded project ID not implemented");
61
87
  },
62
- },
63
- hooks: {
64
- async deploy() {
65
- // Retrieve historical events
66
- console.log("Retrieving historical events...");
88
+ async isAdmin() {
89
+ const { data } = await this.linearApp.makeAxiosRequest({
90
+ method: "POST",
91
+ data: {
92
+ "query": `{
93
+ user(id: "me") {
94
+ admin
95
+ }
96
+ }`,
97
+ },
98
+ });
99
+ return data?.user?.admin;
100
+ },
101
+ async emitPolledResources() {
67
102
  const stream = this.linearApp.paginateResources({
68
103
  resourcesFn: this.getResourcesFn(),
69
104
  resourcesFnArgs: this.getResourcesFnArgs(),
@@ -77,73 +112,89 @@ export default {
77
112
  this.$emit(resource, this.getMetadata(resource));
78
113
  });
79
114
  },
115
+ },
116
+ hooks: {
117
+ async deploy() {
118
+ // Retrieve historical events
119
+ console.log("Retrieving historical events...");
120
+ await this.emitPolledResources();
121
+ },
80
122
  async activate() {
81
- const args = {
82
- resourceTypes: this.getResourceTypes(),
83
- url: this.http.endpoint,
84
- label: this.getWebhookLabel(),
85
- };
86
- if (!this.teamIds && !this.teamId) {
87
- args.allPublicTeams = true;
88
- const { _webhook: webhook } = await this.linearApp.createWebhook(args);
89
- this.setWebhookId("1", webhook.id);
90
- return;
91
- }
92
- const teamIds = this.teamIds || [
93
- this.teamId,
94
- ];
95
- for (const teamId of teamIds) {
96
- const { _webhook: webhook } =
97
- await this.linearApp.createWebhook({
98
- teamId,
99
- ...args,
100
- });
101
- this.setWebhookId(teamId, webhook.id);
123
+ if (await this.isAdmin()) {
124
+ const args = {
125
+ resourceTypes: this.getResourceTypes(),
126
+ url: this.http.endpoint,
127
+ label: this.getWebhookLabel(),
128
+ };
129
+ if (!this.teamIds && !this.teamId) {
130
+ args.allPublicTeams = true;
131
+ const { _webhook: webhook } = await this.linearApp.createWebhook(args);
132
+ this.setWebhookId("1", webhook.id);
133
+ return;
134
+ }
135
+ const teamIds = this.teamIds || [
136
+ this.teamId,
137
+ ];
138
+ for (const teamId of teamIds) {
139
+ const { _webhook: webhook } =
140
+ await this.linearApp.createWebhook({
141
+ teamId,
142
+ ...args,
143
+ });
144
+ this.setWebhookId(teamId, webhook.id);
145
+ }
102
146
  }
103
147
  },
104
148
  async deactivate() {
105
- if (!this.teamIds && !this.teamId) {
106
- const webhookId = this.getWebhookId("1");
107
- if (webhookId) {
108
- await this.linearApp.deleteWebhook(webhookId);
149
+ if (await this.isAdmin()) {
150
+ if (!this.teamIds && !this.teamId) {
151
+ const webhookId = this.getWebhookId("1");
152
+ if (webhookId) {
153
+ await this.linearApp.deleteWebhook(webhookId);
154
+ }
155
+ return;
109
156
  }
110
- return;
111
- }
112
- const teamIds = this.teamIds || [
113
- this.teamId,
114
- ];
115
- for (const teamId of teamIds) {
116
- const webhookId = this.getWebhookId(teamId);
117
- if (webhookId) {
118
- await this.linearApp.deleteWebhook(webhookId);
157
+ const teamIds = this.teamIds || [
158
+ this.teamId,
159
+ ];
160
+ for (const teamId of teamIds) {
161
+ const webhookId = this.getWebhookId(teamId);
162
+ if (webhookId) {
163
+ await this.linearApp.deleteWebhook(webhookId);
164
+ }
119
165
  }
120
166
  }
121
167
  },
122
168
  },
123
169
  async run(event) {
124
- const {
125
- client_ip: clientIp,
126
- body,
127
- headers,
128
- } = event;
170
+ if (!(await this.isAdmin())) {
171
+ await this.emitPolledResources();
172
+ } else {
173
+ const {
174
+ client_ip: clientIp,
175
+ body,
176
+ headers,
177
+ } = event;
129
178
 
130
- const { [constants.LINEAR_DELIVERY_HEADER]: delivery } = headers;
179
+ const { [constants.LINEAR_DELIVERY_HEADER]: delivery } = headers;
131
180
 
132
- const resource = {
133
- ...body,
134
- delivery,
135
- };
181
+ const resource = {
182
+ ...body,
183
+ delivery,
184
+ };
136
185
 
137
- if (!this.isWebhookValid(clientIp)) {
138
- console.log("Webhook is not valid");
139
- return;
140
- }
186
+ if (!this.isWebhookValid(clientIp)) {
187
+ console.log("Webhook is not valid");
188
+ return;
189
+ }
141
190
 
142
- if (!(await this.isFromProject(body)) || !this.isRelevant(body)) {
143
- return;
144
- }
191
+ if (!(await this.isFromProject(body)) || !this.isRelevant(body)) {
192
+ return;
193
+ }
145
194
 
146
- const meta = this.getMetadata(resource);
147
- this.$emit(body, meta);
195
+ const meta = this.getMetadata(resource);
196
+ const item = await this.getResource(body.data);
197
+ this.$emit(item, meta);
198
+ }
148
199
  },
149
200
  };
@@ -5,9 +5,9 @@ export default {
5
5
  ...common,
6
6
  key: "linear_app-issue-created-instant",
7
7
  name: "New Created Issue (Instant)",
8
- description: "Emit new event when a new issue is created. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)",
8
+ description: "Emit new event when a new issue is created. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)",
9
9
  type: "source",
10
- version: "0.3.5",
10
+ version: "0.3.7",
11
11
  dedupe: "unique",
12
12
  methods: {
13
13
  ...common.methods,
@@ -42,6 +42,11 @@ export default {
42
42
  isRelevant(body) {
43
43
  return body?.action === "create";
44
44
  },
45
+ getResource(issue) {
46
+ return this.linearApp.getIssue({
47
+ issueId: issue.id,
48
+ });
49
+ },
45
50
  getMetadata(resource) {
46
51
  const {
47
52
  delivery,
@@ -5,9 +5,9 @@ export default {
5
5
  ...common,
6
6
  key: "linear_app-issue-updated-instant",
7
7
  name: "New Updated Issue (Instant)",
8
- description: "Emit new event when an issue is updated. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)",
8
+ description: "Emit new event when an issue is updated. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)",
9
9
  type: "source",
10
- version: "0.3.5",
10
+ version: "0.3.7",
11
11
  dedupe: "unique",
12
12
  methods: {
13
13
  ...common.methods,
@@ -39,17 +39,22 @@ export default {
39
39
  },
40
40
  };
41
41
  },
42
+ getResource(issue) {
43
+ return this.linearApp.getIssue({
44
+ issueId: issue.id,
45
+ });
46
+ },
42
47
  getMetadata(resource) {
43
48
  const {
44
- delivery,
45
49
  title,
46
50
  data,
47
51
  updatedAt,
48
52
  } = resource;
53
+ const ts = Date.parse(data?.updatedAt || updatedAt);
49
54
  return {
50
- id: delivery || resource.id,
55
+ id: `${resource.id}-${ts}`,
51
56
  summary: `Issue Updated: ${data?.title || title}`,
52
- ts: Date.parse(updatedAt),
57
+ ts,
53
58
  };
54
59
  },
55
60
  },
@@ -1,18 +1,18 @@
1
1
  import common from "../common/webhook.mjs";
2
2
  import constants from "../../common/constants.mjs";
3
+ import utils from "../../common/utils.mjs";
3
4
 
4
5
  export default {
5
6
  ...common,
6
7
  key: "linear_app-new-issue-status-updated",
7
8
  name: "New Issue Status Updated (Instant)",
8
- description: "Emit new event when the status of an issue is updated. See the docs [here](https://developers.linear.app/docs/graphql/webhooks)",
9
+ description: "Emit new event when the status of an issue is updated. [See the documentation](https://developers.linear.app/docs/graphql/webhooks)",
9
10
  type: "source",
10
- version: "0.1.5",
11
+ version: "0.1.7",
11
12
  dedupe: "unique",
12
13
  props: {
13
14
  linearApp: common.props.linearApp,
14
- http: common.props.http,
15
- db: common.props.db,
15
+ db: "$.service.db",
16
16
  teamId: {
17
17
  label: "Team ID",
18
18
  type: "string",
@@ -20,7 +20,7 @@ export default {
20
20
  common.props.linearApp,
21
21
  "teamId",
22
22
  ],
23
- optional: true,
23
+ reloadProps: true,
24
24
  },
25
25
  projectId: {
26
26
  propDefinition: [
@@ -41,6 +41,12 @@ export default {
41
41
  },
42
42
  methods: {
43
43
  ...common.methods,
44
+ _getPreviousStatuses() {
45
+ return this.db.get("previousStatuses") || {};
46
+ },
47
+ _setPreviousStatuses(previousStatuses) {
48
+ this.db.set("previousStatuses", previousStatuses);
49
+ },
44
50
  getResourceTypes() {
45
51
  return [
46
52
  constants.RESOURCE_TYPE.ISSUE,
@@ -79,6 +85,11 @@ export default {
79
85
  isRelevant(body) {
80
86
  return body?.updatedFrom?.stateId && (!this.stateId || body.data.stateId === this.stateId);
81
87
  },
88
+ getResource(issue) {
89
+ return this.linearApp.getIssue({
90
+ issueId: issue.id,
91
+ });
92
+ },
82
93
  getMetadata(resource) {
83
94
  const {
84
95
  delivery,
@@ -86,11 +97,43 @@ export default {
86
97
  data,
87
98
  updatedAt,
88
99
  } = resource;
100
+ const ts = Date.parse(updatedAt);
89
101
  return {
90
- id: delivery || resource.id,
102
+ id: delivery || `${resource.id}-${ts}`,
91
103
  summary: `Issue status updated: ${data?.title || title}`,
92
- ts: Date.parse(updatedAt),
104
+ ts,
93
105
  };
94
106
  },
107
+ async emitPolledResources() {
108
+ const previousStatuses = this._getPreviousStatuses();
109
+ const newStatuses = {};
110
+
111
+ const stream = this.linearApp.paginateResources({
112
+ resourcesFn: this.getResourcesFn(),
113
+ resourcesFnArgs: this.getResourcesFnArgs(),
114
+ useGraphQl: this.useGraphQl(),
115
+ max: 1000,
116
+ });
117
+ const resources = await utils.streamIterator(stream);
118
+
119
+ const updatedResources = [];
120
+ for (const issue of resources) {
121
+ newStatuses[issue.id] = issue.state.id;
122
+ if (issue.createdAt === issue.updatedAt) {
123
+ continue;
124
+ }
125
+ if (previousStatuses[issue.id] !== issue.state.id) {
126
+ updatedResources.push(issue);
127
+ }
128
+ }
129
+
130
+ this._setPreviousStatuses(newStatuses);
131
+
132
+ updatedResources
133
+ .reverse()
134
+ .forEach((resource) => {
135
+ this.$emit(resource, this.getMetadata(resource));
136
+ });
137
+ },
95
138
  },
96
139
  };