├── .env.example
├── .eslintrc
├── .github
└── FUNDING.yml
├── .gitignore
├── .prettierrc
├── README.md
├── api
└── index.ts
├── package.json
├── pnpm-lock.yaml
├── prisma
├── migrations
│ ├── 20220622100647_synced_issues
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── tsconfig.json
└── typings
├── environment.d.ts
└── index.ts
/.env.example:
--------------------------------------------------------------------------------
1 | LINEAR_API_KEY=""
2 | GITHUB_API_KEY=""
3 | GITHUB_WEBHOOK_SECRET=""
4 |
5 | LINEAR_USER_ID=""
6 | LINEAR_TEAM_ID=""
7 |
8 | LINEAR_PUBLIC_LABEL_ID=""
9 | LINEAR_CANCELED_STATE_ID=""
10 | LINEAR_DONE_STATE_ID=""
11 | LINEAR_TODO_STATE_ID=""
12 | LINEAR_IN_PROGRESS_STATE_ID=""
13 |
14 | GITHUB_OWNER=""
15 | GITHUB_REPO=""
16 |
17 | DATABASE_URL=""
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "airbnb-base",
4 | "airbnb-typescript/base",
5 | "plugin:prettier/recommended"
6 | ],
7 | "plugins": ["prettier"],
8 | "rules": {
9 | "prettier/prettier": ["off"],
10 | "func-names": "off",
11 | "no-unused-vars": "off",
12 | "@typescript-eslint/no-unused-vars": "off",
13 | "max-classes-per-file": "off",
14 | "no-bitwise": "off",
15 | "class-methods-use-this": "off",
16 | "no-new": "off",
17 | "no-plusplus": "off",
18 | "no-param-reassign": "off",
19 | "no-else-return": "off",
20 | "no-useless-return": "off",
21 | "no-return-assign": "off",
22 | "consistent-return": "off",
23 | "no-underscore-dangle": "off",
24 | "no-console": "error",
25 | "no-restricted-syntax": "off",
26 | "@typescript-eslint/no-empty-function": "off"
27 | },
28 | "parserOptions": {
29 | "project": "./tsconfig.json"
30 | }
31 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | ko_fi: xPolar
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.log
3 | pnpm-debug.log*
4 | .vercel
5 | .env
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "endOfLine": "crlf",
4 | "tabWidth": 4,
5 | "useTabs": false,
6 | "printWidth": 80,
7 | "arrowParens": "avoid",
8 | "trailingComma": "none"
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Linear GitHub Sync
2 |
3 | **This repository is deprecated** and is officially survived by [SyncLinear.com](https://github.com/calcom/synclinear.com).
4 |
5 | ---
6 |
7 | This is a system to synchronize Linear issues to GitHub issues when a specific tag tag is added to the Linear issue. Spacedrive uses this to allow contributors to work with us without having to give them access to our internal Linear team.
8 |
9 |
--------------------------------------------------------------------------------
/api/index.ts:
--------------------------------------------------------------------------------
1 | import { VercelRequest, VercelResponse } from "@vercel/node";
2 | import petitio from "petitio";
3 | import { components } from "@octokit/openapi-types";
4 | import { PrismaClient } from "@prisma/client";
5 | import { LinearWebhookPayload } from "../typings";
6 | import { createHmac, timingSafeEqual } from "crypto";
7 | import {
8 | IssueCommentCreatedEvent,
9 | IssuesEditedEvent,
10 | IssuesClosedEvent,
11 | IssuesOpenedEvent
12 | } from "@octokit/webhooks-types";
13 | import { LinearClient } from "@linear/sdk";
14 |
15 | const LINEAR_PUBLIC_LABEL_ID = process.env.LINEAR_PUBLIC_LABEL_ID || "";
16 | const LINEAR_CANCELED_STATE_ID = process.env.LINEAR_CANCELED_STATE_ID || "";
17 | const LINEAR_DONE_STATE_ID = process.env.LINEAR_DONE_STATE_ID || "";
18 | const LINEAR_TODO_STATE_ID = process.env.LINEAR_TODO_STATE_ID || "";
19 |
20 | const prisma = new PrismaClient();
21 | const linear = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });
22 |
23 | const HMAC = createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET || "");
24 |
25 | export default async (req: VercelRequest, res: VercelResponse) => {
26 | if (req.method !== "POST")
27 | return res.status(405).send({
28 | success: false,
29 | message: "Only POST requests are accepted."
30 | });
31 | else if (
32 | ["35.231.147.226", "35.243.134.228"].includes(
33 | req.socket.remoteAddress || ""
34 | ) &&
35 | !req.headers["x-hub-signature-256"]
36 | )
37 | return res.status(403).send({
38 | success: false,
39 | message: "Request not from Linear or GitHub."
40 | });
41 |
42 | if (req.headers["user-agent"] === "Linear-Webhook") {
43 | const webhookPayload: LinearWebhookPayload = req.body;
44 |
45 | if (
46 | webhookPayload.action === "update" &&
47 | webhookPayload.updatedFrom &&
48 | webhookPayload.data.labelIds.includes(LINEAR_PUBLIC_LABEL_ID)
49 | ) {
50 | if (
51 | webhookPayload.updatedFrom.labelIds &&
52 | !webhookPayload.updatedFrom.labelIds.includes(
53 | LINEAR_PUBLIC_LABEL_ID
54 | )
55 | ) {
56 | const issueAlreadyExists = await prisma.syncedIssue.findFirst({
57 | where: {
58 | linearIssueId: webhookPayload.data.id,
59 | linearTeamId: webhookPayload.data.teamId
60 | }
61 | });
62 |
63 | if (issueAlreadyExists) {
64 | console.log(
65 | `Not creating issue after label added as issue ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] already exists on GitHub as issue #${issueAlreadyExists.githubIssueNumber} [${issueAlreadyExists.githubIssueId}].`
66 | );
67 |
68 | return res.status(200).send({
69 | success: true,
70 | message: "Issue already exists on GitHub."
71 | });
72 | }
73 |
74 | const issueCreator = await linear.user(
75 | webhookPayload.data.creatorId
76 | );
77 |
78 | const createdIssueResponse = await petitio(
79 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues`,
80 | "POST"
81 | )
82 | .header(
83 | "User-Agent",
84 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
85 | )
86 | .header(
87 | "Authorization",
88 | `token ${process.env.GITHUB_API_KEY}`
89 | )
90 | .body({
91 | title: `[${webhookPayload.data.team.key}-${webhookPayload.data.number}] ${webhookPayload.data.title}`,
92 | body: `${webhookPayload.data.description}${
93 | issueCreator.id !== process.env.LINEAR_USER_ID
94 | ? `\n${issueCreator.name} on Linear`
95 | : ""
96 | }`
97 | })
98 | .send();
99 |
100 | if (createdIssueResponse.statusCode !== 201) {
101 | console.log(
102 | `Failed to create GitHub issue for ${
103 | webhookPayload.data.team.key
104 | }-${webhookPayload.data.number}, received status code ${
105 | createdIssueResponse.statusCode
106 | }, body of ${JSON.stringify(
107 | await createdIssueResponse.json(),
108 | null,
109 | 4
110 | )}.`
111 | );
112 |
113 | return res.status(500).send({
114 | success: false,
115 | message: `I was unable to create an issue on Github. Status code: ${createdIssueResponse.statusCode}`
116 | });
117 | }
118 |
119 | let createdIssueData: components["schemas"]["issue"] =
120 | await createdIssueResponse.json();
121 |
122 | const linearIssue = await linear.issue(webhookPayload.data.id);
123 |
124 | const linearComments = await linearIssue
125 | .comments()
126 | .then(comments =>
127 | Promise.all(
128 | comments.nodes.map(comment =>
129 | comment.user?.then(user => ({
130 | comment,
131 | user
132 | }))
133 | )
134 | )
135 | );
136 |
137 | await Promise.all([
138 | petitio("https://api.linear.app/graphql", "POST")
139 | .header(
140 | "Authorization",
141 | `Bearer ${process.env.LINEAR_API_KEY}`
142 | )
143 | .header("Content-Type", "application/json")
144 | .body({
145 | query: `mutation {
146 | attachmentCreate(input:{
147 | issueId: "${webhookPayload.data.id}"
148 | title: "GitHub Issue #${createdIssueData.number}"
149 | subtitle: "Synchronized"
150 | url: "https://github.com/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${createdIssueData.number}"
151 | iconUrl: "https://cdn.discordapp.com/attachments/937628023497297930/988735284504043520/github.png"
152 | }) {
153 | success
154 | attachment {
155 | id
156 | }
157 | }
158 | }`
159 | })
160 | .send()
161 | .then(attachmentResponse => {
162 | const attachmentData: {
163 | success: boolean;
164 | attachment: {
165 | id: string;
166 | };
167 | } = attachmentResponse.json();
168 | if (attachmentResponse.statusCode !== 201)
169 | console.log(
170 | `Failed to create attachment for ${
171 | webhookPayload.data.team.key
172 | }-${webhookPayload.data.number} [${
173 | webhookPayload.data.id
174 | }] for GitHub issue #${
175 | createdIssueData.number
176 | } [${
177 | createdIssueData.id
178 | }], received status code ${
179 | createdIssueResponse.statusCode
180 | }, body of ${JSON.stringify(
181 | attachmentData,
182 | null,
183 | 4
184 | )}.`
185 | );
186 | else if (!attachmentData.success)
187 | console.log(
188 | `Failed to create attachment for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] for GitHub issue #${createdIssueData.number} [${createdIssueData.id}].`
189 | );
190 | else
191 | console.log(
192 | `Created attachment for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] for GitHub issue #${createdIssueData.number} [${createdIssueData.id}].`
193 | );
194 | }),
195 | prisma.syncedIssue.create({
196 | data: {
197 | githubIssueId: createdIssueData.id,
198 | linearIssueId: webhookPayload.data.id,
199 | linearTeamId: webhookPayload.data.teamId,
200 | githubIssueNumber: createdIssueData.number,
201 | linearIssueNumber: webhookPayload.data.number
202 | }
203 | })
204 | ] as Promise[]);
205 |
206 | for (const linearComment of linearComments) {
207 | if (!linearComment) continue;
208 |
209 | const { comment, user } = linearComment;
210 |
211 | await petitio(
212 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${createdIssueData.number}/comments`,
213 | "POST"
214 | )
215 | .header(
216 | "User-Agent",
217 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
218 | )
219 | .header(
220 | "Authorization",
221 | `token ${process.env.GITHUB_API_KEY}`
222 | )
223 | .body({
224 | body: `${comment.body}\n${user.name} on Linear`
225 | })
226 | .send()
227 | .then(commentResponse => {
228 | if (commentResponse.statusCode !== 201)
229 | console.log(
230 | `Failed to create GitHub comment for ${
231 | webhookPayload.data.team.key
232 | }-${webhookPayload.data.number} [${
233 | webhookPayload.data.id
234 | }] on GitHub issue #${
235 | createdIssueData.number
236 | } [${
237 | createdIssueData.id
238 | }], received status code ${
239 | createdIssueResponse.statusCode
240 | }, body of ${JSON.stringify(
241 | commentResponse.json(),
242 | null,
243 | 4
244 | )}.`
245 | );
246 | else
247 | console.log(
248 | `Created comment on GitHub issue #${createdIssueData.number} [${createdIssueData.id}] for Linear issue ${webhookPayload.data.team.key}-${webhookPayload.data.number}.`
249 | );
250 | });
251 | }
252 | }
253 |
254 | if (webhookPayload.updatedFrom.title) {
255 | const syncedIssue = await prisma.syncedIssue.findFirst({
256 | where: {
257 | linearTeamId: webhookPayload.data.teamId,
258 | linearIssueId: webhookPayload.data.id
259 | }
260 | });
261 |
262 | if (!syncedIssue) {
263 | console.log(
264 | `Skipping over title change for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] as it is not synced.`
265 | );
266 |
267 | return res.status(200).send({
268 | success: true,
269 | message: `This is not a synced issue.`
270 | });
271 | }
272 |
273 | await petitio(
274 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${syncedIssue.githubIssueNumber}`,
275 | "PATCH"
276 | )
277 | .header(
278 | "User-Agent",
279 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
280 | )
281 | .header(
282 | "Authorization",
283 | `token ${process.env.GITHUB_API_KEY}`
284 | )
285 | .body({
286 | title: `[${webhookPayload.data.team.key}-${webhookPayload.data.number}] ${webhookPayload.data.title}`
287 | })
288 | .send()
289 | .then(updatedIssueResponse => {
290 | if (updatedIssueResponse.statusCode !== 200)
291 | console.log(
292 | `Failed to update GitHub issue title for ${
293 | webhookPayload.data.team.key
294 | }-${webhookPayload.data.number} [${
295 | webhookPayload.data.id
296 | }] on GitHub issue #${
297 | syncedIssue.githubIssueNumber
298 | } [${
299 | syncedIssue.githubIssueId
300 | }], received status code ${
301 | updatedIssueResponse.statusCode
302 | }, body of ${JSON.stringify(
303 | updatedIssueResponse,
304 | null,
305 | 4
306 | )}.`
307 | );
308 | else
309 | console.log(
310 | `Updated GitHub issue title for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] on GitHub issue #${syncedIssue.githubIssueNumber} [${syncedIssue.githubIssueId}].`
311 | );
312 | });
313 | }
314 |
315 | if (webhookPayload.updatedFrom.description) {
316 | const syncedIssue = await prisma.syncedIssue.findFirst({
317 | where: {
318 | linearIssueId: webhookPayload.data.id,
319 | linearTeamId: webhookPayload.data.teamId
320 | }
321 | });
322 |
323 | if (!syncedIssue) {
324 | console.log(
325 | `Skipping over description change for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] as it is not synced.`
326 | );
327 |
328 | return res.status(200).send({
329 | success: true,
330 | message: `This is not a synced issue.`
331 | });
332 | }
333 |
334 | const issueCreator = await linear.user(
335 | webhookPayload.data.creatorId
336 | );
337 |
338 | await petitio(
339 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${syncedIssue.githubIssueNumber}`,
340 | "PATCH"
341 | )
342 | .header(
343 | "User-Agent",
344 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
345 | )
346 | .header(
347 | "Authorization",
348 | `token ${process.env.GITHUB_API_KEY}`
349 | )
350 | .body({
351 | body: `${webhookPayload.data.description}${
352 | issueCreator.id !== process.env.LINEAR_USER_ID
353 | ? `\n${issueCreator.name} on Linear`
354 | : ""
355 | }`
356 | })
357 | .send()
358 | .then(updatedIssueResponse => {
359 | if (updatedIssueResponse.statusCode !== 200)
360 | console.log(
361 | `Failed to update GitHub issue description for ${
362 | webhookPayload.data.team.key
363 | }-${webhookPayload.data.number} [${
364 | webhookPayload.data.id
365 | }] on GitHub issue #${
366 | syncedIssue.githubIssueNumber
367 | } [${
368 | syncedIssue.githubIssueId
369 | }], received status code ${
370 | updatedIssueResponse.statusCode
371 | }, body of ${JSON.stringify(
372 | updatedIssueResponse.json(),
373 | null,
374 | 4
375 | )}.`
376 | );
377 | else
378 | console.log(
379 | `Updated GitHub issue description for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] on GitHub issue #${syncedIssue.githubIssueNumber} [${syncedIssue.githubIssueId}].`
380 | );
381 | });
382 | }
383 |
384 | if (webhookPayload.updatedFrom.stateId) {
385 | if (
386 | webhookPayload.data.user?.id === process.env.LINEAR_USER_ID
387 | ) {
388 | console.log(
389 | `Skipping over state change for ${webhookPayload.data.team.key}-${webhookPayload.data.number} as it is caused by sync.`
390 | );
391 |
392 | return res.status(200).send({
393 | success: true,
394 | message: `Skipping over state change as it is created by sync.`
395 | });
396 | }
397 |
398 | const syncedIssue = await prisma.syncedIssue.findFirst({
399 | where: {
400 | linearIssueId: webhookPayload.data.id,
401 | linearTeamId: webhookPayload.data.teamId
402 | }
403 | });
404 |
405 | if (!syncedIssue) {
406 | console.log(
407 | `Skipping over state change for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] as it is not synced.`
408 | );
409 |
410 | return res.status(200).send({
411 | success: true,
412 | message: `This is not a synced issue.`
413 | });
414 | }
415 |
416 | await petitio(
417 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${syncedIssue.githubIssueNumber}`,
418 | "PATCH"
419 | )
420 | .header(
421 | "User-Agent",
422 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
423 | )
424 | .header(
425 | "Authorization",
426 | `token ${process.env.GITHUB_API_KEY}`
427 | )
428 | .body({
429 | state: [
430 | LINEAR_DONE_STATE_ID,
431 | LINEAR_CANCELED_STATE_ID
432 | ].includes(webhookPayload.data.stateId)
433 | ? "closed"
434 | : "open",
435 | state_reason:
436 | LINEAR_DONE_STATE_ID === webhookPayload.data.stateId
437 | ? "completed"
438 | : "not_planned"
439 | })
440 | .send()
441 | .then(updatedIssueResponse => {
442 | if (updatedIssueResponse.statusCode !== 200)
443 | console.log(
444 | `Failed to update GitHub issue state for ${
445 | webhookPayload.data.team.key
446 | }-${webhookPayload.data.number} [${
447 | webhookPayload.data.id
448 | }] on GitHub issue #${
449 | syncedIssue.githubIssueNumber
450 | } [${
451 | syncedIssue.githubIssueId
452 | }], received status code ${
453 | updatedIssueResponse.statusCode
454 | }, body of ${JSON.stringify(
455 | updatedIssueResponse.json(),
456 | null,
457 | 4
458 | )}.`
459 | );
460 | else
461 | console.log(
462 | `Updated GitHub issue state for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] on GitHub issue #${syncedIssue.githubIssueNumber} [${syncedIssue.githubIssueId}].`
463 | );
464 | });
465 | }
466 | }
467 |
468 | if (webhookPayload.action === "create") {
469 | if (webhookPayload.type === "Comment") {
470 | if (
471 | webhookPayload.data.user?.id === process.env.LINEAR_USER_ID
472 | ) {
473 | console.log(
474 | `Skipping over comment creation for ${
475 | webhookPayload.data.issue!.id
476 | } as it is caused by sync.`
477 | );
478 |
479 | return res.status(200).send({
480 | success: true,
481 | message: `Skipping over comment as it is created by sync.`
482 | });
483 | }
484 |
485 | const syncedIssue = await prisma.syncedIssue.findFirst({
486 | where: {
487 | linearIssueId: webhookPayload.data.issueId
488 | }
489 | });
490 |
491 | if (!syncedIssue) {
492 | console.log(
493 | `Skipping over comment for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] as it is not synced.`
494 | );
495 |
496 | return res.status(200).send({
497 | success: true,
498 | message: `This is not a synced issue.`
499 | });
500 | }
501 |
502 | await petitio(
503 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${syncedIssue.githubIssueNumber}/comments`,
504 | "POST"
505 | )
506 | .header(
507 | "User-Agent",
508 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
509 | )
510 | .header(
511 | "Authorization",
512 | `token ${process.env.GITHUB_API_KEY}`
513 | )
514 | .body({
515 | body: `${webhookPayload.data.body}\n${
516 | webhookPayload.data.user!.name
517 | } on Linear`
518 | })
519 | .send()
520 | .then(commentResponse => {
521 | if (commentResponse.statusCode !== 201)
522 | console.log(
523 | `Failed to update GitHub issue state for ${
524 | webhookPayload.data.issue?.id
525 | } on GitHub issue #${
526 | syncedIssue.githubIssueNumber
527 | } [${
528 | syncedIssue.githubIssueId
529 | }], received status code ${
530 | commentResponse.statusCode
531 | }, body of ${JSON.stringify(
532 | commentResponse.json(),
533 | null,
534 | 4
535 | )}.`
536 | );
537 | else
538 | console.log(
539 | `Synced comment [${webhookPayload.data.id}] for ${webhookPayload.data.issue?.id} on GitHub issue #${syncedIssue.githubIssueNumber} [${syncedIssue.githubIssueId}].`
540 | );
541 | });
542 | } else if (
543 | webhookPayload.type === "Issue" &&
544 | webhookPayload.data.labelIds.includes(LINEAR_PUBLIC_LABEL_ID)
545 | ) {
546 | if (
547 | webhookPayload.data.creatorId === process.env.LINEAR_USER_ID
548 | ) {
549 | console.log(
550 | `Skipping over issue creation for ${webhookPayload.data.id} as it is caused by sync.`
551 | );
552 |
553 | return res.status(200).send({
554 | success: true,
555 | message: `Skipping over issue as it is created by sync.`
556 | });
557 | }
558 |
559 | const issueAlreadyExists = await prisma.syncedIssue.findFirst({
560 | where: {
561 | linearIssueId: webhookPayload.data.id,
562 | linearTeamId: webhookPayload.data.teamId
563 | }
564 | });
565 |
566 | if (issueAlreadyExists) {
567 | console.log(
568 | `Not creating issue after label added as issue ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] already exists on GitHub as issue #${issueAlreadyExists.githubIssueNumber} [${issueAlreadyExists.githubIssueId}].`
569 | );
570 |
571 | return res.status(200).send({
572 | success: true,
573 | message: "Issue already exists on GitHub."
574 | });
575 | }
576 |
577 | const issueCreator = await linear.user(
578 | webhookPayload.data.creatorId
579 | );
580 |
581 | const createdIssueResponse = await petitio(
582 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues`,
583 | "POST"
584 | )
585 | .header(
586 | "User-Agent",
587 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
588 | )
589 | .header(
590 | "Authorization",
591 | `token ${process.env.GITHUB_API_KEY}`
592 | )
593 | .body({
594 | title: `[${webhookPayload.data.team.key}-${webhookPayload.data.number}] ${webhookPayload.data.title}`,
595 | body: `${webhookPayload.data.description}${
596 | issueCreator.id !== process.env.LINEAR_USER_ID
597 | ? `\n${issueCreator.name} on Linear`
598 | : ""
599 | }`
600 | })
601 | .send();
602 |
603 | if (createdIssueResponse.statusCode !== 201) {
604 | console.log(
605 | `Failed to create GitHub issue for ${
606 | webhookPayload.data.team.key
607 | }-${webhookPayload.data.number}, received status code ${
608 | createdIssueResponse.statusCode
609 | }, body of ${JSON.stringify(
610 | await createdIssueResponse.json(),
611 | null,
612 | 4
613 | )}.`
614 | );
615 |
616 | return res.status(500).send({
617 | success: false,
618 | message: `I was unable to create an issue on Github. Status code: ${createdIssueResponse.statusCode}`
619 | });
620 | }
621 |
622 | let createdIssueData: components["schemas"]["issue"] =
623 | await createdIssueResponse.json();
624 |
625 | await Promise.all([
626 | petitio("https://api.linear.app/graphql", "POST")
627 | .header(
628 | "Authorization",
629 | `Bearer ${process.env.LINEAR_API_KEY}`
630 | )
631 | .header("Content-Type", "application/json")
632 | .body({
633 | query: `mutation {
634 | attachmentCreate(input:{
635 | issueId: "${webhookPayload.data.id}"
636 | title: "GitHub Issue #${createdIssueData.number}"
637 | subtitle: "Synchronized"
638 | url: "https://github.com/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${createdIssueData.number}"
639 | iconUrl: "https://cdn.discordapp.com/attachments/937628023497297930/988735284504043520/github.png"
640 | }) {
641 | success
642 | attachment {
643 | id
644 | }
645 | }
646 | }`
647 | })
648 | .send()
649 | .then(attachmentResponse => {
650 | const attachmentData: {
651 | success: boolean;
652 | attachment: {
653 | id: string;
654 | };
655 | } = attachmentResponse.json();
656 | if (attachmentResponse.statusCode !== 201)
657 | console.log(
658 | `Failed to create attachment for ${
659 | webhookPayload.data.team.key
660 | }-${webhookPayload.data.number} [${
661 | webhookPayload.data.id
662 | }] for GitHub issue #${
663 | createdIssueData.number
664 | } [${
665 | createdIssueData.id
666 | }], received status code ${
667 | createdIssueResponse.statusCode
668 | }, body of ${JSON.stringify(
669 | attachmentData,
670 | null,
671 | 4
672 | )}.`
673 | );
674 | else if (!attachmentData.success)
675 | console.log(
676 | `Failed to create attachment for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] for GitHub issue #${createdIssueData.number} [${createdIssueData.id}].`
677 | );
678 | else
679 | console.log(
680 | `Created attachment for ${webhookPayload.data.team.key}-${webhookPayload.data.number} [${webhookPayload.data.id}] for GitHub issue #${createdIssueData.number} [${createdIssueData.id}].`
681 | );
682 | }),
683 | prisma.syncedIssue.create({
684 | data: {
685 | githubIssueId: createdIssueData.id,
686 | linearIssueId: webhookPayload.data.id,
687 | linearTeamId: webhookPayload.data.teamId,
688 | githubIssueNumber: createdIssueData.number,
689 | linearIssueNumber: webhookPayload.data.number
690 | }
691 | })
692 | ]);
693 | }
694 | }
695 | } else {
696 | const digest = Buffer.from(
697 | `sha256=${HMAC.update(JSON.stringify(req.body)).digest("hex")}`,
698 | "utf-8"
699 | );
700 |
701 | const sig = Buffer.from(
702 | req.headers["x-hub-signature-256"] as string,
703 | "utf-8"
704 | );
705 |
706 | if (sig.length !== digest.length || !timingSafeEqual(digest, sig)) {
707 | console.log(`Failed to verify signature for webhook.`);
708 |
709 | return res.status(403).send({
710 | success: false,
711 | message: "GitHub webhook secret doesn't match up."
712 | });
713 | }
714 |
715 | if (req.body.sender.login === "spacedrive-bot") {
716 | console.log(`Skipping over request as it is created by sync.`);
717 |
718 | return res.status(200).send({
719 | success: true,
720 | message: `Skipping over request as it is created by sync.`
721 | });
722 | }
723 |
724 | if (
725 | req.headers["x-github-event"] === "issue_comment" &&
726 | req.body.action === "created"
727 | ) {
728 | const webhookPayload: IssueCommentCreatedEvent = req.body;
729 |
730 | const syncedIssue = await prisma.syncedIssue.findFirst({
731 | where: {
732 | githubIssueNumber: webhookPayload.issue.number
733 | }
734 | });
735 |
736 | if (!syncedIssue) {
737 | console.log(
738 | `Skipping over comment for GitHub issue #${webhookPayload.issue.number} as it is not synced.`
739 | );
740 |
741 | return res.status(200).send({
742 | success: true,
743 | message: `This is not a synced issue.`
744 | });
745 | }
746 |
747 | await linear
748 | .commentCreate({
749 | issueId: syncedIssue.linearIssueId,
750 | body: `${webhookPayload.comment.body}\n— [${webhookPayload.sender.login}](${webhookPayload.sender.html_url}) on GitHub`
751 | })
752 | .then(comment => {
753 | comment.comment?.then(commentData => {
754 | commentData.issue?.then(issueData => {
755 | issueData.team?.then(teamData => {
756 | if (!comment.success)
757 | console.log(
758 | `Failed to create comment for ${syncedIssue.linearIssueNumber} [${syncedIssue.linearIssueNumber}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
759 | );
760 | else
761 | console.log(
762 | `Created comment for ${teamData.key}-${syncedIssue.linearIssueNumber} [${syncedIssue.linearIssueId}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
763 | );
764 | });
765 | });
766 | });
767 | });
768 | } else if (
769 | req.headers["x-github-event"] === "issues" &&
770 | req.body.action === "edited"
771 | ) {
772 | const webhookPayload: IssuesEditedEvent = req.body;
773 |
774 | const syncedIssue = await prisma.syncedIssue.findFirst({
775 | where: {
776 | githubIssueNumber: webhookPayload.issue.number
777 | }
778 | });
779 |
780 | if (!syncedIssue) {
781 | console.log(
782 | `Skipping over issue edit for GitHub issue #${webhookPayload.issue.number} as it is not synced.`
783 | );
784 |
785 | return res.status(200).send({
786 | success: true,
787 | message: `This is not a synced issue.`
788 | });
789 | }
790 |
791 | const title = webhookPayload.issue.title.split(
792 | `${syncedIssue.linearIssueNumber}]`
793 | );
794 | if (title.length > 1) title.shift();
795 |
796 | const description = webhookPayload.issue.body?.split("");
797 | if ((description?.length || 0) > 1) description?.pop();
798 |
799 | await linear
800 | .issueUpdate(syncedIssue.linearIssueId, {
801 | title: title.join(`${syncedIssue.linearIssueNumber}]`),
802 | description: description?.join("")
803 | })
804 | .then(updatedIssue => {
805 | updatedIssue.issue?.then(updatedIssueData => {
806 | updatedIssueData.team?.then(teamData => {
807 | if (!updatedIssue.success)
808 | console.log(
809 | `Failed to edit issue for ${syncedIssue.linearIssueNumber} [${syncedIssue.linearIssueNumber}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
810 | );
811 | else
812 | console.log(
813 | `Edited issue ${teamData.key}-${syncedIssue.linearIssueNumber} [${syncedIssue.linearIssueId}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
814 | );
815 | });
816 | });
817 | });
818 | } else if (
819 | req.headers["x-github-event"] === "issues" &&
820 | ["closed", "reopened"].includes(req.body.action)
821 | ) {
822 | const webhookPayload: IssuesClosedEvent = req.body;
823 |
824 | const syncedIssue = await prisma.syncedIssue.findFirst({
825 | where: {
826 | githubIssueNumber: webhookPayload.issue.number
827 | }
828 | });
829 |
830 | if (!syncedIssue) {
831 | console.log(
832 | `Skipping over issue edit for GitHub issue #${webhookPayload.issue.number} as it is not synced.`
833 | );
834 |
835 | return res.status(200).send({
836 | success: true,
837 | message: `This is not a synced issue.`
838 | });
839 | }
840 |
841 | const title = webhookPayload.issue.title.split(
842 | `${syncedIssue.linearIssueNumber}]`
843 | );
844 | if (title.length > 1) title.shift();
845 |
846 | await linear
847 | .issueUpdate(syncedIssue.linearIssueId, {
848 | stateId:
849 | webhookPayload.issue.state_reason === "not_planned"
850 | ? LINEAR_CANCELED_STATE_ID
851 | : webhookPayload.issue.state_reason === "completed"
852 | ? LINEAR_DONE_STATE_ID
853 | : LINEAR_TODO_STATE_ID
854 | })
855 | .then(updatedIssue => {
856 | console.log(-1);
857 | updatedIssue.issue?.then(updatedIssueData => {
858 | console.log(-2);
859 | updatedIssueData.team?.then(teamData => {
860 | if (!updatedIssue.success)
861 | console.log(
862 | `Failed to change state for ${syncedIssue.linearIssueNumber} [${syncedIssue.linearIssueNumber}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
863 | );
864 | else
865 | console.log(
866 | `Changed state ${teamData.key}-${syncedIssue.linearIssueNumber} [${syncedIssue.linearIssueId}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
867 | );
868 | });
869 | });
870 | });
871 | } else if (
872 | req.headers["x-github-event"] === "issues" &&
873 | req.body.action === "opened"
874 | ) {
875 | const webhookPayload: IssuesOpenedEvent = req.body;
876 |
877 | const createdIssueData = await linear.issueCreate({
878 | title: webhookPayload.issue.title,
879 | description: webhookPayload.issue.body,
880 | teamId: process.env.LINEAR_TEAM_ID || "",
881 | labelIds: [process.env.LINEAR_PUBLIC_LABEL_ID || ""]
882 | });
883 |
884 | if (!createdIssueData.success) {
885 | console.log(
886 | `Failed to create issue for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
887 | );
888 |
889 | return res.status(500).send({
890 | success: false,
891 | message: `Failed creating issue on Linear.`
892 | });
893 | }
894 |
895 | const createdIssue = await createdIssueData.issue;
896 |
897 | if (!createdIssue)
898 | console.log(
899 | `Failed to fetch issue I just created for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
900 | );
901 | else {
902 | const team = await createdIssue.team;
903 |
904 | if (!team) {
905 | console.log(
906 | `Failed to fetch team for issue, ${createdIssue.id} for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
907 | );
908 | } else {
909 | await Promise.all([
910 | petitio(
911 | `https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${webhookPayload.issue.number}`,
912 | "PATCH"
913 | )
914 | .header(
915 | "User-Agent",
916 | `${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}, linear-github-sync`
917 | )
918 | .header(
919 | "Authorization",
920 | `token ${process.env.GITHUB_API_KEY}`
921 | )
922 | .body({
923 | title: `[${team.key}-${createdIssue.number}] ${webhookPayload.issue.title}`
924 | })
925 | .send()
926 | .then(titleRenameResponse => {
927 | if (titleRenameResponse.statusCode !== 200)
928 | console.log(
929 | `Failed to update GitHub issue title for ${
930 | team.key
931 | }-${createdIssue.number} [${
932 | createdIssue.id
933 | }] on GitHub issue #${
934 | webhookPayload.issue.number
935 | } [${
936 | webhookPayload.issue.id
937 | }], received status code ${
938 | titleRenameResponse.statusCode
939 | }, body of ${JSON.stringify(
940 | titleRenameResponse.json(),
941 | null,
942 | 4
943 | )}.`
944 | );
945 | else
946 | console.log(
947 | `Created comment on GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}] for Linear issue ${team.key}-${createdIssue.number}.`
948 | );
949 | }),
950 | petitio("https://api.linear.app/graphql", "POST")
951 | .header(
952 | "Authorization",
953 | `Bearer ${process.env.LINEAR_API_KEY}`
954 | )
955 | .header("Content-Type", "application/json")
956 | .body({
957 | query: `mutation {
958 | attachmentCreate(input:{
959 | issueId: "${createdIssue.id}"
960 | title: "GitHub Issue #${webhookPayload.issue.number}"
961 | subtitle: "Synchronized"
962 | url: "https://github.com/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/issues/${webhookPayload.issue.number}"
963 | iconUrl: "https://cdn.discordapp.com/attachments/937628023497297930/988735284504043520/github.png"
964 | }) {
965 | success
966 | attachment {
967 | id
968 | }
969 | }
970 | }`
971 | })
972 | .send()
973 | .then(attachmentResponse => {
974 | const attachmentData: {
975 | success: boolean;
976 | attachment: {
977 | id: string;
978 | };
979 | } = attachmentResponse.json();
980 | if (attachmentResponse.statusCode !== 200)
981 | console.log(
982 | `Failed to create attachment for ${
983 | team.key
984 | }-${createdIssue.number} [${
985 | createdIssue.id
986 | }] for GitHub issue #${
987 | webhookPayload.issue.number
988 | } [${
989 | webhookPayload.issue.id
990 | }], received status code ${
991 | attachmentResponse.statusCode
992 | }, body of ${JSON.stringify(
993 | attachmentData,
994 | null,
995 | 4
996 | )}.`
997 | );
998 | else if (!attachmentData.success)
999 | console.log(
1000 | `Failed to create attachment for ${team.key}-${createdIssue.number} [${createdIssue.id}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}], received status code ${attachmentResponse.statusCode}`,
1001 | attachmentData
1002 | );
1003 | else
1004 | console.log(
1005 | `Created attachment for ${team.key}-${createdIssue.number} [${createdIssue.id}] for GitHub issue #${webhookPayload.issue.number} [${webhookPayload.issue.id}].`
1006 | );
1007 | }),
1008 | prisma.syncedIssue.create({
1009 | data: {
1010 | githubIssueNumber: webhookPayload.issue.number,
1011 | githubIssueId: webhookPayload.issue.id,
1012 | linearIssueId: createdIssue.id,
1013 | linearIssueNumber: createdIssue.number,
1014 | linearTeamId: team.id
1015 | }
1016 | })
1017 | ]);
1018 | }
1019 | }
1020 | }
1021 | }
1022 |
1023 | return res.status(200).send({
1024 | success: true
1025 | });
1026 | };
1027 |
1028 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "linear-github-sync",
3 | "private": true,
4 | "devDependencies": {
5 | "@octokit/openapi-types": "^12.4.0",
6 | "@octokit/webhooks-types": "^5.8.0",
7 | "@types/node": "^17.0.45",
8 | "@vercel/node": "^1.15.4",
9 | "eslint": "8.16.0",
10 | "prisma": "^3.15.2",
11 | "typescript": "4.7.2"
12 | },
13 | "dependencies": {
14 | "@linear/sdk": "^1.22.0",
15 | "@prisma/client": "^3.15.2",
16 | "petitio": "^1.4.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: 5.4
2 |
3 | specifiers:
4 | '@linear/sdk': ^1.22.0
5 | '@octokit/openapi-types': ^12.4.0
6 | '@octokit/webhooks-types': ^5.8.0
7 | '@prisma/client': ^3.15.2
8 | '@types/node': ^17.0.45
9 | '@vercel/node': ^1.15.4
10 | eslint: 8.16.0
11 | petitio: ^1.4.0
12 | prisma: ^3.15.2
13 | typescript: 4.7.2
14 |
15 | dependencies:
16 | '@linear/sdk': 1.22.0
17 | '@prisma/client': 3.15.2_prisma@3.15.2
18 | petitio: 1.4.0
19 |
20 | devDependencies:
21 | '@octokit/openapi-types': 12.4.0
22 | '@octokit/webhooks-types': 5.8.0
23 | '@types/node': 17.0.45
24 | '@vercel/node': 1.15.4
25 | eslint: 8.16.0
26 | prisma: 3.15.2
27 | typescript: 4.7.2
28 |
29 | packages:
30 |
31 | /@eslint/eslintrc/1.3.0:
32 | resolution: {integrity: sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==}
33 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
34 | dependencies:
35 | ajv: 6.12.6
36 | debug: 4.3.4
37 | espree: 9.3.2
38 | globals: 13.15.0
39 | ignore: 5.2.0
40 | import-fresh: 3.3.0
41 | js-yaml: 4.1.0
42 | minimatch: 3.1.2
43 | strip-json-comments: 3.1.1
44 | transitivePeerDependencies:
45 | - supports-color
46 | dev: true
47 |
48 | /@graphql-typed-document-node/core/3.1.1_graphql@15.8.0:
49 | resolution: {integrity: sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==}
50 | peerDependencies:
51 | graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
52 | dependencies:
53 | graphql: 15.8.0
54 | dev: false
55 |
56 | /@humanwhocodes/config-array/0.9.5:
57 | resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==}
58 | engines: {node: '>=10.10.0'}
59 | dependencies:
60 | '@humanwhocodes/object-schema': 1.2.1
61 | debug: 4.3.4
62 | minimatch: 3.1.2
63 | transitivePeerDependencies:
64 | - supports-color
65 | dev: true
66 |
67 | /@humanwhocodes/object-schema/1.2.1:
68 | resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
69 | dev: true
70 |
71 | /@linear/sdk/1.22.0:
72 | resolution: {integrity: sha512-QNWmtar3ZvxWmCdsAwpRU9EWHkxnopb8fsL/6JBvTiFyy4+DWRb9kW9s262njPy/Em07dU3FWSk8gcjQaoZ3kA==}
73 | engines: {node: '>=12.x', yarn: 1.x}
74 | dependencies:
75 | '@graphql-typed-document-node/core': 3.1.1_graphql@15.8.0
76 | graphql: 15.8.0
77 | isomorphic-unfetch: 3.1.0
78 | transitivePeerDependencies:
79 | - encoding
80 | dev: false
81 |
82 | /@octokit/openapi-types/12.4.0:
83 | resolution: {integrity: sha512-Npcb7Pv30b33U04jvcD7l75yLU0mxhuX2Xqrn51YyZ5WTkF04bpbxLaZ6GcaTqu03WZQHoO/Gbfp95NGRueDUA==}
84 | dev: true
85 |
86 | /@octokit/webhooks-types/5.8.0:
87 | resolution: {integrity: sha512-8adktjIb76A7viIdayQSFuBEwOzwhDC+9yxZpKNHjfzrlostHCw0/N7JWpWMObfElwvJMk2fY2l1noENCk9wmw==}
88 | dev: true
89 |
90 | /@prisma/client/3.15.2_prisma@3.15.2:
91 | resolution: {integrity: sha512-ErqtwhX12ubPhU4d++30uFY/rPcyvjk+mdifaZO5SeM21zS3t4jQrscy8+6IyB0GIYshl5ldTq6JSBo1d63i8w==}
92 | engines: {node: '>=12.6'}
93 | requiresBuild: true
94 | peerDependencies:
95 | prisma: '*'
96 | peerDependenciesMeta:
97 | prisma:
98 | optional: true
99 | dependencies:
100 | '@prisma/engines-version': 3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e
101 | prisma: 3.15.2
102 | dev: false
103 |
104 | /@prisma/engines-version/3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e:
105 | resolution: {integrity: sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==}
106 | dev: false
107 |
108 | /@prisma/engines/3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e:
109 | resolution: {integrity: sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==}
110 | requiresBuild: true
111 |
112 | /@types/node/17.0.45:
113 | resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
114 | dev: true
115 |
116 | /@vercel/node-bridge/2.2.2:
117 | resolution: {integrity: sha512-haGBC8noyA5BfjCRXRH+VIkHCDVW5iD5UX24P2nOdilwUxI4qWsattS/co8QBGq64XsNLRAMdM5pQUE3zxkF9Q==}
118 | dev: true
119 |
120 | /@vercel/node/1.15.4:
121 | resolution: {integrity: sha512-45fV7qVVw1cWCD6tWBXH0i4pSfYck4yF2qNKlJb1gmbO9JHWRqMYm0uxNWISD6E6Z69Pl1KDvfa+l48w/qEkaw==}
122 | dependencies:
123 | '@types/node': 17.0.45
124 | '@vercel/node-bridge': 2.2.2
125 | ts-node: 8.9.1_typescript@4.3.4
126 | typescript: 4.3.4
127 | dev: true
128 |
129 | /acorn-jsx/5.3.2_acorn@8.7.1:
130 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
131 | peerDependencies:
132 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
133 | dependencies:
134 | acorn: 8.7.1
135 | dev: true
136 |
137 | /acorn/8.7.1:
138 | resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==}
139 | engines: {node: '>=0.4.0'}
140 | hasBin: true
141 | dev: true
142 |
143 | /ajv/6.12.6:
144 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
145 | dependencies:
146 | fast-deep-equal: 3.1.3
147 | fast-json-stable-stringify: 2.1.0
148 | json-schema-traverse: 0.4.1
149 | uri-js: 4.4.1
150 | dev: true
151 |
152 | /ansi-regex/5.0.1:
153 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
154 | engines: {node: '>=8'}
155 | dev: true
156 |
157 | /ansi-styles/4.3.0:
158 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
159 | engines: {node: '>=8'}
160 | dependencies:
161 | color-convert: 2.0.1
162 | dev: true
163 |
164 | /arg/4.1.3:
165 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
166 | dev: true
167 |
168 | /argparse/2.0.1:
169 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
170 | dev: true
171 |
172 | /balanced-match/1.0.2:
173 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
174 | dev: true
175 |
176 | /brace-expansion/1.1.11:
177 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
178 | dependencies:
179 | balanced-match: 1.0.2
180 | concat-map: 0.0.1
181 | dev: true
182 |
183 | /buffer-from/1.1.2:
184 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
185 | dev: true
186 |
187 | /callsites/3.1.0:
188 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
189 | engines: {node: '>=6'}
190 | dev: true
191 |
192 | /chalk/4.1.2:
193 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
194 | engines: {node: '>=10'}
195 | dependencies:
196 | ansi-styles: 4.3.0
197 | supports-color: 7.2.0
198 | dev: true
199 |
200 | /color-convert/2.0.1:
201 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
202 | engines: {node: '>=7.0.0'}
203 | dependencies:
204 | color-name: 1.1.4
205 | dev: true
206 |
207 | /color-name/1.1.4:
208 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
209 | dev: true
210 |
211 | /concat-map/0.0.1:
212 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
213 | dev: true
214 |
215 | /cross-spawn/7.0.3:
216 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
217 | engines: {node: '>= 8'}
218 | dependencies:
219 | path-key: 3.1.1
220 | shebang-command: 2.0.0
221 | which: 2.0.2
222 | dev: true
223 |
224 | /debug/4.3.4:
225 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
226 | engines: {node: '>=6.0'}
227 | peerDependencies:
228 | supports-color: '*'
229 | peerDependenciesMeta:
230 | supports-color:
231 | optional: true
232 | dependencies:
233 | ms: 2.1.2
234 | dev: true
235 |
236 | /deep-is/0.1.4:
237 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
238 | dev: true
239 |
240 | /diff/4.0.2:
241 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
242 | engines: {node: '>=0.3.1'}
243 | dev: true
244 |
245 | /doctrine/3.0.0:
246 | resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
247 | engines: {node: '>=6.0.0'}
248 | dependencies:
249 | esutils: 2.0.3
250 | dev: true
251 |
252 | /escape-string-regexp/4.0.0:
253 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
254 | engines: {node: '>=10'}
255 | dev: true
256 |
257 | /eslint-scope/7.1.1:
258 | resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==}
259 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
260 | dependencies:
261 | esrecurse: 4.3.0
262 | estraverse: 5.3.0
263 | dev: true
264 |
265 | /eslint-utils/3.0.0_eslint@8.16.0:
266 | resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
267 | engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
268 | peerDependencies:
269 | eslint: '>=5'
270 | dependencies:
271 | eslint: 8.16.0
272 | eslint-visitor-keys: 2.1.0
273 | dev: true
274 |
275 | /eslint-visitor-keys/2.1.0:
276 | resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
277 | engines: {node: '>=10'}
278 | dev: true
279 |
280 | /eslint-visitor-keys/3.3.0:
281 | resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==}
282 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
283 | dev: true
284 |
285 | /eslint/8.16.0:
286 | resolution: {integrity: sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==}
287 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
288 | hasBin: true
289 | dependencies:
290 | '@eslint/eslintrc': 1.3.0
291 | '@humanwhocodes/config-array': 0.9.5
292 | ajv: 6.12.6
293 | chalk: 4.1.2
294 | cross-spawn: 7.0.3
295 | debug: 4.3.4
296 | doctrine: 3.0.0
297 | escape-string-regexp: 4.0.0
298 | eslint-scope: 7.1.1
299 | eslint-utils: 3.0.0_eslint@8.16.0
300 | eslint-visitor-keys: 3.3.0
301 | espree: 9.3.2
302 | esquery: 1.4.0
303 | esutils: 2.0.3
304 | fast-deep-equal: 3.1.3
305 | file-entry-cache: 6.0.1
306 | functional-red-black-tree: 1.0.1
307 | glob-parent: 6.0.2
308 | globals: 13.15.0
309 | ignore: 5.2.0
310 | import-fresh: 3.3.0
311 | imurmurhash: 0.1.4
312 | is-glob: 4.0.3
313 | js-yaml: 4.1.0
314 | json-stable-stringify-without-jsonify: 1.0.1
315 | levn: 0.4.1
316 | lodash.merge: 4.6.2
317 | minimatch: 3.1.2
318 | natural-compare: 1.4.0
319 | optionator: 0.9.1
320 | regexpp: 3.2.0
321 | strip-ansi: 6.0.1
322 | strip-json-comments: 3.1.1
323 | text-table: 0.2.0
324 | v8-compile-cache: 2.3.0
325 | transitivePeerDependencies:
326 | - supports-color
327 | dev: true
328 |
329 | /espree/9.3.2:
330 | resolution: {integrity: sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==}
331 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
332 | dependencies:
333 | acorn: 8.7.1
334 | acorn-jsx: 5.3.2_acorn@8.7.1
335 | eslint-visitor-keys: 3.3.0
336 | dev: true
337 |
338 | /esquery/1.4.0:
339 | resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==}
340 | engines: {node: '>=0.10'}
341 | dependencies:
342 | estraverse: 5.3.0
343 | dev: true
344 |
345 | /esrecurse/4.3.0:
346 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
347 | engines: {node: '>=4.0'}
348 | dependencies:
349 | estraverse: 5.3.0
350 | dev: true
351 |
352 | /estraverse/5.3.0:
353 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
354 | engines: {node: '>=4.0'}
355 | dev: true
356 |
357 | /esutils/2.0.3:
358 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
359 | engines: {node: '>=0.10.0'}
360 | dev: true
361 |
362 | /fast-deep-equal/3.1.3:
363 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
364 | dev: true
365 |
366 | /fast-json-stable-stringify/2.1.0:
367 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
368 | dev: true
369 |
370 | /fast-levenshtein/2.0.6:
371 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
372 | dev: true
373 |
374 | /file-entry-cache/6.0.1:
375 | resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
376 | engines: {node: ^10.12.0 || >=12.0.0}
377 | dependencies:
378 | flat-cache: 3.0.4
379 | dev: true
380 |
381 | /flat-cache/3.0.4:
382 | resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
383 | engines: {node: ^10.12.0 || >=12.0.0}
384 | dependencies:
385 | flatted: 3.2.5
386 | rimraf: 3.0.2
387 | dev: true
388 |
389 | /flatted/3.2.5:
390 | resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==}
391 | dev: true
392 |
393 | /fs.realpath/1.0.0:
394 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
395 | dev: true
396 |
397 | /functional-red-black-tree/1.0.1:
398 | resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==}
399 | dev: true
400 |
401 | /glob-parent/6.0.2:
402 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
403 | engines: {node: '>=10.13.0'}
404 | dependencies:
405 | is-glob: 4.0.3
406 | dev: true
407 |
408 | /glob/7.2.3:
409 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
410 | dependencies:
411 | fs.realpath: 1.0.0
412 | inflight: 1.0.6
413 | inherits: 2.0.4
414 | minimatch: 3.1.2
415 | once: 1.4.0
416 | path-is-absolute: 1.0.1
417 | dev: true
418 |
419 | /globals/13.15.0:
420 | resolution: {integrity: sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==}
421 | engines: {node: '>=8'}
422 | dependencies:
423 | type-fest: 0.20.2
424 | dev: true
425 |
426 | /graphql/15.8.0:
427 | resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==}
428 | engines: {node: '>= 10.x'}
429 | dev: false
430 |
431 | /has-flag/4.0.0:
432 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
433 | engines: {node: '>=8'}
434 | dev: true
435 |
436 | /ignore/5.2.0:
437 | resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==}
438 | engines: {node: '>= 4'}
439 | dev: true
440 |
441 | /import-fresh/3.3.0:
442 | resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
443 | engines: {node: '>=6'}
444 | dependencies:
445 | parent-module: 1.0.1
446 | resolve-from: 4.0.0
447 | dev: true
448 |
449 | /imurmurhash/0.1.4:
450 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
451 | engines: {node: '>=0.8.19'}
452 | dev: true
453 |
454 | /inflight/1.0.6:
455 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
456 | dependencies:
457 | once: 1.4.0
458 | wrappy: 1.0.2
459 | dev: true
460 |
461 | /inherits/2.0.4:
462 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
463 | dev: true
464 |
465 | /is-extglob/2.1.1:
466 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
467 | engines: {node: '>=0.10.0'}
468 | dev: true
469 |
470 | /is-glob/4.0.3:
471 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
472 | engines: {node: '>=0.10.0'}
473 | dependencies:
474 | is-extglob: 2.1.1
475 | dev: true
476 |
477 | /isexe/2.0.0:
478 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
479 | dev: true
480 |
481 | /isomorphic-unfetch/3.1.0:
482 | resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
483 | dependencies:
484 | node-fetch: 2.6.7
485 | unfetch: 4.2.0
486 | transitivePeerDependencies:
487 | - encoding
488 | dev: false
489 |
490 | /js-yaml/4.1.0:
491 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
492 | hasBin: true
493 | dependencies:
494 | argparse: 2.0.1
495 | dev: true
496 |
497 | /json-schema-traverse/0.4.1:
498 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
499 | dev: true
500 |
501 | /json-stable-stringify-without-jsonify/1.0.1:
502 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
503 | dev: true
504 |
505 | /levn/0.4.1:
506 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
507 | engines: {node: '>= 0.8.0'}
508 | dependencies:
509 | prelude-ls: 1.2.1
510 | type-check: 0.4.0
511 | dev: true
512 |
513 | /lodash.merge/4.6.2:
514 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
515 | dev: true
516 |
517 | /make-error/1.3.6:
518 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
519 | dev: true
520 |
521 | /minimatch/3.1.2:
522 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
523 | dependencies:
524 | brace-expansion: 1.1.11
525 | dev: true
526 |
527 | /ms/2.1.2:
528 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
529 | dev: true
530 |
531 | /natural-compare/1.4.0:
532 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
533 | dev: true
534 |
535 | /node-fetch/2.6.7:
536 | resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
537 | engines: {node: 4.x || >=6.0.0}
538 | peerDependencies:
539 | encoding: ^0.1.0
540 | peerDependenciesMeta:
541 | encoding:
542 | optional: true
543 | dependencies:
544 | whatwg-url: 5.0.0
545 | dev: false
546 |
547 | /once/1.4.0:
548 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
549 | dependencies:
550 | wrappy: 1.0.2
551 | dev: true
552 |
553 | /optionator/0.9.1:
554 | resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
555 | engines: {node: '>= 0.8.0'}
556 | dependencies:
557 | deep-is: 0.1.4
558 | fast-levenshtein: 2.0.6
559 | levn: 0.4.1
560 | prelude-ls: 1.2.1
561 | type-check: 0.4.0
562 | word-wrap: 1.2.3
563 | dev: true
564 |
565 | /parent-module/1.0.1:
566 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
567 | engines: {node: '>=6'}
568 | dependencies:
569 | callsites: 3.1.0
570 | dev: true
571 |
572 | /path-is-absolute/1.0.1:
573 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
574 | engines: {node: '>=0.10.0'}
575 | dev: true
576 |
577 | /path-key/3.1.1:
578 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
579 | engines: {node: '>=8'}
580 | dev: true
581 |
582 | /petitio/1.4.0:
583 | resolution: {integrity: sha512-9LaVd/5BLmbNU8Q4Ax8NezihiPt2ISNqi2vKilEchSSf+YSOXxfsLUb0SUmDskm1WkBOVTsqdyuyYI0RYKqr0Q==}
584 | engines: {node: '>=12.3.0'}
585 | dev: false
586 |
587 | /prelude-ls/1.2.1:
588 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
589 | engines: {node: '>= 0.8.0'}
590 | dev: true
591 |
592 | /prisma/3.15.2:
593 | resolution: {integrity: sha512-nMNSMZvtwrvoEQ/mui8L/aiCLZRCj5t6L3yujKpcDhIPk7garp8tL4nMx2+oYsN0FWBacevJhazfXAbV1kfBzA==}
594 | engines: {node: '>=12.6'}
595 | hasBin: true
596 | requiresBuild: true
597 | dependencies:
598 | '@prisma/engines': 3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e
599 |
600 | /punycode/2.1.1:
601 | resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
602 | engines: {node: '>=6'}
603 | dev: true
604 |
605 | /regexpp/3.2.0:
606 | resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
607 | engines: {node: '>=8'}
608 | dev: true
609 |
610 | /resolve-from/4.0.0:
611 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
612 | engines: {node: '>=4'}
613 | dev: true
614 |
615 | /rimraf/3.0.2:
616 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
617 | hasBin: true
618 | dependencies:
619 | glob: 7.2.3
620 | dev: true
621 |
622 | /shebang-command/2.0.0:
623 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
624 | engines: {node: '>=8'}
625 | dependencies:
626 | shebang-regex: 3.0.0
627 | dev: true
628 |
629 | /shebang-regex/3.0.0:
630 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
631 | engines: {node: '>=8'}
632 | dev: true
633 |
634 | /source-map-support/0.5.21:
635 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
636 | dependencies:
637 | buffer-from: 1.1.2
638 | source-map: 0.6.1
639 | dev: true
640 |
641 | /source-map/0.6.1:
642 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
643 | engines: {node: '>=0.10.0'}
644 | dev: true
645 |
646 | /strip-ansi/6.0.1:
647 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
648 | engines: {node: '>=8'}
649 | dependencies:
650 | ansi-regex: 5.0.1
651 | dev: true
652 |
653 | /strip-json-comments/3.1.1:
654 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
655 | engines: {node: '>=8'}
656 | dev: true
657 |
658 | /supports-color/7.2.0:
659 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
660 | engines: {node: '>=8'}
661 | dependencies:
662 | has-flag: 4.0.0
663 | dev: true
664 |
665 | /text-table/0.2.0:
666 | resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
667 | dev: true
668 |
669 | /tr46/0.0.3:
670 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
671 | dev: false
672 |
673 | /ts-node/8.9.1_typescript@4.3.4:
674 | resolution: {integrity: sha512-yrq6ODsxEFTLz0R3BX2myf0WBCSQh9A+py8PBo1dCzWIOcvisbyH6akNKqDHMgXePF2kir5mm5JXJTH3OUJYOQ==}
675 | engines: {node: '>=6.0.0'}
676 | hasBin: true
677 | peerDependencies:
678 | typescript: '>=2.7'
679 | dependencies:
680 | arg: 4.1.3
681 | diff: 4.0.2
682 | make-error: 1.3.6
683 | source-map-support: 0.5.21
684 | typescript: 4.3.4
685 | yn: 3.1.1
686 | dev: true
687 |
688 | /type-check/0.4.0:
689 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
690 | engines: {node: '>= 0.8.0'}
691 | dependencies:
692 | prelude-ls: 1.2.1
693 | dev: true
694 |
695 | /type-fest/0.20.2:
696 | resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
697 | engines: {node: '>=10'}
698 | dev: true
699 |
700 | /typescript/4.3.4:
701 | resolution: {integrity: sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==}
702 | engines: {node: '>=4.2.0'}
703 | hasBin: true
704 | dev: true
705 |
706 | /typescript/4.7.2:
707 | resolution: {integrity: sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==}
708 | engines: {node: '>=4.2.0'}
709 | hasBin: true
710 | dev: true
711 |
712 | /unfetch/4.2.0:
713 | resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
714 | dev: false
715 |
716 | /uri-js/4.4.1:
717 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
718 | dependencies:
719 | punycode: 2.1.1
720 | dev: true
721 |
722 | /v8-compile-cache/2.3.0:
723 | resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==}
724 | dev: true
725 |
726 | /webidl-conversions/3.0.1:
727 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
728 | dev: false
729 |
730 | /whatwg-url/5.0.0:
731 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
732 | dependencies:
733 | tr46: 0.0.3
734 | webidl-conversions: 3.0.1
735 | dev: false
736 |
737 | /which/2.0.2:
738 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
739 | engines: {node: '>= 8'}
740 | hasBin: true
741 | dependencies:
742 | isexe: 2.0.0
743 | dev: true
744 |
745 | /word-wrap/1.2.3:
746 | resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==}
747 | engines: {node: '>=0.10.0'}
748 | dev: true
749 |
750 | /wrappy/1.0.2:
751 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
752 | dev: true
753 |
754 | /yn/3.1.1:
755 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
756 | engines: {node: '>=6'}
757 | dev: true
758 |
--------------------------------------------------------------------------------
/prisma/migrations/20220622100647_synced_issues/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "synced_issues" (
3 | "id" TEXT NOT NULL,
4 | "githubIssueNumber" INTEGER NOT NULL,
5 | "linearIssueNumber" INTEGER NOT NULL,
6 | "githubIssueId" INTEGER NOT NULL,
7 | "linearIssueId" TEXT NOT NULL,
8 | "linearTeamId" TEXT NOT NULL,
9 |
10 | CONSTRAINT "synced_issues_pkey" PRIMARY KEY ("id")
11 | );
12 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgres"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model SyncedIssue {
11 | id String @id @default(cuid())
12 |
13 | githubIssueNumber Int
14 | linearIssueNumber Int
15 |
16 | githubIssueId Int
17 | linearIssueId String
18 |
19 | linearTeamId String
20 |
21 | @@map("synced_issues")
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "lib": ["ESNext"],
7 |
8 | "rootDir": ".",
9 | "outDir": "dist",
10 |
11 | "strict": true,
12 | "alwaysStrict": true,
13 | "strictFunctionTypes": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "allowSyntheticDefaultImports": true,
17 |
18 | "forceConsistentCasingInFileNames": true,
19 | "noImplicitAny": true,
20 | "noImplicitThis": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noImplicitOverride": true,
25 |
26 | "skipLibCheck": true,
27 |
28 | "pretty": true,
29 |
30 | "typeRoots": ["node_modules/@types", "typings/"]
31 | },
32 | "main": "src/index.ts"
33 | }
--------------------------------------------------------------------------------
/typings/environment.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | LINEAR_API_KEY: string;
5 | GITHUB_API_KEY: string;
6 | GITHUB_WEBHOOK_SECRET: string;
7 |
8 | LINEAR_USER_ID: string;
9 | LINEAR_TEAM_ID: string;
10 |
11 | LINEAR_PUBLIC_LABEL_ID: string;
12 | LINEAR_CANCELED_STATE_ID: string;
13 | LINEAR_DONE_STATE_ID: string;
14 | LINEAR_TODO_STATE_ID: string;
15 | LINEAR_IN_PROGRESS_STATE_ID: string;
16 |
17 | GITHUB_OWNER: string;
18 | GITHUB_REPO: string;
19 | }
20 | }
21 | }
22 |
23 | export {};
24 |
25 |
--------------------------------------------------------------------------------
/typings/index.ts:
--------------------------------------------------------------------------------
1 | interface LinearWebhookPayload {
2 | action: "create" | "update" | "remove";
3 | type: string;
4 | createdAt: string;
5 | data: LinearData;
6 | url: string;
7 | updatedFrom?: Partial;
8 | }
9 |
10 | interface LinearData {
11 | id: string;
12 | createdAt: string;
13 | updatedAt: string;
14 | number: number;
15 | title: string;
16 | description: string;
17 | priority: number;
18 | boardOrder: number;
19 | sortOrder: number;
20 | startedAt: string;
21 | teamId: string;
22 | projectId: string;
23 | // previousIdentifiers: string[];
24 | creatorId: string;
25 | assigneeId: string;
26 | stateId: string;
27 | priorityLabel: string;
28 | subscriberIds: string[];
29 | labelIds: string[];
30 | assignee: LinearObject;
31 | project: LinearObject;
32 | state: LinearState;
33 | team: LinearTeam;
34 | user?: LinearObject;
35 | body?: string;
36 | issueId?: string;
37 | issue?: {
38 | id: string;
39 | title: string;
40 | };
41 | }
42 |
43 | interface LinearObject {
44 | id: string;
45 | name: string;
46 | }
47 |
48 | interface ColoredLinearObject extends LinearObject {
49 | color: string;
50 | }
51 |
52 | interface LinearState extends ColoredLinearObject {
53 | type: string;
54 | }
55 |
56 | interface LinearTeam extends LinearObject {
57 | key: string;
58 | }
59 |
60 | export { LinearWebhookPayload };
61 |
62 |
--------------------------------------------------------------------------------