Compare commits

...

6 Commits

Author SHA1 Message Date
Mauricio Siu
80d5313dd8 Merge branch 'canary' into 1365-create-preview-deployment-using-api 2025-07-13 20:49:12 -06:00
Mauricio Siu
3f3ff9670b chore(package): bump version to v0.24.2 2025-07-13 20:45:33 -06:00
Mauricio Siu
7fb902551e Merge pull request #2189 from jhon2c/fix/logs-overflow
fix(logs): Restore overflow classnames in logs components
2025-07-13 20:44:34 -06:00
Jhon
a201b3f979 fix(ui): regression of overflow-y-auto class in non dialog related componentes 2025-07-13 21:28:50 -03:00
Jhon
01d78e50fc fix(logs): adds back overflow classnames 2025-07-13 21:09:12 -03:00
Mauricio Siu
da0e726326 feat(preview-deployment): enhance external deployment support
- Add support for external preview deployments with optional GitHub comment handling
- Modify deployment services to conditionally update GitHub issue comments
- Update queue types and deployment worker to handle external deployment flag
- Refactor preview deployment creation to support external deployments
- Improve preview deployment router with more flexible deployment creation logic
2025-03-08 17:07:07 -06:00
17 changed files with 293 additions and 115 deletions

View File

@@ -48,7 +48,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
</div>
) : (
<div className="flex flex-col pt-2 relative">
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem]">
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
<CodeEditor
lineWrapping
value={data || "Empty"}

View File

@@ -158,7 +158,7 @@ export const ShowDeployment = ({
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{" "}
{filteredLogs.length > 0 ? (

View File

@@ -42,7 +42,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
See in detail the config of this container
</DialogDescription>
</DialogHeader>
<div className="text-wrap rounded-lg border p-4 text-sm bg-card max-h-[80vh]">
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
<code>
<pre className="whitespace-pre-wrap break-words">
<CodeEditor

View File

@@ -274,7 +274,7 @@ export const DockerLogsId: React.FC<Props> = ({
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (

View File

@@ -138,7 +138,7 @@ export function LineCountFilter({
}}
/>
</div>
<CommandPrimitive.List className="max-h-[300px] overflow-x-hidden">
<CommandPrimitive.List className="max-h-[300px] overflow-y-auto overflow-x-hidden">
<CommandPrimitive.Group className="px-2 py-1.5">
{lineCountOptions.map((option) => {
const isSelected = value === option.value;

View File

@@ -167,7 +167,7 @@ export const DuplicateProject = ({
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
<div className="space-y-2 max-h-[200px] border rounded-md p-4">
<div className="space-y-2 max-h-[200px] overflow-y-auto border rounded-md p-4">
{selectedServices.map((service) => (
<div key={service.id} className="flex items-center space-x-2">
<span className="text-sm">

View File

@@ -157,7 +157,7 @@ export const ShowProjects = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 max-h-[400px]"
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
onClick={(e) => e.stopPropagation()}
>
{project.applications.length > 0 && (
@@ -265,7 +265,7 @@ export const ShowProjects = () => {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2 max-h-[280px]"
className="w-[200px] space-y-2 overflow-y-auto max-h-[280px]"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuLabel className="font-normal">

View File

@@ -147,7 +147,7 @@ export const SetupServer = ({ serverId }: Props) => {
<li>2. Add The SSH Key to Server Manually</li>
</ul>
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2">
<div className="flex relative flex-col gap-2 overflow-y-auto">
<div className="text-sm text-primary flex flex-row gap-2 items-center">
Copy Public Key ({server?.sshKey?.name})
<button

View File

@@ -117,7 +117,7 @@ export const CreateSSHKey = () => {
Option 2
</span>
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2">
<div className="flex relative flex-col gap-2 overflow-y-auto">
<div className="text-sm text-primary flex flex-row gap-2 items-center">
Copy Public Key
<button

View File

@@ -128,7 +128,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-h-48">
<div className="max-h-48 overflow-y-auto">
{activeNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2">
{node.Hostname}
@@ -162,7 +162,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
</div>
</TooltipTrigger>
<TooltipContent>
<div className="max-h-48">
<div className="max-h-48 overflow-y-auto">
{managerNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2">
{node.Hostname}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.24.1",
"version": "v0.24.2",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -1,13 +1,23 @@
import { apiFindAllByApplication } from "@/server/db/schema";
import { db } from "@/server/db";
import { apiFindAllByApplication, applications } from "@/server/db/schema";
import {
createPreviewDeployment,
findApplicationById,
findPreviewDeploymentByApplicationId,
findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId,
findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { eq } from "drizzle-orm";
import { and } from "drizzle-orm";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import type { DeploymentJob } from "@/server/queues/queue-types";
export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
}
return previewDeployment;
}),
create: protectedProcedure
.input(
z.object({
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
pullRequestId: z.string(),
repository: z.string(),
owner: z.string(),
branch: z.string(),
deploymentHash: z.string(),
prBranch: z.string(),
prNumber: z.any(),
prTitle: z.string(),
prURL: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const organizationId = ctx.session.activeOrganizationId;
const action = input.action;
const prId = input.pullRequestId;
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
(previewDeployment) =>
previewDeployment.application.project.organizationId ===
organizationId,
);
if (filteredPreviewDeploymentResult.length > 0) {
for (const previewDeployment of filteredPreviewDeploymentResult) {
try {
await removePreviewDeployment(
previewDeployment.previewDeploymentId,
);
} catch (error) {
console.log(error);
}
}
}
return {
message: "Preview Deployments Closed",
};
}
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const deploymentHash = input.deploymentHash;
const prBranch = input.prBranch;
const prNumber = input.prNumber;
const prTitle = input.prTitle;
const prURL = input.prURL;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, input.repository),
eq(applications.branch, input.branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, input.owner),
),
with: {
previewDeployments: true,
project: true,
},
});
const filteredApps = apps.filter(
(app) => app.project.organizationId === organizationId,
);
console.log(filteredApps);
for (const app of filteredApps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
try {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
console.log(previewDeployment);
previewDeploymentId = previewDeployment.previewDeploymentId;
} catch (error) {
console.log(error);
}
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
isExternal: true,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
}
return {
message: "Preview Deployments Created",
};
}),
});

View File

@@ -641,16 +641,10 @@ export const settingsRouter = createTRPCRouter({
},
})
.input(
z
.object({
dateRange: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
z.object({
start: z.string().optional(),
end: z.string().optional(),
}),
)
.query(async ({ input }) => {
if (IS_CLOUD) {

View File

@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
});
}
} else {
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
});
}
}

View File

@@ -26,6 +26,7 @@ type DeployJob =
applicationType: "application-preview";
previewDeploymentId: string;
serverId?: string;
isExternal?: boolean;
};
export type DeploymentJob = DeployJob;

View File

@@ -404,11 +404,13 @@ export const deployPreviewApplication = async ({
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
isExternal = false,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
isExternal?: boolean;
}) => {
const application = await findApplicationById(applicationId);
@@ -434,36 +436,39 @@ export const deployPreviewApplication = async ({
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
if (!isExternal) {
const commentExists = await issueCommentExists({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs;
@@ -477,25 +482,31 @@ export const deployPreviewApplication = async ({
});
await buildApplication(application, deployment.logPath);
}
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
if (!isExternal) {
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
if (!isExternal) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
@@ -511,11 +522,13 @@ export const deployRemotePreviewApplication = async ({
titleLog = "Preview Deployment",
descriptionLog = "",
previewDeploymentId,
isExternal = false,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
isExternal?: boolean;
}) => {
const application = await findApplicationById(applicationId);
@@ -541,36 +554,39 @@ export const deployRemotePreviewApplication = async ({
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
if (!isExternal) {
const commentExists = await issueCommentExists({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs;
@@ -592,25 +608,29 @@ export const deployRemotePreviewApplication = async ({
await mechanizeDockerContainer(application);
}
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
if (!isExternal) {
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
if (!isExternal) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",

View File

@@ -152,6 +152,7 @@ export const findPreviewDeploymentsByApplicationId = async (
export const createPreviewDeployment = async (
schema: typeof apiCreatePreviewDeployment._type,
isExternal = false,
) => {
const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`;
@@ -166,27 +167,32 @@ export const createPreviewDeployment = async (
org?.ownerId || "",
);
const octokit = authGithub(application?.github as Github);
let issueId = "";
if (!isExternal) {
const octokit = authGithub(application?.github as Github);
const runningComment = getIssueComment(
application.name,
"initializing",
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
);
const runningComment = getIssueComment(
application.name,
"initializing",
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
);
const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "",
repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "",
repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
});
issueId = `${issue.data.id}`;
}
const previewDeployment = await db
.insert(previewDeployments)
.values({
...schema,
appName: appName,
pullRequestCommentId: `${issue.data.id}`,
pullRequestCommentId: issueId,
})
.returning()
.then((value) => value[0]);
@@ -233,6 +239,13 @@ export const findPreviewDeploymentsByPullRequestId = async (
) => {
const previewDeploymentResult = await db.query.previewDeployments.findMany({
where: eq(previewDeployments.pullRequestId, pullRequestId),
with: {
application: {
with: {
project: true,
},
},
},
});
return previewDeploymentResult;