├── .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 | --------------------------------------------------------------------------------