diff --git a/.changeset/new-rivers-tell.md b/.changeset/new-rivers-tell.md new file mode 100644 index 000000000..4920e868c --- /dev/null +++ b/.changeset/new-rivers-tell.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/sdk": patch +"@trigger.dev/core": patch +--- + +Added replayRun function to the SDK diff --git a/.changeset/tiny-doors-type.md b/.changeset/tiny-doors-type.md new file mode 100644 index 000000000..3c58eb9a3 --- /dev/null +++ b/.changeset/tiny-doors-type.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/sdk": patch +"@trigger.dev/core": patch +--- + +Added cancelRun to the SDK diff --git a/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx index 97f031d33..c16502d06 100644 --- a/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/CancelRunDialog.tsx @@ -1,5 +1,5 @@ import { StopCircleIcon } from "@heroicons/react/20/solid"; -import { useFetcher } from "@remix-run/react"; +import { Form, useFetcher, useNavigation } from "@remix-run/react"; import { Button } from "~/components/primitives/Buttons"; import { DialogContent, @@ -14,29 +14,32 @@ type CancelRunDialogProps = { }; export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialogProps) { - const cancelFetcher = useFetcher(); + const navigation = useNavigation(); + + const formAction = `/resources/taskruns/${runFriendlyId}/cancel`; + const isLoading = navigation.formAction === formAction; return ( - + Cancel this run? Canceling a run will stop execution. If you want to run this later you will have to replay the entire run with the original payload. - +
- +
); diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx new file mode 100644 index 000000000..74b2ff017 --- /dev/null +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -0,0 +1,44 @@ +import { ArrowPathIcon } from "@heroicons/react/20/solid"; +import { Form, useFetcher, useNavigation } from "@remix-run/react"; +import { Button } from "~/components/primitives/Buttons"; +import { + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, +} from "~/components/primitives/Dialog"; + +type ReplayRunDialogProps = { + runFriendlyId: string; + failedRedirect: string; +}; + +export function ReplayRunDialog({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) { + const navigation = useNavigation(); + + const formAction = `/resources/taskruns/${runFriendlyId}/replay`; + const isLoading = navigation.formAction === formAction; + + return ( + + Replay this run? + + Replaying a run will create a new run with the same payload and environment as the original. + + +
+ + +
+
+
+ ); +} diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index fb58765dc..bc2c7bf1b 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -24,10 +24,11 @@ import { formatDuration } from "@trigger.dev/core/v3"; import { TaskRunStatusCombo } from "./TaskRunStatus"; import { useEnvironments } from "~/hooks/useEnvironments"; import { Button, LinkButton } from "~/components/primitives/Buttons"; -import { StopCircleIcon } from "@heroicons/react/20/solid"; +import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid"; import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; import { CancelRunDialog } from "./CancelRunDialog"; import { useLocation } from "@remix-run/react"; +import { ReplayRunDialog } from "./ReplayRunDialog"; type RunsTableProps = { total: number; @@ -49,7 +50,6 @@ export function TaskRunsTable({ }: RunsTableProps) { const organization = useOrganization(); const project = useProject(); - const location = useLocation(); return ( @@ -110,23 +110,7 @@ export function TaskRunsTable({ {run.createdAt ? : "–"} - {run.isCancellable ? ( - - - - - - - - - ) : ( - {""} - )} + ); }) @@ -144,6 +128,43 @@ export function TaskRunsTable({ ); } +function RunActionsCell({ run, path }: { run: RunListItem; path: string }) { + const location = useLocation(); + + if (!run.isCancellable && !run.isReplayable) return {""}; + + return ( + + {run.isCancellable && ( + + + + + + + )} + {run.isReplayable && ( + + + + + + + )} + + ); +} + function NoRuns({ title }: { title: string }) { return (
diff --git a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts index 144fe2e1c..27e3706f8 100644 --- a/apps/webapp/app/presenters/v3/RunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunListPresenter.server.ts @@ -222,6 +222,7 @@ export class RunListPresenter { version: run.version, taskIdentifier: run.taskIdentifier, attempts: Number(run.attempts), + isReplayable: true, isCancellable: CANCELLABLE_STATUSES.includes(run.status), environment: { type: environment.type, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx index 8ae95dd16..5bcd94e47 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx @@ -1,4 +1,9 @@ -import { CloudArrowDownIcon, QueueListIcon, StopCircleIcon } from "@heroicons/react/20/solid"; +import { + ArrowPathIcon, + CloudArrowDownIcon, + QueueListIcon, + StopCircleIcon, +} from "@heroicons/react/20/solid"; import { useParams } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationNanoseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3"; @@ -14,6 +19,7 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { Property, PropertyTable } from "~/components/primitives/PropertyTable"; import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; +import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog"; import { RunIcon } from "~/components/runs/v3/RunIcon"; import { SpanEvents } from "~/components/runs/v3/SpanEvents"; import { SpanTitle } from "~/components/runs/v3/SpanTitle"; @@ -22,7 +28,7 @@ import { TaskRunAttemptStatusCombo } from "~/components/runs/v3/TaskRunAttemptSt import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithErrorMessage } from "~/models/message.server"; -import { SpanPresenter } from "~/presenters/v3/SpanPresenter.server"; +import { Span, SpanPresenter } from "~/presenters/v3/SpanPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { v3RunPath, v3RunSpanPath, v3SpanParamsSchema, v3TraceSpanPath } from "~/utils/pathBuilder"; @@ -188,24 +194,7 @@ export default function Page() { )}
- {event.isPartial && runParam && ( - - - - - - - )} +
) : null} @@ -213,6 +202,54 @@ export default function Page() { ); } +function RunActionButtons({ span }: { span: Span }) { + const organization = useOrganization(); + const project = useProject(); + const { runParam } = useParams(); + + if (!runParam) return null; + + if (span.isPartial) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} + function PacketDisplay({ data, dataType, diff --git a/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts b/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts new file mode 100644 index 000000000..d2b6230aa --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts @@ -0,0 +1,72 @@ +import type { ActionFunctionArgs } from "@remix-run/server-runtime"; +import { json } from "@remix-run/server-runtime"; +import { PrismaErrorSchema, prisma } from "~/db.server"; +import { z } from "zod"; +import { authenticateApiRequest } from "~/services/apiAuth.server"; +import { CancelRunService } from "~/services/runs/cancelRun.server"; +import { ApiRunPresenter } from "~/presenters/ApiRunPresenter.server"; +import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server"; +import { logger } from "~/services/logger.server"; + +const ParamsSchema = z.object({ + /* This is the run friendly ID */ + runParam: z.string(), +}); + +export async function action({ request, params }: ActionFunctionArgs) { + // Ensure this is a POST request + if (request.method.toUpperCase() !== "POST") { + return { status: 405, body: "Method Not Allowed" }; + } + + // Authenticate the request + const authenticationResult = await authenticateApiRequest(request); + if (!authenticationResult) { + return json({ error: "Invalid or Missing API Key" }, { status: 401 }); + } + + const parsed = ParamsSchema.safeParse(params); + if (!parsed.success) { + return json({ error: "Invalid or missing run ID" }, { status: 400 }); + } + + const { runParam } = parsed.data; + + try { + const taskRun = await prisma.taskRun.findUnique({ + where: { + friendlyId: runParam, + }, + }); + + if (!taskRun) { + return json({ error: "Run not found" }, { status: 404 }); + } + + const service = new ReplayTaskRunService(); + const newRun = await service.call(taskRun); + + if (!newRun) { + return json({ error: "Failed to create new run" }, { status: 400 }); + } + + return json({ + id: newRun?.friendlyId, + }); + } catch (error) { + if (error instanceof Error) { + logger.error("Failed to replay run", { + error: { + name: error.name, + message: error.message, + stack: error.stack, + }, + run: runParam, + }); + return json({ error: error.message }, { status: 400 }); + } else { + logger.error("Failed to replay run", { error: JSON.stringify(error), run: runParam }); + return json({ error: JSON.stringify(error) }, { status: 400 }); + } + } +} diff --git a/apps/webapp/app/routes/api.v2.runs.$runParam.cancel.ts b/apps/webapp/app/routes/api.v2.runs.$runParam.cancel.ts index d23fad9e2..8bfe68d94 100644 --- a/apps/webapp/app/routes/api.v2.runs.$runParam.cancel.ts +++ b/apps/webapp/app/routes/api.v2.runs.$runParam.cancel.ts @@ -25,7 +25,7 @@ export async function action({ request, params }: ActionFunctionArgs) { const parsed = ParamsSchema.safeParse(params); if (!parsed.success) { - return json({ error: "Invalid or Missing runId" }, { status: 400 }); + return json({ error: "Invalid or Missing run id" }, { status: 400 }); } const { runParam } = parsed.data; diff --git a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts new file mode 100644 index 000000000..19caf4a07 --- /dev/null +++ b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts @@ -0,0 +1,85 @@ +import { parse } from "@conform-to/zod"; +import { ActionFunction, json } from "@remix-run/node"; +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; +import { logger } from "~/services/logger.server"; +import { v3RunPath } from "~/utils/pathBuilder"; +import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server"; + +const FormSchema = z.object({ + failedRedirect: z.string(), +}); + +const ParamSchema = z.object({ + runParam: z.string(), +}); + +export const action: ActionFunction = async ({ request, params }) => { + const { runParam } = ParamSchema.parse(params); + + const formData = await request.formData(); + const submission = parse(formData, { schema: FormSchema }); + + if (!submission.value) { + return json(submission); + } + + try { + const taskRun = await prisma.taskRun.findUnique({ + where: { + friendlyId: runParam, + }, + include: { + project: { + include: { + organization: true, + }, + }, + }, + }); + + if (!taskRun) { + return redirectWithErrorMessage(submission.value.failedRedirect, request, "Run not found"); + } + + const replayRunService = new ReplayTaskRunService(); + const newRun = await replayRunService.call(taskRun); + + if (!newRun) { + return redirectWithErrorMessage( + submission.value.failedRedirect, + request, + "Failed to replay run" + ); + } + + const runPath = v3RunPath( + { + slug: taskRun.project.organization.slug, + }, + { slug: taskRun.project.slug }, + { friendlyId: newRun.friendlyId } + ); + + return redirectWithSuccessMessage(runPath, request, `Replaying run`); + } catch (error) { + if (error instanceof Error) { + logger.error("Failed to replay run", { + error: { + name: error.name, + message: error.message, + stack: error.stack, + }, + }); + return redirectWithErrorMessage(submission.value.failedRedirect, request, error.message); + } else { + logger.error("Failed to replay run", { error }); + return redirectWithErrorMessage( + submission.value.failedRedirect, + request, + JSON.stringify(error) + ); + } + } +}; diff --git a/apps/webapp/app/v3/services/replayTaskRun.server.ts b/apps/webapp/app/v3/services/replayTaskRun.server.ts new file mode 100644 index 000000000..1a281df79 --- /dev/null +++ b/apps/webapp/app/v3/services/replayTaskRun.server.ts @@ -0,0 +1,53 @@ +import { conditionallyImportPacket, parsePacket } from "@trigger.dev/core/v3"; +import { Prisma, TaskRun } from "@trigger.dev/database"; +import { findEnvironmentById } from "~/models/runtimeEnvironment.server"; +import { logger } from "~/services/logger.server"; +import { BaseService } from "./baseService.server"; +import { TriggerTaskService } from "./triggerTask.server"; + +type ExtendedTaskRunAttempt = Prisma.TaskRunAttemptGetPayload<{ + include: { + runtimeEnvironment: true; + backgroundWorker: true; + }; +}>; + +export class ReplayTaskRunService extends BaseService { + public async call(existingTaskRun: TaskRun) { + const authenticatedEnvironment = await findEnvironmentById( + existingTaskRun.runtimeEnvironmentId + ); + if (!authenticatedEnvironment) { + return; + } + + logger.info("Replaying task run", { + taskRunId: existingTaskRun.id, + taskRunFriendlyId: existingTaskRun.friendlyId, + }); + + const payloadPacket = await conditionallyImportPacket({ + data: existingTaskRun.payload, + dataType: existingTaskRun.payloadType, + }); + const parsedPayload = await parsePacket(payloadPacket); + + logger.info("Replaying task run payload", { + taskRunId: existingTaskRun.id, + taskRunFriendlyId: existingTaskRun.friendlyId, + payloadPacketType: payloadPacket.dataType, + }); + + const triggerTaskService = new TriggerTaskService(); + return await triggerTaskService.call(existingTaskRun.taskIdentifier, authenticatedEnvironment, { + payload: parsedPayload, + options: { + queue: { + name: existingTaskRun.queue, + }, + concurrencyKey: existingTaskRun.concurrencyKey ?? undefined, + test: existingTaskRun.isTest, + }, + }); + } +} diff --git a/docs/mint.json b/docs/mint.json index b64d6bd01..c492ce574 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -1,7 +1,7 @@ { "$schema": "https://mintlify.com/schema.json", "name": "Trigger.dev", - "openapi": ["/openapi.yml"], + "openapi": ["/openapi.yml", "/v3-openapi.json"], "versions": ["v3 (Developer Preview)", "v2"], "logo": { "dark": "/logo/dark.png", @@ -586,6 +586,16 @@ "v3/reference-cli-build", "v3/reference-cli-who-am-i" ] + }, + { + "group": "Management API", + "pages": [ + "v3/management-start-run", + "v3/management-get-run", + "v3/management-get-runs", + "v3/management-replay-run", + "v3/management-cancel-run" + ] } ] }, diff --git a/docs/v3-openapi.json b/docs/v3-openapi.json new file mode 100644 index 000000000..ced681cd4 --- /dev/null +++ b/docs/v3-openapi.json @@ -0,0 +1,216 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Trigger.dev v3 REST API", + "description": "The REST API lets you trigger and manage runs on Trigger.dev. You can trigger a run, get the status of a run, and get the results of a run. ", + "version": "2024-04" + }, + "servers": [ + { + "url": "https://api.trigger.dev", + "description": "Trigger.dev API" + } + ], + "paths": { + "/api/v1/runs/{run_id}/replay": { + "post": { + "description": "Creates a new run with the same payload and options as the original run.", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of an existing run. When you trigger a run you will get an id in the response." + } + ], + "responses": { + "200": { + "description": "Successful request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the new run." + } + } + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["Invalid or missing run ID", "Failed to create new run"] + } + } + } + } + } + }, + "401": { + "description": "Unauthorized request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["Invalid or Missing API key"] + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["Run not found"] + } + } + } + } + } + } + }, + "tags": ["run"], + "security": [{ "bearerAuth": [] }], + "operationId": "replay_run_v1", + "x-codeSamples": [ + { + "lang": "typescript", + "source": "const handle = await runs.replay(existingRun.id);" + }, + { + "lang": "sh", + "source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/runs/{run_id}/replay \\\n\t--header 'Authorization: Bearer '" + } + ] + } + }, + "/api/v1/runs/{run_id}/cancel": { + "post": { + "description": "Cancels a run.", + "parameters": [ + { + "in": "path", + "name": "run_id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The ID of an existing run. When you trigger a run you will get an id in the response." + } + ], + "responses": { + "200": { + "description": "Successful request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Confirmation message that the run was canceled." + } + } + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["Invalid or missing run ID", "Failed to create new run"] + } + } + } + } + } + }, + "401": { + "description": "Unauthorized request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["Invalid or Missing API key"] + } + } + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["Run not found"] + } + } + } + } + } + } + }, + "tags": ["run"], + "security": [{ "bearerAuth": [] }], + "operationId": "replay_run_v1", + "x-codeSamples": [ + { + "lang": "typescript", + "source": "await runs.cancel(existingRun.id);" + }, + { + "lang": "sh", + "source": "curl --request POST \\\n\t--url https://api.trigger.dev/api/v1/runs/{run_id}/cancel \\\n\t--header 'Authorization: Bearer '" + } + ] + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "Use your Secret API key in the form 'Bearer ' (without the quotation marks)" + } + } + }, + "security": [{ "bearerAuth": [] }] +} diff --git a/docs/v3/management-cancel-run.mdx b/docs/v3/management-cancel-run.mdx new file mode 100644 index 000000000..d17ca453d --- /dev/null +++ b/docs/v3/management-cancel-run.mdx @@ -0,0 +1,4 @@ +--- +title: "Cancel run" +openapi: "v3-openapi POST /api/v1/runs/{run_id}/cancel" +--- diff --git a/docs/v3/management-get-run.mdx b/docs/v3/management-get-run.mdx new file mode 100644 index 000000000..c74c5ecb5 --- /dev/null +++ b/docs/v3/management-get-run.mdx @@ -0,0 +1,6 @@ +--- +title: "Get run" +description: "Get a run using the Task id." +--- + + diff --git a/docs/v3/management-get-runs.mdx b/docs/v3/management-get-runs.mdx new file mode 100644 index 000000000..887567ac4 --- /dev/null +++ b/docs/v3/management-get-runs.mdx @@ -0,0 +1,6 @@ +--- +title: "Get runs" +description: "Get runs using a Task id." +--- + + diff --git a/docs/v3/management-replay-run.mdx b/docs/v3/management-replay-run.mdx new file mode 100644 index 000000000..8de9a1921 --- /dev/null +++ b/docs/v3/management-replay-run.mdx @@ -0,0 +1,4 @@ +--- +title: "Replay run" +openapi: "v3-openapi POST /api/v1/runs/{run_id}/replay" +--- diff --git a/docs/v3/management-start-run.mdx b/docs/v3/management-start-run.mdx new file mode 100644 index 000000000..3cc8f7fe0 --- /dev/null +++ b/docs/v3/management-start-run.mdx @@ -0,0 +1,6 @@ +--- +title: "Start run" +description: "Start a run using the Task id, payload and options." +--- + + diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index ce80dfc6e..29d8526ad 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -9,7 +9,10 @@ import { BatchTriggerTaskRequestBody, BatchTriggerTaskResponse, CreateUploadPayloadUrlResponseBody, + ReplayRunResponse, + CanceledRunResponse, } from "../schemas"; +import { z } from "zod"; export type TriggerOptions = { spanParentAsLink?: boolean; @@ -88,6 +91,30 @@ export class ApiClient { ); } + replayRun(runId: string) { + return zodfetch( + ReplayRunResponse, + `${this.baseUrl}/api/v1/runs/${runId}/replay`, + { + method: "POST", + headers: this.#getHeaders(false), + }, + zodFetchOptions + ); + } + + cancelRun(runId: string) { + return zodfetch( + CanceledRunResponse, + `${this.baseUrl}/api/v2/runs/${runId}/cancel`, + { + method: "POST", + headers: this.#getHeaders(false), + }, + zodFetchOptions + ); + } + #getHeaders(spanParentAsLink: boolean) { const headers: Record = { "Content-Type": "application/json", diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 372e2c660..d46e69f55 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -198,3 +198,15 @@ export const CreateUploadPayloadUrlResponseBody = z.object({ }); export type CreateUploadPayloadUrlResponseBody = z.infer; + +export const ReplayRunResponse = z.object({ + id: z.string(), +}); + +export type ReplayRunResponse = z.infer; + +export const CanceledRunResponse = z.object({ + message: z.string(), +}); + +export type CanceledRunResponse = z.infer; diff --git a/packages/trigger-sdk/src/v3/index.ts b/packages/trigger-sdk/src/v3/index.ts index c405fddea..e90e3ffff 100644 --- a/packages/trigger-sdk/src/v3/index.ts +++ b/packages/trigger-sdk/src/v3/index.ts @@ -8,3 +8,5 @@ import type { Context } from "./shared"; export type { Context }; export { logger, type LogLevel } from "@trigger.dev/core/v3"; + +export { runs } from "./management"; diff --git a/packages/trigger-sdk/src/v3/management.ts b/packages/trigger-sdk/src/v3/management.ts new file mode 100644 index 000000000..d995e4e91 --- /dev/null +++ b/packages/trigger-sdk/src/v3/management.ts @@ -0,0 +1,39 @@ +import { CanceledRunResponse, ReplayRunResponse, apiClientManager } from "@trigger.dev/core/v3"; +import { apiClientMissingError } from "./shared"; + +export const runs = { + replay: replayRun, + cancel: cancelRun, +}; + +async function replayRun(runId: string): Promise { + const apiClient = apiClientManager.client; + + if (!apiClient) { + throw apiClientMissingError(); + } + + const response = await apiClient.replayRun(runId); + + if (!response.ok) { + throw new Error(response.error); + } + + return response.data; +} + +async function cancelRun(runId: string): Promise { + const apiClient = apiClientManager.client; + + if (!apiClient) { + throw apiClientMissingError(); + } + + const response = await apiClient.cancelRun(runId); + + if (!response.ok) { + throw new Error(response.error); + } + + return response.data; +} diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index ef73efbc3..8a1cd5e71 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -562,7 +562,7 @@ async function handleTaskRunExecutionResult( } } -function apiClientMissingError() { +export function apiClientMissingError() { const hasBaseUrl = !!apiClientManager.baseURL; const hasAccessToken = !!apiClientManager.accessToken; if (!hasBaseUrl && !hasAccessToken) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1bbf1384..86e6760a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3071,6 +3071,9 @@ importers: '@trigger.dev/sdk': specifier: workspace:^3.0.0-beta.0 version: link:../../packages/trigger-sdk + dotenv: + specifier: ^16.4.5 + version: 16.4.5 execa: specifier: ^8.0.1 version: 8.0.1 @@ -3096,6 +3099,9 @@ importers: trigger.dev: specifier: workspace:* version: link:../../packages/cli-v3 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.4.2)(typescript@5.3.3) typescript: specifier: ^5.3.0 version: 5.3.3 @@ -17875,18 +17881,6 @@ packages: dependencies: ms: 2.1.2 - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: false - /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -18303,6 +18297,11 @@ packages: resolution: {integrity: sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==} engines: {node: '>=12'} + /dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + dev: false + /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} @@ -19362,7 +19361,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.42.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.42.0)(typescript@5.3.3) debug: 3.2.7(supports-color@5.5.0) eslint: 8.42.0 eslint-import-resolver-node: 0.3.7 @@ -19505,7 +19504,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.42.0)(typescript@5.0.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.42.0)(typescript@5.3.3) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 @@ -24992,7 +24991,7 @@ packages: hasBin: true dependencies: acorn: 8.10.0 - acorn-walk: 8.2.0 + acorn-walk: 8.3.2 capnp-ts: 0.7.0 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 @@ -25015,7 +25014,7 @@ packages: hasBin: true dependencies: acorn: 8.10.0 - acorn-walk: 8.2.0 + acorn-walk: 8.3.2 capnp-ts: 0.7.0 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 @@ -25038,7 +25037,7 @@ packages: hasBin: true dependencies: acorn: 8.10.0 - acorn-walk: 8.2.0 + acorn-walk: 8.3.2 capnp-ts: 0.7.0 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 @@ -28941,7 +28940,7 @@ packages: resolution: {integrity: sha512-OScOjQjrrjhAdFpQmnkE/qbIBGCRFhQB/YaJhcC3CPOlmhe7llnW46Ac1J5+EjcNXOTnDdpF96Erw/yedsGksQ==} engines: {node: '>=8.6.0'} dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@8.1.1) module-details-from-path: 1.0.3 resolve: 1.22.4 transitivePeerDependencies: @@ -31374,6 +31373,37 @@ packages: yn: 3.1.1 dev: true + /ts-node@10.9.2(@types/node@20.4.2)(typescript@5.3.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 20.4.2 + acorn: 8.10.0 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-poet@6.6.0: resolution: {integrity: sha512-4vEH/wkhcjRPFOdBwIh9ItO6jOoumVLRF4aABDX5JSNEubSqwOulihxQPqai+OkuygJm3WYMInxXQX4QwVNMuw==} dependencies: diff --git a/references/v3-catalog/package.json b/references/v3-catalog/package.json index fe7e48fa4..b88b4dc84 100644 --- a/references/v3-catalog/package.json +++ b/references/v3-catalog/package.json @@ -3,7 +3,8 @@ "version": "0.1.0", "private": true, "scripts": { - "dev:trigger": "triggerdev dev" + "dev:trigger": "triggerdev dev", + "management": "ts-node ./src/management.ts" }, "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", @@ -13,6 +14,7 @@ "@traceloop/instrumentation-openai": "^0.3.9", "@trigger.dev/core": "workspace:^3.0.0-beta.0", "@trigger.dev/sdk": "workspace:^3.0.0-beta.0", + "dotenv": "^16.4.5", "execa": "^8.0.1", "msw": "^2.2.1", "openai": "^4.28.0", @@ -23,6 +25,7 @@ "@trigger.dev/tsconfig": "workspace:*", "@types/node": "20.4.2", "trigger.dev": "workspace:*", + "ts-node": "^10.9.2", "typescript": "^5.3.0" } } diff --git a/references/v3-catalog/src/management.ts b/references/v3-catalog/src/management.ts new file mode 100644 index 000000000..fc60c4d18 --- /dev/null +++ b/references/v3-catalog/src/management.ts @@ -0,0 +1,16 @@ +import { runs } from "@trigger.dev/sdk/v3"; +import { simpleChildTask } from "./trigger/subtasks"; +import dotenv from "dotenv"; + +dotenv.config(); + +export async function run() { + const run = await simpleChildTask.trigger({ payload: { message: "Hello, World!" } }); + const canceled = await runs.cancel(run.id); + console.log("canceled run", canceled); + + const replayed = await runs.replay(run.id); + console.log("replayed run", replayed); +} + +run();