Skip to content

Commit 77a979f

Browse files
committed
Expose endpoint to start deployments
1 parent 965cd22 commit 77a979f

File tree

3 files changed

+135
-0
lines changed

3 files changed

+135
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
2+
import { StartDeploymentRequestBody } from "@trigger.dev/core/v3";
3+
import { z } from "zod";
4+
import { authenticateRequest } from "~/services/apiAuth.server";
5+
import { logger } from "~/services/logger.server";
6+
import { DeploymentService } from "~/v3/services/deployment.server";
7+
8+
const ParamsSchema = z.object({
9+
deploymentId: z.string(),
10+
});
11+
12+
export async function action({ request, params }: ActionFunctionArgs) {
13+
if (request.method.toUpperCase() !== "POST") {
14+
return json({ error: "Method Not Allowed" }, { status: 405 });
15+
}
16+
17+
const parsedParams = ParamsSchema.safeParse(params);
18+
19+
if (!parsedParams.success) {
20+
return json({ error: "Invalid params" }, { status: 400 });
21+
}
22+
23+
const authenticationResult = await authenticateRequest(request, {
24+
apiKey: true,
25+
organizationAccessToken: false,
26+
personalAccessToken: false,
27+
});
28+
29+
if (!authenticationResult || !authenticationResult.result.ok) {
30+
logger.info("Invalid or missing api key", { url: request.url });
31+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
32+
}
33+
34+
const { environment: authenticatedEnv } = authenticationResult.result;
35+
const { deploymentId } = parsedParams.data;
36+
37+
const rawBody = await request.json();
38+
const body = StartDeploymentRequestBody.safeParse(rawBody);
39+
40+
if (!body.success) {
41+
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
42+
}
43+
44+
const deploymentService = new DeploymentService();
45+
46+
const result = await deploymentService.startDeployment(authenticatedEnv, deploymentId, body.data);
47+
return result.match(
48+
() => {
49+
return json(null, { status: 204 });
50+
},
51+
(error) => {
52+
switch (error.type) {
53+
case "deployment_not_found":
54+
return json({ error: "Deployment not found" }, { status: 404 });
55+
case "deployment_not_pending":
56+
return json({ error: "Deployment is not pending" }, { status: 409 });
57+
case "other":
58+
default:
59+
error.type satisfies "other";
60+
return json({ error: "Internal server error" }, { status: 500 });
61+
}
62+
}
63+
);
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { type AuthenticatedEnvironment } from "~/services/apiAuth.server";
2+
import { BaseService } from "./baseService.server";
3+
import { errAsync, fromPromise, okAsync } from "neverthrow";
4+
import { type WorkerDeployment } from "@trigger.dev/database";
5+
import { type GitMeta } from "@trigger.dev/core/v3";
6+
7+
export class DeploymentService extends BaseService {
8+
public async startDeployment(
9+
authenticatedEnv: AuthenticatedEnvironment,
10+
friendlyId: string,
11+
updates: Partial<Pick<WorkerDeployment, "contentHash" | "runtime"> & { git: GitMeta }>
12+
) {
13+
const getDeployment = () =>
14+
fromPromise(
15+
this._prisma.workerDeployment.findFirst({
16+
where: {
17+
friendlyId,
18+
environmentId: authenticatedEnv.id,
19+
},
20+
select: {
21+
status: true,
22+
id: true,
23+
},
24+
}),
25+
(error) => ({
26+
type: "other" as const,
27+
cause: error,
28+
})
29+
).andThen((deployment) => {
30+
if (!deployment) {
31+
return errAsync({ type: "deployment_not_found" as const });
32+
}
33+
return okAsync(deployment);
34+
});
35+
36+
const validateDeployment = (deployment: Pick<WorkerDeployment, "id" | "status">) => {
37+
if (deployment.status !== "PENDING") {
38+
return errAsync({ type: "deployment_not_pending" as const });
39+
}
40+
41+
return okAsync(deployment);
42+
};
43+
44+
const updateDeployment = (deployment: Pick<WorkerDeployment, "id">) =>
45+
fromPromise(
46+
this._prisma.workerDeployment.updateMany({
47+
where: { id: deployment.id, status: "PENDING" }, // status could've changed in the meantime, we're not locking the row
48+
data: { ...updates, status: "BUILDING" },
49+
}),
50+
(error) => ({
51+
type: "other" as const,
52+
cause: error,
53+
})
54+
).andThen((result) => {
55+
if (result.count === 0) {
56+
return errAsync({ type: "deployment_not_pending" as const });
57+
}
58+
return okAsync(undefined);
59+
});
60+
61+
return getDeployment().andThen(validateDeployment).andThen(updateDeployment);
62+
}
63+
}

packages/core/src/v3/schemas/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,14 @@ export const FinalizeDeploymentRequestBody = z.object({
377377

378378
export type FinalizeDeploymentRequestBody = z.infer<typeof FinalizeDeploymentRequestBody>;
379379

380+
export const StartDeploymentRequestBody = z.object({
381+
contentHash: z.string().optional(),
382+
gitMeta: GitMeta.optional(),
383+
runtime: z.string().optional(),
384+
});
385+
386+
export type StartDeploymentRequestBody = z.infer<typeof StartDeploymentRequestBody>;
387+
380388
export const ExternalBuildData = z.object({
381389
buildId: z.string(),
382390
buildToken: z.string(),

0 commit comments

Comments
 (0)