From 3589d26fb3bd266bead01ecd5a2cafcbb83be7a4 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 13 Jun 2023 15:47:59 +0100 Subject: [PATCH] Implement task retrying to recording task attempts --- apps/webapp/app/models/task.server.ts | 28 +++ .../api.v1.runs.$runId.tasks.$id.complete.ts | 53 ++++-- .../api.v1.runs.$runId.tasks.$id.fail.ts | 166 ++++++++++++++++++ .../app/routes/api.v1.runs.$runId.tasks.ts | 35 ++-- .../app/services/runs/performRunExecution.ts | 125 +++++++++++-- apps/webapp/app/utils.ts | 12 ++ .../migration.sql | 22 +++ apps/webapp/prisma/schema.prisma | 28 +++ examples/nextjs-example/package.json | 5 +- examples/nextjs-example/src/mocks/handlers.ts | 40 +++++ examples/nextjs-example/src/trigger.ts | 78 ++++++-- integrations/github/package.json | 4 +- integrations/github/src/index.ts | 16 +- integrations/github/src/tasks.ts | 53 +++++- packages/internal/src/schemas/api.ts | 27 +++ packages/internal/src/schemas/errors.ts | 1 + packages/internal/src/schemas/tasks.ts | 3 + packages/trigger-sdk/src/apiClient.ts | 24 +++ packages/trigger-sdk/src/errors.ts | 21 +++ packages/trigger-sdk/src/index.ts | 4 + packages/trigger-sdk/src/integrations.ts | 4 + packages/trigger-sdk/src/io.ts | 98 ++++++++--- .../trigger-sdk/src/ioWithIntegrations.ts | 17 +- packages/trigger-sdk/src/retry.ts | 37 ++++ packages/trigger-sdk/src/triggerClient.ts | 16 +- pnpm-lock.yaml | 78 ++++++-- 26 files changed, 900 insertions(+), 95 deletions(-) create mode 100644 apps/webapp/app/models/task.server.ts create mode 100644 apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts create mode 100644 apps/webapp/prisma/migrations/20230612150500_add_task_attempts/migration.sql create mode 100644 examples/nextjs-example/src/mocks/handlers.ts create mode 100644 packages/trigger-sdk/src/errors.ts create mode 100644 packages/trigger-sdk/src/retry.ts diff --git a/apps/webapp/app/models/task.server.ts b/apps/webapp/app/models/task.server.ts new file mode 100644 index 000000000..1bc644e78 --- /dev/null +++ b/apps/webapp/app/models/task.server.ts @@ -0,0 +1,28 @@ +import type { Task, TaskAttempt } from ".prisma/client"; +import { ServerTask } from "@trigger.dev/internal"; + +export type TaskWithAttempts = Task & { attempts: TaskAttempt[] }; + +export function taskWithAttemptsToServerTask( + task: TaskWithAttempts +): ServerTask { + return { + id: task.id, + name: task.name, + icon: task.icon, + noop: task.noop, + startedAt: task.startedAt, + completedAt: task.completedAt, + delayUntil: task.delayUntil, + status: task.status, + description: task.description, + params: task.params as any, + output: task.output as any, + properties: task.properties as any, + style: task.style as any, + error: task.error, + parentId: task.parentId, + attempts: task.attempts.length, + idempotencyKey: task.idempotencyKey, + }; +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts index e015d2229..9bd414b59 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.complete.ts @@ -1,10 +1,10 @@ import type { ActionArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; -import type { CompleteTaskBodyOutput } from "@trigger.dev/internal"; +import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/internal"; import { CompleteTaskBodyInputSchema } from "@trigger.dev/internal"; import { z } from "zod"; -import type { PrismaClient } from "~/db.server"; -import { prisma } from "~/db.server"; +import { $transaction, PrismaClient, prisma } from "~/db.server"; +import { taskWithAttemptsToServerTask } from "~/models/task.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger"; @@ -81,7 +81,7 @@ export class CompleteRunTaskService { runId: string, id: string, taskBody: CompleteTaskBodyOutput - ) { + ): Promise { // Using a transaction, we'll first check to see if the task already exists and return if if it does // If it doesn't exist, we'll create it and return it const task = await this.#prismaClient.$transaction(async (prisma) => { @@ -91,6 +91,15 @@ export class CompleteRunTaskService { }, include: { run: true, + attempts: { + where: { + status: "PENDING", + }, + orderBy: { + number: "desc", + }, + take: 1, + }, }, }); @@ -117,20 +126,36 @@ export class CompleteRunTaskService { return existingTask; } - const task = await prisma.task.update({ - where: { - id, - }, - data: { - status: "COMPLETED", - output: taskBody.output ?? undefined, - completedAt: new Date(), - }, + const task = await $transaction(prisma, async (tx) => { + if (existingTask.attempts.length === 1) { + await tx.taskAttempt.update({ + where: { + id: existingTask.attempts[0].id, + }, + data: { + status: "COMPLETED", + }, + }); + } + + return await tx.task.update({ + where: { + id, + }, + data: { + status: "COMPLETED", + output: taskBody.output ?? undefined, + completedAt: new Date(), + }, + include: { + attempts: true, + }, + }); }); return task; }); - return task; + return task ? taskWithAttemptsToServerTask(task) : undefined; } } diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts new file mode 100644 index 000000000..3da7269e6 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.$id.fail.ts @@ -0,0 +1,166 @@ +import type { ActionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { + FailTaskBodyInput, + FailTaskBodyInputSchema, + ServerTask, +} from "@trigger.dev/internal"; +import { z } from "zod"; +import { $transaction, PrismaClient, prisma } from "~/db.server"; +import { taskWithAttemptsToServerTask } from "~/models/task.server"; +import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { logger } from "~/services/logger"; +import { formatError } from "~/utils"; + +const ParamsSchema = z.object({ + runId: z.string(), + id: z.string(), +}); + +export async function action({ request, params }: ActionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + // Next authenticate the request + const authenticatedEnv = await authenticateApiRequest(request); + + if (!authenticatedEnv) { + return json({ error: "Invalid or Missing API key" }, { status: 401 }); + } + + const { runId, id } = ParamsSchema.parse(params); + + // Now parse the request body + const anyBody = await request.json(); + + logger.debug("FailRunTaskService.call() request body", { + body: anyBody, + runId, + id, + }); + + const body = FailTaskBodyInputSchema.safeParse(anyBody); + + if (!body.success) { + return json({ error: "Invalid request body" }, { status: 400 }); + } + + const service = new FailRunTaskService(); + + try { + const task = await service.call(authenticatedEnv, runId, id, body.data); + + logger.debug("FailRunTaskService.call() response body", { + runId, + id, + task, + }); + + if (!task) { + return json({ message: "Task not found" }, { status: 404 }); + } + + return json(task); + } catch (error) { + if (error instanceof Error) { + return json({ error: error.message }, { status: 400 }); + } + + return json({ error: "Something went wrong" }, { status: 500 }); + } +} + +export class FailRunTaskService { + #prismaClient: PrismaClient; + + constructor(prismaClient: PrismaClient = prisma) { + this.#prismaClient = prismaClient; + } + + public async call( + environment: AuthenticatedEnvironment, + runId: string, + id: string, + taskBody: FailTaskBodyInput + ): Promise { + // Using a transaction, we'll first check to see if the task already exists and return if if it does + // If it doesn't exist, we'll create it and return it + const task = await this.#prismaClient.$transaction(async (prisma) => { + const existingTask = await prisma.task.findUnique({ + where: { + id, + }, + include: { + run: true, + attempts: { + where: { + status: "PENDING", + }, + orderBy: { + number: "desc", + }, + take: 1, + }, + }, + }); + + if (!existingTask) { + return; + } + + if (existingTask.runId !== runId) { + return; + } + + if (existingTask.run.environmentId !== environment.id) { + return; + } + + if ( + existingTask.status === "COMPLETED" || + existingTask.status === "ERRORED" + ) { + logger.debug("Task already completed", { + existingTask, + }); + + return existingTask; + } + + const task = await $transaction(prisma, async (tx) => { + if (existingTask.attempts.length === 1) { + await tx.taskAttempt.update({ + where: { + id: existingTask.attempts[0].id, + }, + data: { + status: "ERRORED", + error: formatError(taskBody.error), + }, + }); + } + + return await prisma.task.update({ + where: { + id, + }, + data: { + status: "ERRORED", + output: taskBody.error ?? undefined, + completedAt: new Date(), + }, + include: { + attempts: true, + }, + }); + }); + + return task; + }); + + return task ? taskWithAttemptsToServerTask(task) : undefined; + } +} diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts index 22ece590d..69e1f11a4 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.tasks.ts @@ -1,10 +1,13 @@ import type { ActionArgs, LoaderArgs } from "@remix-run/server-runtime"; import { json } from "@remix-run/server-runtime"; -import { RunTaskBodyOutput, ServerTaskSchema } from "@trigger.dev/internal"; -import { RunTaskBodyOutputSchema } from "@trigger.dev/internal"; +import { + RunTaskBodyOutput, + RunTaskBodyOutputSchema, + ServerTask, +} from "@trigger.dev/internal"; import { z } from "zod"; -import type { PrismaClient } from "~/db.server"; -import { prisma } from "~/db.server"; +import { $transaction, PrismaClient, prisma } from "~/db.server"; +import { taskWithAttemptsToServerTask } from "~/models/task.server"; import { authenticateApiRequest } from "~/services/apiAuth.server"; import { logger } from "~/services/logger"; import { ulid } from "~/services/ulid.server"; @@ -145,7 +148,7 @@ export async function action({ request, params }: ActionArgs) { task, }); - return json(ServerTaskSchema.parse(task)); + return json(task); } catch (error) { if (error instanceof Error) { return json({ error: error.message }, { status: 400 }); @@ -166,17 +169,18 @@ export class RunTaskService { runId: string, idempotencyKey: string, taskBody: RunTaskBodyOutput - ) { - // Using a transaction, we'll first check to see if the task already exists and return if if it does - // If it doesn't exist, we'll create it and return it - const task = await this.#prismaClient.$transaction(async (prisma) => { - const existingTask = await prisma.task.findUnique({ + ): Promise { + const task = await $transaction(this.#prismaClient, async (tx) => { + const existingTask = await tx.task.findUnique({ where: { runId_idempotencyKey: { runId, idempotencyKey, }, }, + include: { + attempts: true, + }, }); if (existingTask) { @@ -192,7 +196,7 @@ export class RunTaskService { ? "COMPLETED" : "RUNNING"; - const task = await prisma.task.create({ + const task = await tx.task.create({ data: { id: ulid(), idempotencyKey, @@ -227,15 +231,22 @@ export class RunTaskService { properties: taskBody.properties ?? undefined, redact: taskBody.redact ?? undefined, style: taskBody.style ?? { style: "normal" }, + attempts: { + create: { + number: 1, + status: "PENDING", + }, + }, }, include: { run: true, + attempts: true, }, }); return task; }); - return task; + return taskWithAttemptsToServerTask(task); } } diff --git a/apps/webapp/app/services/runs/performRunExecution.ts b/apps/webapp/app/services/runs/performRunExecution.ts index 507b51401..e29e6e184 100644 --- a/apps/webapp/app/services/runs/performRunExecution.ts +++ b/apps/webapp/app/services/runs/performRunExecution.ts @@ -1,11 +1,14 @@ +import type { Task } from ".prisma/client"; import { ApiEventLogSchema, CachedTaskSchema, RunJobError, RunJobResumeWithTask, + RunJobRetryWithTask, RunJobSuccess, } from "@trigger.dev/internal"; import { generateErrorMessage } from "zod-error"; +import { EXECUTE_JOB_RETRY_LIMIT } from "~/consts"; import { $transaction, PrismaClient, @@ -13,11 +16,10 @@ import { prisma, } from "~/db.server"; import { resolveRunConnections } from "~/models/runConnection.server"; +import { formatError } from "~/utils"; import { safeJsonZodParse } from "~/utils/json"; import { EndpointApi } from "../endpointApi"; import { workerQueue } from "../worker.server"; -import type { Task } from ".prisma/client"; -import { EXECUTE_JOB_RETRY_LIMIT } from "~/consts"; type FoundRunExecution = NonNullable< Awaited> @@ -216,15 +218,24 @@ export class PerformRunExecutionService { let resumedTask: Task | undefined; if (execution.resumeTaskId) { - resumedTask = await this.#prismaClient.task.update({ - where: { - id: execution.resumeTaskId, - }, - data: { - status: "COMPLETED", - completedAt: new Date(), - }, - }); + resumedTask = + (await this.#prismaClient.task.findUnique({ + where: { + id: execution.resumeTaskId, + }, + })) ?? undefined; + + if (resumedTask) { + resumedTask = await this.#prismaClient.task.update({ + where: { + id: execution.resumeTaskId, + }, + data: { + status: resumedTask.noop ? "COMPLETED" : "RUNNING", + completedAt: resumedTask.noop ? new Date() : undefined, + }, + }); + } } const { response, parser } = await client.executeJobRequest({ @@ -289,7 +300,9 @@ export class PerformRunExecutionService { }); } - switch (safeBody.data.status) { + const status = safeBody.data.status; + + switch (status) { case "SUCCESS": { await this.#completeRunWithSuccess(execution, safeBody.data); @@ -305,6 +318,15 @@ export class PerformRunExecutionService { break; } + case "RETRY_WITH_TASK": { + await this.#retryRunWithTask(execution, safeBody.data); + + break; + } + default: { + const _exhaustiveCheck: never = status; + throw new Error(`Non-exhaustive match for value: ${status}`); + } } } @@ -416,6 +438,85 @@ export class PerformRunExecutionService { }); } + async #retryRunWithTask( + execution: FoundRunExecution, + data: RunJobRetryWithTask + ) { + const { run } = execution; + + return await $transaction(this.#prismaClient, async (tx) => { + // We need to check for an existing task attempt + const existingAttempt = await tx.taskAttempt.findFirst({ + where: { + taskId: data.task.id, + status: "PENDING", + }, + orderBy: { + number: "desc", + }, + }); + + if (existingAttempt) { + await tx.taskAttempt.update({ + where: { + id: existingAttempt.id, + }, + data: { + status: "ERRORED", + error: formatError(data.error), + }, + }); + } + + // We need to create a new task attempt + await tx.taskAttempt.create({ + data: { + taskId: data.task.id, + number: existingAttempt ? existingAttempt.number + 1 : 1, + status: "PENDING", + runAt: data.retryAt, + }, + }); + + await tx.task.update({ + where: { + id: data.task.id, + }, + data: { + status: "WAITING", + }, + }); + + // Now we need to create a new job execution + const newJobExecution = await tx.jobRunExecution.create({ + data: { + runId: run.id, + reason: "EXECUTE_JOB", + status: "PENDING", + retryLimit: EXECUTE_JOB_RETRY_LIMIT, + resumeTaskId: data.task.id, + }, + }); + + const graphileJob = await workerQueue.enqueue( + "performRunExecution", + { + id: newJobExecution.id, + }, + { tx, runAt: data.retryAt } + ); + + await tx.jobRunExecution.update({ + where: { + id: newJobExecution.id, + }, + data: { + graphileJobId: graphileJob.id, + }, + }); + }); + } + async #failRunExecutionWithRetry( execution: FoundRunExecution, output: Record diff --git a/apps/webapp/app/utils.ts b/apps/webapp/app/utils.ts index 1916dab47..d5d43651a 100644 --- a/apps/webapp/app/utils.ts +++ b/apps/webapp/app/utils.ts @@ -1,3 +1,4 @@ +import { ErrorWithStack } from "@/../../packages/internal/src"; import type { RouteMatch } from "@remix-run/react"; import { useMatches } from "@remix-run/react"; import humanizeDuration from "humanize-duration"; @@ -182,3 +183,14 @@ export const obfuscateApiKey = (apiKey: string) => { const [prefix, slug, secretPart] = apiKey.split("_"); return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`; }; + +export function formatError( + error: ErrorWithStack, + style: "short" | "long" = "short" +): string { + if (style === "short") { + return error.name ? `${error.name}: ${error.message}` : error.message; + } + + return formatError(error, "short") + "\n" + error.stack; +} diff --git a/apps/webapp/prisma/migrations/20230612150500_add_task_attempts/migration.sql b/apps/webapp/prisma/migrations/20230612150500_add_task_attempts/migration.sql new file mode 100644 index 000000000..2231e54b6 --- /dev/null +++ b/apps/webapp/prisma/migrations/20230612150500_add_task_attempts/migration.sql @@ -0,0 +1,22 @@ +-- CreateEnum +CREATE TYPE "TaskAttemptStatus" AS ENUM ('PENDING', 'STARTED', 'COMPLETED', 'ERRORED'); + +-- CreateTable +CREATE TABLE "TaskAttempt" ( + "id" TEXT NOT NULL, + "number" INTEGER NOT NULL, + "taskId" TEXT NOT NULL, + "status" "TaskAttemptStatus" NOT NULL DEFAULT 'PENDING', + "error" TEXT, + "runAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TaskAttempt_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TaskAttempt_taskId_number_key" ON "TaskAttempt"("taskId", "number"); + +-- AddForeignKey +ALTER TABLE "TaskAttempt" ADD CONSTRAINT "TaskAttempt_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma index 6f1aabd2f..841892bf0 100644 --- a/apps/webapp/prisma/schema.prisma +++ b/apps/webapp/prisma/schema.prisma @@ -676,6 +676,7 @@ model Task { children Task[] @relation("TaskParent") executions JobRunExecution[] + attempts TaskAttempt[] @@unique([runId, idempotencyKey]) } @@ -688,6 +689,33 @@ enum TaskStatus { ERRORED } +model TaskAttempt { + id String @id @default(cuid()) + + number Int + + task Task @relation(fields: [taskId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskId String + + status TaskAttemptStatus @default(PENDING) + + error String? + + runAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([taskId, number]) +} + +enum TaskAttemptStatus { + PENDING + STARTED + COMPLETED + ERRORED +} + model SecretReference { id String @id @default(cuid()) key String @unique diff --git a/examples/nextjs-example/package.json b/examples/nextjs-example/package.json index e20a5be0d..633fb4392 100644 --- a/examples/nextjs-example/package.json +++ b/examples/nextjs-example/package.json @@ -9,19 +9,22 @@ "lint": "next lint" }, "dependencies": { - "@trigger.dev/sdk": "workspace:*", "@trigger.dev/github": "workspace:*", "@trigger.dev/nextjs": "workspace:*", + "@trigger.dev/sdk": "workspace:*", "@types/node": "18.15.13", "@types/react": "^18.0.21", "@types/react-dom": "^18.0.6", "next": "13.3.1", + "node-fetch": "2.6.x", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "5.0.4", "zod": "^3.20.2" }, "devDependencies": { + "@types/node-fetch": "2.6.x", + "msw": "^0.47.0", "ts-loader": "^9.4.2", "webpack": "^5.0.0" } diff --git a/examples/nextjs-example/src/mocks/handlers.ts b/examples/nextjs-example/src/mocks/handlers.ts new file mode 100644 index 000000000..8a06f03c0 --- /dev/null +++ b/examples/nextjs-example/src/mocks/handlers.ts @@ -0,0 +1,40 @@ +// src/mocks/handlers.js +import { rest } from "msw"; +import { z } from "zod"; + +export const handlers = [ + rest.get( + "https://api.github.com/repos/ericallam/triggerdotdev-test-repo", + (req, res, ctx) => { + const { "x-trigger-attempt": attempts } = z + .object({ + "x-trigger-attempt": z.coerce.number().optional(), + }) + .parse(Object.fromEntries(req.headers.entries())); + + if (typeof attempts === "number" && attempts > 2) { + return req.passthrough(); + } + + // Return a rate-limited error + const nowInSeconds = Math.floor(Date.now() / 1000); + const rateLimitResetTime = nowInSeconds + 10; + + return res( + // Respond with a 200 status code + ctx.status(403), + // And a response body of an error + ctx.json({ + message: "API rate limit exceeded", + documentation_url: + "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting", + }), + ctx.set({ + "x-ratelimit-limit": "60", + "x-ratelimit-remaining": "0", + "x-ratelimit-reset": rateLimitResetTime.toString(), + }) + ); + } + ), +]; diff --git a/examples/nextjs-example/src/trigger.ts b/examples/nextjs-example/src/trigger.ts index dc096100e..26a90227e 100644 --- a/examples/nextjs-example/src/trigger.ts +++ b/examples/nextjs-example/src/trigger.ts @@ -1,3 +1,11 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./mocks/handlers"; + +const mockServer = setupServer(...handlers); +mockServer.listen({ + onUnhandledRequest: "bypass", +}); + import { Github, events } from "@trigger.dev/github"; import { DynamicSchedule, @@ -7,11 +15,13 @@ import { cronTrigger, eventTrigger, intervalTrigger, + isTriggerError, missingConnectionNotification, missingConnectionResolvedNotification, } from "@trigger.dev/sdk"; import { Slack } from "@trigger.dev/slack"; import { z } from "zod"; +import fetch from "node-fetch"; export const client = new TriggerClient({ id: "nextjs-example", @@ -21,8 +31,12 @@ export const client = new TriggerClient({ logLevel: "debug", }); -export const github = new Github({ id: "github" }); -const githubUser = new Github({ id: "github-user" }); +export const github = new Github({ + id: "github", + octokitRequest: { fetch }, +}); + +const githubUser = new Github({ id: "github-user", octokitRequest: { fetch } }); // const githubLocal = new Github({ // id: "github-local", @@ -140,12 +154,6 @@ new Job(client, { github: githubUser, }, run: async (payload, io, ctx) => { - await io.logger.info("This is a log info message", { - payload, - }); - - await io.wait("wait", 1); - return await io.github.getRepo("get.repo", payload); }, }); @@ -240,9 +248,57 @@ new Job(client, { name: "foo.bar", }), run: async (payload, io, ctx) => { - await io.runTask("task-1", { name: "task-1" }, async (task) => { - throw new Error("Task failed"); - }); + await io.try( + async () => { + return await io.runTask( + "task-1", + { name: "task-1", retry: { limit: 3 } }, + async (task) => { + if (task.attempts > 2) { + return { + bar: "foo", + }; + } + + throw new Error(`Task failed on ${task.attempts} attempt(s)`); + } + ); + }, + async (error) => { + // These should never be reached + await io.wait("wait-after-error", 5); + + await io.logger.error("This is a log error message", { + payload, + error, + }); + + return { + foo: "bar", + }; + } + ); + + try { + await io.runTask( + "task-2", + { name: "task-2", retry: { limit: 5 } }, + async (task) => { + throw new Error(`Task failed on ${task.attempts} attempt(s)`); + } + ); + } catch (error) { + if (isTriggerError(error)) { + throw error; + } + + await io.wait("wait-after-error", 5); + + await io.logger.error("This is a log error message", { + payload, + error, + }); + } return { payload, diff --git a/integrations/github/package.json b/integrations/github/package.json index baa19b76a..b2b1ad565 100644 --- a/integrations/github/package.json +++ b/integrations/github/package.json @@ -13,8 +13,8 @@ "dist/index.js.map" ], "devDependencies": { - "@octokit/webhooks-types": "^6.10.0", "@octokit/types": "^9.2.3", + "@octokit/webhooks-types": "^6.10.0", "@trigger.dev/tsconfig": "workspace:*", "@types/node": "18", "rimraf": "^3.0.2", @@ -26,6 +26,8 @@ "build:tsup": "tsup" }, "dependencies": { + "@octokit/request": "^6.2.5", + "@octokit/request-error": "^4.0.1", "@octokit/webhooks": "^10.4.0", "@trigger.dev/sdk": "workspace:^2.0.0-next.0", "octokit": "^2.0.14", diff --git a/integrations/github/src/index.ts b/integrations/github/src/index.ts index 031de868b..dcbd65665 100644 --- a/integrations/github/src/index.ts +++ b/integrations/github/src/index.ts @@ -16,10 +16,12 @@ import { Octokit } from "octokit"; import { clientFactory } from "./clientFactory"; import { createOrgEventSource, createRepoEventSource } from "./sources"; import { tasks } from "./tasks"; +import { RequestRequestOptions } from "@octokit/types"; export type GithubIntegrationOptions = { id: string; token?: string; + octokitRequest?: RequestRequestOptions; }; type GithubSources = { @@ -78,6 +80,10 @@ function createConnectionFromOptions( if (options.token) { const client = new Octokit({ auth: options.token, + request: options.octokitRequest, + retry: { + enabled: false, + }, }); return { @@ -89,7 +95,15 @@ function createConnectionFromOptions( return { usesLocalAuth: false, - clientFactory, + clientFactory: (auth) => { + return new Octokit({ + auth: auth.accessToken, + request: options.octokitRequest, + retry: { + enabled: false, + }, + }); + }, tasks, }; } diff --git a/integrations/github/src/tasks.ts b/integrations/github/src/tasks.ts index f6f37f979..eaaa4a7a8 100644 --- a/integrations/github/src/tasks.ts +++ b/integrations/github/src/tasks.ts @@ -1,6 +1,7 @@ -import { Octokit } from "octokit"; +import { RequestError } from "@octokit/request-error"; import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"; import type { AuthenticatedTask } from "@trigger.dev/sdk"; +import { Octokit } from "octokit"; type OctokitClient = InstanceType; @@ -13,11 +14,37 @@ type GithubAuthenticatedTask< GetResponseDataTypeFromEndpointMethod >; +function isRequestError(error: unknown): error is RequestError { + return typeof error === "object" && error !== null && "status" in error; +} + +function onError(error: unknown) { + if (!isRequestError(error)) { + return; + } + + // Check if this is a rate limit error + if (error.status === 403 && error.response) { + const rateLimitRemaining = error.response.headers["x-ratelimit-remaining"]; + const rateLimitReset = error.response.headers["x-ratelimit-reset"]; + + if (rateLimitRemaining === "0" && rateLimitReset) { + const resetDate = new Date(Number(rateLimitReset) * 1000); + + return { + retryAt: resetDate, + error, + }; + } + } +} + export const createIssue: GithubAuthenticatedTask< { title: string; repo: string }, OctokitClient["rest"]["issues"]["create"] > = { - run: async (params, client) => { + onError, + run: async (params, client, task) => { const [owner, repo] = params.repo.split("/"); return client.rest.issues @@ -42,6 +69,13 @@ export const createIssue: GithubAuthenticatedTask< text: params.title, }, ], + retry: { + limit: 3, + factor: 2, + minTimeoutInMs: 500, + maxTimeoutInMs: 30000, + randomize: true, + }, }; }, }; @@ -50,6 +84,7 @@ export const createIssueComment: GithubAuthenticatedTask< { body: string; repo: string; issueNumber: number }, OctokitClient["rest"]["issues"]["createComment"] > = { + onError, run: async (params, client) => { const [owner, repo] = params.repo.split("/"); @@ -84,12 +119,16 @@ export const getRepo: GithubAuthenticatedTask< { repo: string }, OctokitClient["rest"]["repos"]["get"] > = { - run: async (params, client) => { + onError, + run: async (params, client, task) => { const [owner, repo] = params.repo.split("/"); const response = await client.rest.repos.get({ owner, repo, + headers: { + "x-trigger-attempt": String(task.attempts), + }, }); return response.data; @@ -126,6 +165,7 @@ export const addIssueCommentReaction: GithubAuthenticatedTask< }, OctokitClient["rest"]["reactions"]["createForIssueComment"] > = { + onError, run: async (params, client) => { const [owner, repo] = params.repo.split("/"); @@ -195,6 +235,7 @@ export const createIssueCommentWithReaction: GithubAuthenticatedTask< }, OctokitClient["rest"]["issues"]["createComment"] > = { + onError, run: async (params, client, task, io) => { const comment = await io.runTask( `Comment on Issue #${params.issueNumber}`, @@ -255,6 +296,7 @@ export const updateWebhook: GithubAuthenticatedTask< }, OctokitClient["rest"]["repos"]["updateWebhook"] > = { + onError, run: async (params, client) => { const [owner, repo] = params.repo.split("/"); @@ -300,6 +342,7 @@ export const updateOrgWebhook: GithubAuthenticatedTask< }, OctokitClient["rest"]["orgs"]["updateWebhook"] > = { + onError, run: async (params, client) => { return client.rest.orgs .updateWebhook({ @@ -341,6 +384,7 @@ export const createWebhook: GithubAuthenticatedTask< }, OctokitClient["rest"]["repos"]["createWebhook"] > = { + onError, run: async (params, client) => { const [owner, repo] = params.repo.split("/"); @@ -384,6 +428,7 @@ export const createOrgWebhook: GithubAuthenticatedTask< }, OctokitClient["rest"]["orgs"]["createWebhook"] > = { + onError, run: async (params, client, task) => { return client.rest.orgs .createWebhook({ @@ -422,6 +467,7 @@ export const listWebhooks: GithubAuthenticatedTask< }, OctokitClient["rest"]["repos"]["listWebhooks"] > = { + onError, run: async (params, client) => { const [owner, repo] = params.repo.split("/"); @@ -452,6 +498,7 @@ export const listOrgWebhooks: GithubAuthenticatedTask< }, OctokitClient["rest"]["orgs"]["listWebhooks"] > = { + onError, run: async (params, client) => { return client.rest.orgs .listWebhooks({ diff --git a/packages/internal/src/schemas/api.ts b/packages/internal/src/schemas/api.ts index d75dc03fd..9cdb64b3f 100644 --- a/packages/internal/src/schemas/api.ts +++ b/packages/internal/src/schemas/api.ts @@ -276,6 +276,15 @@ export const RunJobResumeWithTaskSchema = z.object({ export type RunJobResumeWithTask = z.infer; +export const RunJobRetryWithTaskSchema = z.object({ + status: z.literal("RETRY_WITH_TASK"), + task: TaskSchema, + error: ErrorWithStackSchema, + retryAt: z.coerce.date(), +}); + +export type RunJobRetryWithTask = z.infer; + export const RunJobSuccessSchema = z.object({ status: z.literal("SUCCESS"), output: DeserializedJsonSchema.optional(), @@ -286,6 +295,7 @@ export type RunJobSuccess = z.infer; export const RunJobResponseSchema = z.discriminatedUnion("status", [ RunJobErrorSchema, RunJobResumeWithTaskSchema, + RunJobRetryWithTaskSchema, RunJobSuccessSchema, ]); @@ -380,6 +390,16 @@ export const RedactSchema = z.object({ paths: z.array(z.string()), }); +export const RetryOptionsSchema = z.object({ + limit: z.number().optional(), + factor: z.number().optional(), + minTimeoutInMs: z.number().optional(), + maxTimeoutInMs: z.number().optional(), + randomize: z.boolean().optional(), +}); + +export type RetryOptions = z.infer; + export const RunTaskOptionsSchema = z.object({ name: z.string(), icon: z.string().optional(), @@ -393,6 +413,7 @@ export const RunTaskOptionsSchema = z.object({ redact: RedactSchema.optional(), connectionKey: z.string().optional(), style: StyleSchema.optional(), + retry: RetryOptionsSchema.optional(), }); export type RunTaskOptions = z.input; @@ -425,6 +446,12 @@ export type CompleteTaskBodyOutput = z.infer< typeof CompleteTaskBodyInputSchema >; +export const FailTaskBodyInputSchema = z.object({ + error: ErrorWithStackSchema, +}); + +export type FailTaskBodyInput = z.infer; + export const NormalizedRequestSchema = z.object({ headers: z.record(z.string()), method: z.string(), diff --git a/packages/internal/src/schemas/errors.ts b/packages/internal/src/schemas/errors.ts index 96b10caec..eab959be8 100644 --- a/packages/internal/src/schemas/errors.ts +++ b/packages/internal/src/schemas/errors.ts @@ -2,6 +2,7 @@ import { z } from "zod"; export const ErrorWithStackSchema = z.object({ message: z.string(), + name: z.string().optional(), stack: z.string().optional(), }); diff --git a/packages/internal/src/schemas/tasks.ts b/packages/internal/src/schemas/tasks.ts index e29da9cf5..4f1246ccb 100644 --- a/packages/internal/src/schemas/tasks.ts +++ b/packages/internal/src/schemas/tasks.ts @@ -32,8 +32,11 @@ export const TaskSchema = z.object({ export const ServerTaskSchema = TaskSchema.extend({ idempotencyKey: z.string(), + attempts: z.number(), }); +export type ServerTask = z.infer; + export const CachedTaskSchema = z.object({ id: z.string(), idempotencyKey: z.string(), diff --git a/packages/trigger-sdk/src/apiClient.ts b/packages/trigger-sdk/src/apiClient.ts index aa38e4ddd..9e872044a 100644 --- a/packages/trigger-sdk/src/apiClient.ts +++ b/packages/trigger-sdk/src/apiClient.ts @@ -5,6 +5,7 @@ import { ConnectionAuthSchema, CreateRunBody, CreateRunResponseBodySchema, + FailTaskBodyInput, LogLevel, Logger, RegisterScheduleResponseBodySchema, @@ -168,6 +169,29 @@ export class ApiClient { ); } + async failTask(runId: string, id: string, body: FailTaskBodyInput) { + const apiKey = await this.#apiKey(); + + this.#logger.debug("Fail Task", { + id, + runId, + body, + }); + + return await zodfetch( + ServerTaskSchema, + `${this.#apiUrl}/api/v1/runs/${runId}/tasks/${id}/fail`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + } + ); + } + async sendEvent(event: SendEvent, options: SendEventOptions = {}) { const apiKey = await this.#apiKey(); diff --git a/packages/trigger-sdk/src/errors.ts b/packages/trigger-sdk/src/errors.ts new file mode 100644 index 000000000..fb2b331a4 --- /dev/null +++ b/packages/trigger-sdk/src/errors.ts @@ -0,0 +1,21 @@ +import { ErrorWithStack, ServerTask } from "@trigger.dev/internal"; + +export class ResumeWithTaskError { + constructor(public task: ServerTask) {} +} + +export class RetryWithTaskError { + constructor( + public cause: ErrorWithStack, + public task: ServerTask, + public retryAt: Date + ) {} +} + +export function isTriggerError( + err: unknown +): err is ResumeWithTaskError | RetryWithTaskError { + return ( + err instanceof ResumeWithTaskError || err instanceof RetryWithTaskError + ); +} diff --git a/packages/trigger-sdk/src/index.ts b/packages/trigger-sdk/src/index.ts index a61bd9094..594a30cf2 100644 --- a/packages/trigger-sdk/src/index.ts +++ b/packages/trigger-sdk/src/index.ts @@ -9,10 +9,14 @@ export * from "./triggers/notifications"; export * from "./io"; export * from "./types"; +import { ServerTask } from "@trigger.dev/internal"; import { SecureString } from "./types"; +export { isTriggerError } from "./errors"; export type { NormalizedRequest, EventFilter } from "@trigger.dev/internal"; +export type Task = ServerTask; + /* * This function is used to create a secure string that can be used in the headers of a fetch request. * It is used to prevent the string from being logged in trigger.dev. diff --git a/packages/trigger-sdk/src/integrations.ts b/packages/trigger-sdk/src/integrations.ts index 06ead14d1..992eb9d1b 100644 --- a/packages/trigger-sdk/src/integrations.ts +++ b/packages/trigger-sdk/src/integrations.ts @@ -42,6 +42,10 @@ export type AuthenticatedTask = { io: IO ) => Promise; init: (params: TParams) => RunTaskOptions; + onError?: ( + error: unknown, + task: ServerTask + ) => { retryAt: Date; error?: Error } | undefined | void; }; export function authenticatedTask(options: { diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts index d086542eb..ea16a85ec 100644 --- a/packages/trigger-sdk/src/io.ts +++ b/packages/trigger-sdk/src/io.ts @@ -2,6 +2,7 @@ import { CachedTask, ConnectionAuth, CronOptions, + ErrorWithStackSchema, IntervalOptions, LogLevel, Logger, @@ -15,23 +16,21 @@ import { import { AsyncLocalStorage } from "node:async_hooks"; import { webcrypto } from "node:crypto"; import { ApiClient } from "./apiClient"; +import { + ResumeWithTaskError, + RetryWithTaskError, + isTriggerError, +} from "./errors"; +import { createIOWithIntegrations } from "./ioWithIntegrations"; +import { calculateRetryAt } from "./retry"; import { TriggerClient } from "./triggerClient"; import { DynamicTrigger } from "./triggers/dynamic"; import { ExternalSource, ExternalSourceParams, } from "./triggers/externalSource"; -import { EventSpecification, TaskLogger, TriggerContext } from "./types"; -import { createIOWithIntegrations } from "./ioWithIntegrations"; import { DynamicSchedule } from "./triggers/scheduled"; - -export class ResumeWithTask { - constructor(public task: ServerTask) {} -} - -export class TaskError { - constructor(public cause: Error, public task: ServerTask) {} -} +import { EventSpecification, TaskLogger, TriggerContext } from "./types"; export type IOTask = ServerTask; @@ -343,12 +342,16 @@ export class IO { }); } - // TODO: investigate why errors from Github tasks are not being caught here - async runTask( + async runTask( key: string | any[], options: RunTaskOptions, - callback: (task: IOTask, io: IO) => Promise - ): Promise { + callback: (task: IOTask, io: IO) => Promise, + onError?: ( + error: unknown, + task: IOTask, + io: IO + ) => { retryAt: Date; error?: Error } | undefined | void + ): Promise { const parentId = this._taskStorage.getStore()?.taskId; if (parentId) { @@ -371,7 +374,7 @@ export class IO { cachedTask, }); - return cachedTask.output as T; + return cachedTask.output as TResult; } const task = await this._apiClient.runTask(this._id, { @@ -390,7 +393,7 @@ export class IO { this.#addToCachedTasks(task); - return task.output as T; + return task.output as TResult; } if (task.status === "ERRORED") { @@ -408,7 +411,7 @@ export class IO { task, }); - throw new ResumeWithTask(task); + throw new ResumeWithTaskError(task); } const executeTask = async () => { @@ -426,17 +429,72 @@ export class IO { return result; } catch (error) { - if (error instanceof Error) { - throw new TaskError(error, task); + if (onError) { + const onErrorResult = onError(error, task, this); + + if (onErrorResult) { + const parsedError = ErrorWithStackSchema.safeParse( + onErrorResult.error + ); + + throw new RetryWithTaskError( + parsedError.success + ? parsedError.data + : { message: "Unknown error" }, + task, + onErrorResult.retryAt + ); + } } - throw new TaskError(new Error("Unknown error"), task); + const parsedError = ErrorWithStackSchema.safeParse(error); + + if (options.retry) { + const retryAt = calculateRetryAt(options.retry, task.attempts); + + if (retryAt) { + throw new RetryWithTaskError( + parsedError.success + ? parsedError.data + : { message: "Unknown error" }, + task, + retryAt + ); + } + } + + if (parsedError.success) { + await this._apiClient.failTask(this._id, task.id, { + error: parsedError.data, + }); + } else { + await this._apiClient.failTask(this._id, task.id, { + error: { message: JSON.stringify(error), name: "Unknown Error" }, + }); + } + + throw error; } }; return this._taskStorage.run({ taskId: task.id }, executeTask); } + async try( + tryCallback: () => Promise, + catchCallback: (error: unknown) => Promise + ): Promise { + try { + return await tryCallback(); + } catch (error) { + if (isTriggerError(error)) { + throw error; + } + + return await catchCallback(error); + } + } + #addToCachedTasks(task: ServerTask) { this._cachedTasks.set(task.idempotencyKey, task); } diff --git a/packages/trigger-sdk/src/ioWithIntegrations.ts b/packages/trigger-sdk/src/ioWithIntegrations.ts index d91685532..e7c2aa399 100644 --- a/packages/trigger-sdk/src/ioWithIntegrations.ts +++ b/packages/trigger-sdk/src/ioWithIntegrations.ts @@ -1,9 +1,9 @@ import { ConnectionAuth } from "@trigger.dev/internal"; import { - TriggerIntegration, - IntegrationClient, - IOWithIntegrations, AuthenticatedTask, + IOWithIntegrations, + IntegrationClient, + TriggerIntegration, } from "./integrations"; import { IO } from "./io"; @@ -55,9 +55,14 @@ export function createIOWithIntegrations< const options = authenticatedTask.init(params); options.connectionKey = connectionKey; - return await io.runTask(key, options, async (ioTask) => { - return authenticatedTask.run(params, client, ioTask, io); - }); + return await io.runTask( + key, + options, + async (ioTask) => { + return authenticatedTask.run(params, client, ioTask, io); + }, + authenticatedTask.onError + ); }; }); } diff --git a/packages/trigger-sdk/src/retry.ts b/packages/trigger-sdk/src/retry.ts new file mode 100644 index 000000000..b7fc4e8f8 --- /dev/null +++ b/packages/trigger-sdk/src/retry.ts @@ -0,0 +1,37 @@ +import type { RetryOptions } from "@trigger.dev/internal"; + +const DEFAULT_RETRY_OPTIONS = { + limit: 5, + factor: 1.8, + minTimeoutInMs: 1000, + maxTimeoutInMs: 60000, + randomize: true, +} satisfies RetryOptions; + +export function calculateRetryAt( + retryOptions: RetryOptions, + attempts: number +): Date | undefined { + const options = { + ...DEFAULT_RETRY_OPTIONS, + ...retryOptions, + }; + + const retryCount = attempts + 1; + + if (retryCount > options.limit) { + return; + } + + const random = options.randomize ? Math.random() + 1 : 1; + + let timeoutInMs = Math.round( + random * + Math.max(options.minTimeoutInMs, 1) * + Math.pow(options.factor, Math.max(attempts - 1, 0)) + ); + + timeoutInMs = Math.min(timeoutInMs, options.maxTimeoutInMs); + + return new Date(Date.now() + timeoutInMs); +} diff --git a/packages/trigger-sdk/src/triggerClient.ts b/packages/trigger-sdk/src/triggerClient.ts index bc60b170a..39a4c9df9 100644 --- a/packages/trigger-sdk/src/triggerClient.ts +++ b/packages/trigger-sdk/src/triggerClient.ts @@ -22,7 +22,8 @@ import { SourceMetadata, } from "@trigger.dev/internal"; import { ApiClient } from "./apiClient"; -import { IO, ResumeWithTask, TaskError } from "./io"; +import { ResumeWithTaskError, RetryWithTaskError } from "./errors"; +import { IO } from "./io"; import { createIOWithIntegrations } from "./ioWithIntegrations"; import { Job } from "./job"; import { DynamicTrigger } from "./triggers/dynamic"; @@ -584,11 +585,20 @@ export class TriggerClient { return { status: "SUCCESS", output }; } catch (error) { - if (error instanceof ResumeWithTask) { + if (error instanceof ResumeWithTaskError) { return { status: "RESUME_WITH_TASK", task: error.task }; } - if (error instanceof TaskError) { + if (error instanceof RetryWithTaskError) { + return { + status: "RETRY_WITH_TASK", + task: error.task, + error: error.cause, + retryAt: error.retryAt, + }; + } + + if (error instanceof RetryWithTaskError) { const errorWithStack = ErrorWithStackSchema.safeParse(error.cause); if (errorWithStack.success) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93a176f95..9e2dffa5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -460,9 +460,12 @@ importers: '@trigger.dev/nextjs': workspace:* '@trigger.dev/sdk': workspace:* '@types/node': 18.15.13 + '@types/node-fetch': 2.6.x '@types/react': ^18.0.21 '@types/react-dom': ^18.0.6 + msw: ^0.47.0 next: 13.3.1 + node-fetch: 2.6.x react: ^18.2.0 react-dom: ^18.2.0 ts-loader: ^9.4.2 @@ -477,11 +480,14 @@ importers: '@types/react': 18.0.26 '@types/react-dom': 18.0.10 next: 13.3.1_biqbaboplfbrettd7655fr4n2y + node-fetch: 2.6.11 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 typescript: 5.0.4 zod: 3.20.2 devDependencies: + '@types/node-fetch': 2.6.2 + msw: 0.47.4_typescript@5.0.4 ts-loader: 9.4.2_dmnk2hichoesbonenkf5yk7fmy webpack: 5.80.0 @@ -516,6 +522,8 @@ importers: integrations/github: specifiers: + '@octokit/request': ^6.2.5 + '@octokit/request-error': ^4.0.1 '@octokit/types': ^9.2.3 '@octokit/webhooks': ^10.4.0 '@octokit/webhooks-types': ^6.10.0 @@ -527,6 +535,8 @@ importers: tsup: ^6.5.0 zod: ^3.20.2 dependencies: + '@octokit/request': 6.2.5 + '@octokit/request-error': 4.0.1 '@octokit/webhooks': 10.5.1 '@trigger.dev/sdk': link:../../packages/trigger-sdk octokit: 2.0.14 @@ -7059,7 +7069,7 @@ packages: dependencies: '@octokit/auth-oauth-app': 5.0.5 '@octokit/auth-oauth-user': 2.1.1 - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/request-error': 3.0.3 '@octokit/types': 9.2.3 '@types/lru-cache': 5.1.1 @@ -7077,7 +7087,7 @@ packages: dependencies: '@octokit/auth-oauth-device': 4.0.4 '@octokit/auth-oauth-user': 2.1.1 - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/types': 9.2.3 '@types/btoa-lite': 1.0.0 btoa-lite: 1.0.0 @@ -7091,7 +7101,7 @@ packages: engines: {node: '>= 14'} dependencies: '@octokit/oauth-methods': 2.0.5 - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/types': 9.2.3 universal-user-agent: 6.0.0 transitivePeerDependencies: @@ -7104,7 +7114,7 @@ packages: dependencies: '@octokit/auth-oauth-device': 4.0.4 '@octokit/oauth-methods': 2.0.5 - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/types': 9.2.3 btoa-lite: 1.0.0 universal-user-agent: 6.0.0 @@ -7133,7 +7143,7 @@ packages: dependencies: '@octokit/auth-token': 3.0.3 '@octokit/graphql': 5.0.5 - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/request-error': 3.0.3 '@octokit/types': 9.2.3 before-after-hook: 2.2.3 @@ -7155,7 +7165,7 @@ packages: resolution: {integrity: sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==} engines: {node: '>= 14'} dependencies: - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/types': 9.2.3 universal-user-agent: 6.0.0 transitivePeerDependencies: @@ -7189,7 +7199,7 @@ packages: engines: {node: '>= 14'} dependencies: '@octokit/oauth-authorization-url': 5.0.0 - '@octokit/request': 6.2.3 + '@octokit/request': 6.2.5 '@octokit/request-error': 3.0.3 '@octokit/types': 9.2.3 btoa-lite: 1.0.0 @@ -7255,15 +7265,24 @@ packages: once: 1.4.0 dev: false - /@octokit/request/6.2.3: - resolution: {integrity: sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==} + /@octokit/request-error/4.0.1: + resolution: {integrity: sha512-DBTkqzs0K6SlK1gRaQ6A6yOnKKkbVy8n/A9E7Es5qYONIxBghqiETPqWhG9l7qvWgp8v3sDkB8vlV2AAX1N6gw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/types': 9.2.3 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request/6.2.5: + resolution: {integrity: sha512-z83E8UIlPNaJUsXpjD8E0V5o/5f+vJJNbNcBwVZsX3/vC650U41cOkTLjq4PKk9BYonQGOnx7N17gvLyNjgGcQ==} engines: {node: '>= 14'} dependencies: '@octokit/endpoint': 7.0.5 '@octokit/request-error': 3.0.3 '@octokit/types': 9.2.3 is-plain-object: 5.0.0 - node-fetch: 2.6.7 + node-fetch: 2.6.11 universal-user-agent: 6.0.0 transitivePeerDependencies: - encoding @@ -10665,7 +10684,7 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.14.0 + '@types/node': 18.15.13 form-data: 3.0.1 dev: true @@ -18795,6 +18814,43 @@ packages: - supports-color dev: true + /msw/0.47.4_typescript@5.0.4: + resolution: {integrity: sha512-Psftt8Yfl0+l+qqg9OlmKEsxF8S/vtda0CmlR6y8wTaWrMMzuCDa55n2hEGC0ZRDwuV6FFWc/4CjoDsBpATKBw==} + engines: {node: '>=14'} + hasBin: true + requiresBuild: true + peerDependencies: + typescript: '>= 4.2.x <= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@mswjs/cookies': 0.2.2 + '@mswjs/interceptors': 0.17.6 + '@open-draft/until': 1.0.3 + '@types/cookie': 0.4.1 + '@types/js-levenshtein': 1.1.1 + chalk: 4.1.1 + chokidar: 3.5.3 + cookie: 0.4.2 + graphql: 16.6.0 + headers-polyfill: 3.1.2 + inquirer: 8.2.5 + is-node-process: 1.0.1 + js-levenshtein: 1.1.6 + node-fetch: 2.6.11 + outvariant: 1.3.0 + path-to-regexp: 6.2.1 + statuses: 2.0.1 + strict-event-emitter: 0.2.8 + type-fest: 2.19.0 + typescript: 5.0.4 + yargs: 17.6.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: true + /mute-stream/0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}