214 lines · 4.8 KB · TypeScript
RawDownload
import { serve } from "@hono/node-server";import { Hono } from "hono";import "dotenv/config";import { zValidator } from "@hono/zod-validator";import { Inngest } from "inngest";import { serve as serveInngest } from "inngest/hono";import { logger } from "./logger.js";import {	cancelDeploymentSchema,	type DeployJob,	deployJobSchema,} from "./schema.js";import { fetchDeploymentJobs } from "./service.js";import { deploy } from "./utils.js"; const app = new Hono(); // Initialize Inngest clientexport const inngest = new Inngest({	id: "dokploy-deployments",	name: "Dokploy Deployment Service",}); export const deploymentFunction = inngest.createFunction(	{		id: "deploy-application",		name: "Deploy Application",		concurrency: [			{				key: "event.data.serverId",				limit: 1,			},		],		retries: 0,		cancelOn: [			{				event: "deployment/cancelled",				if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId",				timeout: "1h", // Allow cancellation for up to 1 hour			},		],	},	{ event: "deployment/requested" }, 	async ({ event, step }) => {		const jobData = event.data as DeployJob; 		return await step.run("execute-deployment", async () => {			logger.info("Deploying started"); 			try {				const result = await deploy(jobData);				logger.info("Deployment finished", result); 				// Send success event				await inngest.send({					name: "deployment/completed",					data: {						...jobData,						result,						status: "success",					},				}); 				return result;			} catch (error) {				logger.error("Deployment failed", { jobData, error }); 				// Send failure event				await inngest.send({					name: "deployment/failed",					data: {						...jobData,						error: error instanceof Error ? error.message : String(error),						status: "failed",					},				}); 				throw error;			}		});	},); app.use(async (c, next) => {	if (c.req.path === "/health" || c.req.path === "/api/inngest") {		return next();	} 	const authHeader = c.req.header("X-API-Key"); 	if (process.env.API_KEY !== authHeader) {		return c.json({ message: "Invalid API Key" }, 403);	} 	return next();}); app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {	const data = c.req.valid("json");	logger.info("Received deployment request", data); 	try {		// Send event to Inngest instead of adding to Redis queue		await inngest.send({			name: "deployment/requested",			data,		}); 		logger.info("Deployment event sent to Inngest", {			serverId: data.serverId,		}); 		return c.json(			{				message: "Deployment Added to Inngest Queue",				serverId: data.serverId,			},			200,		);	} catch (error) {		logger.error("Failed to send deployment event", error);		return c.json(			{				message: "Failed to queue deployment",				error: error instanceof Error ? error.message : String(error),			},			500,		);	}}); app.post(	"/cancel-deployment",	zValidator("json", cancelDeploymentSchema),	async (c) => {		const data = c.req.valid("json");		logger.info("Received cancel deployment request", data); 		try {			// Send cancellation event to Inngest 			await inngest.send({				name: "deployment/cancelled",				data,			}); 			const identifier =				data.applicationType === "application"					? `applicationId: ${data.applicationId}`					: `composeId: ${data.composeId}`; 			logger.info("Deployment cancellation event sent", {				...data,				identifier,			}); 			return c.json({				message: "Deployment cancellation requested",				applicationType: data.applicationType,			});		} catch (error) {			logger.error("Failed to send deployment cancellation event", error);			return c.json(				{					message: "Failed to cancel deployment",					error: error instanceof Error ? error.message : String(error),				},				500,			);		}	},); app.get("/health", async (c) => {	return c.json({ status: "ok" });}); // List deployment jobs (Inngest runs) for a server - same shape as BullMQ queue for the UIapp.get("/jobs", async (c) => {	const serverId = c.req.query("serverId");	if (!serverId) {		return c.json({ message: "serverId is required" }, 400);	} 	try {		const rows = await fetchDeploymentJobs(serverId);		return c.json(rows);	} catch (error) {		const message = error instanceof Error ? error.message : String(error);		if (message.includes("INNGEST_BASE_URL")) {			return c.json(				{ message: "INNGEST_BASE_URL is required to list deployment jobs" },				503,			);		}		logger.error("Failed to fetch jobs from Inngest", { serverId, error });		return c.json([], 200);	}}); // Serve Inngest functions endpointapp.on(	["GET", "POST", "PUT"],	"/api/inngest",	serveInngest({		client: inngest,		functions: [deploymentFunction],	}),); const port = Number.parseInt(process.env.PORT || "3000");logger.info("Starting Deployments Server with Inngest ✅", port);serve({ fetch: app.fetch, port });