diff --git a/apps/dokploy/__test__/schedule/schedule-command-boundary.test.ts b/apps/dokploy/__test__/schedule/schedule-command-boundary.test.ts index b7febba18f9..aa440051a39 100644 --- a/apps/dokploy/__test__/schedule/schedule-command-boundary.test.ts +++ b/apps/dokploy/__test__/schedule/schedule-command-boundary.test.ts @@ -69,6 +69,7 @@ describe("schedule command boundary", () => { }); mocks.execAsyncRemote.mockResolvedValue({ stdout: "", stderr: "" }); mocks.getServiceContainer.mockResolvedValue({ Id: "container-one" }); + mocks.spawnAsync.mockResolvedValue(undefined); }); it("quotes remote application docker exec command and log paths", async () => { @@ -119,6 +120,70 @@ describe("schedule command boundary", () => { expect(command).toContain("tee -a '/tmp/deployment log;id.log'"); }); + it("runs dokploy-server schedule scripts from the local schedule directory", async () => { + mocks.findScheduleById.mockResolvedValue({ + appName: "schedule-one", + command: "", + shellType: "bash", + scheduleType: "dokploy-server", + }); + + await expect(runCommand("schedule-1")).resolves.toMatchObject({ + deploymentId: "deployment-1", + status: "done", + }); + + expect(mocks.spawnAsync).toHaveBeenCalledTimes(1); + expect(mocks.spawnAsync).toHaveBeenCalledWith( + "bash", + ["-c", "./script.sh"], + expect.any(Function), + { cwd: "/local schedules/schedule-one" }, + ); + expect(mocks.execAsyncRemote).not.toHaveBeenCalled(); + }); + + it("marks dokploy-server schedule runs as failed when the script fails", async () => { + mocks.findScheduleById.mockResolvedValue({ + appName: "schedule-one", + command: "", + shellType: "bash", + scheduleType: "dokploy-server", + }); + mocks.spawnAsync.mockRejectedValue(new Error("exit code 1")); + + await expect(runCommand("schedule-1")).resolves.toMatchObject({ + deploymentId: "deployment-1", + status: "error", + }); + expect(mocks.updateDeploymentStatus).toHaveBeenCalledWith( + "deployment-1", + "error", + ); + }); + + it("runs only the container command for application schedules", async () => { + mocks.findScheduleById.mockResolvedValue({ + appName: "schedule-one", + application: { appName: "app-one", serverId: null }, + command: "echo done", + shellType: "sh", + scheduleType: "application", + }); + + await expect(runCommand("schedule-1")).resolves.toMatchObject({ + deploymentId: "deployment-1", + status: "done", + }); + + expect(mocks.spawnAsync).toHaveBeenCalledTimes(1); + expect(mocks.spawnAsync).toHaveBeenCalledWith( + "docker", + ["exec", "container-one", "sh", "-c", "echo done"], + expect.any(Function), + ); + }); + it("rejects unsafe stored schedule app names before remote script execution", async () => { mocks.findScheduleById.mockResolvedValue({ appName: "schedule;id", diff --git a/packages/server/src/utils/schedules/utils.ts b/packages/server/src/utils/schedules/utils.ts index c9531f68ad1..87cb4c9862d 100644 --- a/packages/server/src/utils/schedules/utils.ts +++ b/packages/server/src/utils/schedules/utils.ts @@ -166,6 +166,7 @@ export const runCommand = async (scheduleId: string) => { throw error; } } + } else if (scheduleType === "dokploy-server") { const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); const { SCHEDULES_PATH } = paths(); const fullPath = getScheduleDirectory(SCHEDULES_PATH, appName);