feat(deployment): add cancellation functionality for deployments

- Introduced a new endpoint for cancelling deployments, allowing users to cancel both application and compose deployments.
- Implemented validation schemas for cancellation requests.
- Enhanced the deployment dashboard to provide a cancellation option for stuck deployments.
- Updated server-side logic to handle cancellation requests and send appropriate events.
This commit is contained in:
Mauricio Siu
2025-09-06 21:53:15 -06:00
parent 3b7d009841
commit 4e69c70697
6 changed files with 274 additions and 4 deletions

View File

@@ -58,9 +58,14 @@ import {
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
// Schema for canceling deployment
const apiCancelDeployment = z.object({
applicationId: z.string().min(1),
});
export const applicationRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateApplication)
@@ -896,4 +901,47 @@ export const applicationRouter = createTRPCRouter({
return updatedApplication;
}),
cancelDeployment: protectedProcedure
.input(apiCancelDeployment)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to cancel this deployment",
});
}
if (IS_CLOUD && application.serverId) {
try {
await updateApplicationStatus(input.applicationId, "idle");
await cancelDeployment({
applicationId: input.applicationId,
applicationType: "application",
});
return {
success: true,
message: "Deployment cancellation requested",
};
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
error instanceof Error
? error.message
: "Failed to cancel deployment",
});
}
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Deployment cancellation only available in cloud version",
});
}),
});

View File

@@ -58,10 +58,15 @@ import {
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
// Schema for canceling deployment
const apiCancelDeployment = z.object({
composeId: z.string().min(1),
});
export const composeRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateCompose)
@@ -928,4 +933,49 @@ export const composeRouter = createTRPCRouter({
});
}
}),
cancelDeployment: protectedProcedure
.input(apiCancelDeployment)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (
compose.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to cancel this deployment",
});
}
if (IS_CLOUD && compose.serverId) {
try {
await updateCompose(input.composeId, {
composeStatus: "idle",
});
await cancelDeployment({
composeId: input.composeId,
applicationType: "compose",
});
return {
success: true,
message: "Deployment cancellation requested",
};
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
error instanceof Error
? error.message
: "Failed to cancel deployment",
});
}
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Deployment cancellation only available in cloud version",
});
}),
});

View File

@@ -23,3 +23,30 @@ export const deploy = async (jobData: DeploymentJob) => {
throw error;
}
};
type CancelDeploymentData =
| { applicationId: string; applicationType: "application" }
| { composeId: string; applicationType: "compose" };
export const cancelDeployment = async (cancelData: CancelDeploymentData) => {
try {
const result = await fetch(`${process.env.SERVER_URL}/cancel-deployment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.API_KEY || "NO-DEFINED",
},
body: JSON.stringify(cancelData),
});
if (!result.ok) {
const errorData = await result.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to cancel deployment");
}
const data = await result.json();
return data;
} catch (error) {
throw error;
}
};