diff --git a/.changeset/kind-kids-teach.md b/.changeset/kind-kids-teach.md
new file mode 100644
index 0000000000..65e19fbe96
--- /dev/null
+++ b/.changeset/kind-kids-teach.md
@@ -0,0 +1,6 @@
+---
+"trigger.dev": patch
+"@trigger.dev/core": patch
+---
+
+Added INSTALLING status to the deployment status enum.
diff --git a/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx b/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx
index 5f5f3e0177..1eae4c548a 100644
--- a/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx
+++ b/apps/webapp/app/components/runs/v3/DeploymentStatus.tsx
@@ -53,6 +53,7 @@ export function DeploymentStatusIcon({
return (
);
+ case "INSTALLING":
case "BUILDING":
case "DEPLOYING":
return ;
@@ -78,6 +79,7 @@ export function deploymentStatusClassNameColor(status: WorkerDeploymentStatus):
switch (status) {
case "PENDING":
return "text-charcoal-500";
+ case "INSTALLING":
case "BUILDING":
case "DEPLOYING":
return "text-pending";
@@ -98,6 +100,8 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: b
switch (status) {
case "PENDING":
return "Queued…";
+ case "INSTALLING":
+ return "Installing…";
case "BUILDING":
return "Building…";
case "DEPLOYING":
@@ -127,6 +131,7 @@ export function deploymentStatusTitle(status: WorkerDeploymentStatus, isBuilt: b
// PENDING and CANCELED are not used so are ommited from the UI
export const deploymentStatuses: WorkerDeploymentStatus[] = [
"PENDING",
+ "INSTALLING",
"BUILDING",
"DEPLOYING",
"DEPLOYED",
@@ -138,6 +143,8 @@ export function deploymentStatusDescription(status: WorkerDeploymentStatus): str
switch (status) {
case "PENDING":
return "The deployment is queued and waiting to be processed.";
+ case "INSTALLING":
+ return "The project dependencies are being installed.";
case "BUILDING":
return "The code is being built and prepared for deployment.";
case "DEPLOYING":
diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.start.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts
similarity index 84%
rename from apps/webapp/app/routes/api.v1.deployments.$deploymentId.start.ts
rename to apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts
index 1272a8418d..d07d9a2f3c 100644
--- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.start.ts
+++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.progress.ts
@@ -1,5 +1,5 @@
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
-import { StartDeploymentRequestBody } from "@trigger.dev/core/v3";
+import { ProgressDeploymentRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { authenticateRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
@@ -35,7 +35,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
const { deploymentId } = parsedParams.data;
const rawBody = await request.json();
- const body = StartDeploymentRequestBody.safeParse(rawBody);
+ const body = ProgressDeploymentRequestBody.safeParse(rawBody);
if (!body.success) {
return json({ error: "Invalid request body", issues: body.error.issues }, { status: 400 });
@@ -44,7 +44,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
const deploymentService = new DeploymentService();
return await deploymentService
- .startDeployment(authenticatedEnv, deploymentId, {
+ .progressDeployment(authenticatedEnv, deploymentId, {
contentHash: body.data.contentHash,
git: body.data.gitMeta,
runtime: body.data.runtime,
@@ -59,8 +59,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
return new Response(null, { status: 204 }); // ignore these errors for now
case "deployment_not_found":
return json({ error: "Deployment not found" }, { status: 404 });
- case "deployment_not_pending":
- return json({ error: "Deployment is not pending" }, { status: 409 });
+ case "deployment_cannot_be_progressed":
+ return json(
+ { error: "Deployment is not in a progressable state (PENDING or INSTALLING)" },
+ { status: 409 }
+ );
case "failed_to_create_remote_build":
return json({ error: "Failed to create remote build" }, { status: 500 });
case "other":
diff --git a/apps/webapp/app/v3/services/deployment.server.ts b/apps/webapp/app/v3/services/deployment.server.ts
index 495ea91496..d3411d9f8f 100644
--- a/apps/webapp/app/v3/services/deployment.server.ts
+++ b/apps/webapp/app/v3/services/deployment.server.ts
@@ -8,7 +8,20 @@ import { env } from "~/env.server";
import { createRemoteImageBuild } from "../remoteImageBuilder.server";
export class DeploymentService extends BaseService {
- public startDeployment(
+ /**
+ * Progresses a deployment from PENDING to INSTALLING and then to BUILDING.
+ * Also extends the deployment timeout.
+ *
+ * When progressing to BUILDING, the remote Depot build is also created.
+ *
+ * Only acts when the current status allows. Not idempotent.
+ *
+ * @param authenticatedEnv The environment which the deployment belongs to.
+ * @param friendlyId The friendly deployment ID.
+ * @param updates Optional deployment details to persist.
+ */
+
+ public progressDeployment(
authenticatedEnv: AuthenticatedEnvironment,
friendlyId: string,
updates: Partial & { git: GitMeta }>
@@ -37,37 +50,26 @@ export class DeploymentService extends BaseService {
});
const validateDeployment = (deployment: Pick) => {
- if (deployment.status !== "PENDING") {
- logger.warn("Attempted starting deployment that is not in PENDING status", {
- deployment,
- });
- return errAsync({ type: "deployment_not_pending" as const });
+ if (deployment.status !== "PENDING" && deployment.status !== "INSTALLING") {
+ logger.warn(
+ "Attempted progressing deployment that is not in PENDING or INSTALLING status",
+ {
+ deployment,
+ }
+ );
+ return errAsync({ type: "deployment_cannot_be_progressed" as const });
}
return okAsync(deployment);
};
- const createRemoteBuild = (deployment: Pick) =>
- fromPromise(createRemoteImageBuild(authenticatedEnv.project), (error) => ({
- type: "failed_to_create_remote_build" as const,
- cause: error,
- })).map((build) => ({
- id: deployment.id,
- externalBuildData: build,
- }));
-
- const updateDeployment = (
- deployment: Pick & {
- externalBuildData: ExternalBuildData | undefined;
- }
- ) =>
+ const progressToInstalling = (deployment: Pick) =>
fromPromise(
this._prisma.workerDeployment.updateMany({
where: { id: deployment.id, status: "PENDING" }, // status could've changed in the meantime, we're not locking the row
data: {
...updates,
- externalBuildData: deployment.externalBuildData,
- status: "BUILDING",
+ status: "INSTALLING",
startedAt: new Date(),
},
}),
@@ -77,17 +79,51 @@ export class DeploymentService extends BaseService {
})
).andThen((result) => {
if (result.count === 0) {
- return errAsync({ type: "deployment_not_pending" as const });
+ return errAsync({ type: "deployment_cannot_be_progressed" as const });
}
- return okAsync({ id: deployment.id });
+ return okAsync({ id: deployment.id, status: "INSTALLING" as const });
});
- const extendTimeout = (deployment: Pick) =>
+ const createRemoteBuild = (deployment: Pick) =>
+ fromPromise(createRemoteImageBuild(authenticatedEnv.project), (error) => ({
+ type: "failed_to_create_remote_build" as const,
+ cause: error,
+ }));
+
+ const progressToBuilding = (deployment: Pick) =>
+ createRemoteBuild(deployment)
+ .andThen((externalBuildData) =>
+ fromPromise(
+ this._prisma.workerDeployment.updateMany({
+ where: { id: deployment.id, status: "INSTALLING" }, // status could've changed in the meantime, we're not locking the row
+ data: {
+ ...updates,
+ externalBuildData,
+ status: "BUILDING",
+ installedAt: new Date(),
+ },
+ }),
+ (error) => ({
+ type: "other" as const,
+ cause: error,
+ })
+ )
+ )
+ .andThen((result) => {
+ if (result.count === 0) {
+ return errAsync({ type: "deployment_cannot_be_progressed" as const });
+ }
+ return okAsync({ id: deployment.id, status: "BUILDING" as const });
+ });
+
+ const extendTimeout = (deployment: Pick) =>
fromPromise(
TimeoutDeploymentService.enqueue(
deployment.id,
- "BUILDING" satisfies WorkerDeploymentStatus,
- "Building timed out",
+ deployment.status,
+ deployment.status === "INSTALLING"
+ ? "Installing dependencies timed out"
+ : "Building timed out",
new Date(Date.now() + env.DEPLOY_TIMEOUT_MS)
),
(error) => ({
@@ -98,8 +134,12 @@ export class DeploymentService extends BaseService {
return getDeployment()
.andThen(validateDeployment)
- .andThen(createRemoteBuild)
- .andThen(updateDeployment)
+ .andThen((deployment) => {
+ if (deployment.status === "PENDING") {
+ return progressToInstalling(deployment);
+ }
+ return progressToBuilding(deployment);
+ })
.andThen(extendTimeout)
.map(() => undefined);
}
diff --git a/internal-packages/database/prisma/migrations/20250923182708_add_installing_status_to_deployments/migration.sql b/internal-packages/database/prisma/migrations/20250923182708_add_installing_status_to_deployments/migration.sql
new file mode 100644
index 0000000000..705623a933
--- /dev/null
+++ b/internal-packages/database/prisma/migrations/20250923182708_add_installing_status_to_deployments/migration.sql
@@ -0,0 +1,3 @@
+ALTER TYPE "public"."WorkerDeploymentStatus" ADD VALUE 'INSTALLING';
+
+ALTER TABLE "public"."WorkerDeployment" ADD COLUMN "installedAt" TIMESTAMP(3);
\ No newline at end of file
diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma
index 041a7a0cc4..c3c26ba507 100644
--- a/internal-packages/database/prisma/schema.prisma
+++ b/internal-packages/database/prisma/schema.prisma
@@ -1764,9 +1764,10 @@ model WorkerDeployment {
triggeredBy User? @relation(fields: [triggeredById], references: [id], onDelete: SetNull, onUpdate: Cascade)
triggeredById String?
- startedAt DateTime?
- builtAt DateTime?
- deployedAt DateTime?
+ startedAt DateTime?
+ installedAt DateTime?
+ builtAt DateTime?
+ deployedAt DateTime?
failedAt DateTime?
errorData Json?
@@ -1787,6 +1788,7 @@ model WorkerDeployment {
enum WorkerDeploymentStatus {
PENDING
+ INSTALLING
/// This is the status when the image is being built
BUILDING
/// This is the status when the image is built and we are waiting for the indexing to finish
diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts
index ce55379267..d8c27a7917 100644
--- a/packages/cli-v3/src/commands/deploy.ts
+++ b/packages/cli-v3/src/commands/deploy.ts
@@ -676,6 +676,7 @@ async function failDeploy(
switch (serverDeployment.status) {
case "PENDING":
+ case "INSTALLING":
case "DEPLOYING":
case "BUILDING": {
await doOutputLogs();
diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts
index 7bfd4e23d3..1a6242b0c1 100644
--- a/packages/core/src/v3/schemas/api.ts
+++ b/packages/core/src/v3/schemas/api.ts
@@ -377,13 +377,13 @@ export const FinalizeDeploymentRequestBody = z.object({
export type FinalizeDeploymentRequestBody = z.infer;
-export const StartDeploymentRequestBody = z.object({
+export const ProgressDeploymentRequestBody = z.object({
contentHash: z.string().optional(),
gitMeta: GitMeta.optional(),
runtime: z.string().optional(),
});
-export type StartDeploymentRequestBody = z.infer;
+export type ProgressDeploymentRequestBody = z.infer;
export const ExternalBuildData = z.object({
buildId: z.string(),
@@ -465,6 +465,7 @@ export const GetDeploymentResponseBody = z.object({
id: z.string(),
status: z.enum([
"PENDING",
+ "INSTALLING",
"BUILDING",
"DEPLOYING",
"DEPLOYED",