Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions apps/dokploy/__test__/deploy/application.command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,13 @@ describe("deployApplication - Command Generation Tests", () => {
const execCalls = vi.mocked(execProcess.execAsync).mock.calls;
expect(execCalls.length).toBeGreaterThan(0);

const fullCommand = execCalls[0]?.[0];
expect(fullCommand).toContain("set -e");
expect(fullCommand).toContain("git clone");
expect(fullCommand).toContain("nixpacks build");
const cloneCommand = execCalls[0]?.[0];
expect(cloneCommand).toContain("set -e");
expect(cloneCommand).toContain("git clone");

const buildCommand = execCalls[1]?.[0];
expect(buildCommand).toContain("set -e");
expect(buildCommand).toContain("nixpacks build");
});

it("should include log redirection in command", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const AddGithubProvider = () => {
metadata: "read",
emails: "read",
pull_requests: "write",
checks: "write",
},
default_events: ["pull_request", "push"],
},
Expand Down
132 changes: 104 additions & 28 deletions packages/server/src/services/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin";
import { setStatusCheck } from "./checks";
import {
createDeployment,
createDeploymentPreview,
Expand Down Expand Up @@ -182,29 +183,53 @@ export const deployApplication = async ({
description: descriptionLog,
});

let commitInfo: {
message: string;
hash: string;
} | null = null;

try {
let command = "set -e;";
let cloneCommand = "set -e;";
if (application.sourceType === "github") {
command += await cloneGithubRepository(application);
cloneCommand += await cloneGithubRepository(application);
} else if (application.sourceType === "gitlab") {
command += await cloneGitlabRepository(application);
cloneCommand += await cloneGitlabRepository(application);
} else if (application.sourceType === "gitea") {
command += await cloneGiteaRepository(application);
cloneCommand += await cloneGiteaRepository(application);
} else if (application.sourceType === "bitbucket") {
command += await cloneBitbucketRepository(application);
cloneCommand += await cloneBitbucketRepository(application);
} else if (application.sourceType === "git") {
command += await cloneGitRepository(application);
cloneCommand += await cloneGitRepository(application);
} else if (application.sourceType === "docker") {
command += await buildRemoteDocker(application);
cloneCommand += await buildRemoteDocker(application);
}

command += await getBuildCommand(application);

const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
const cloneCommandWithLog = `(${cloneCommand}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
await execAsyncRemote(serverId, cloneCommandWithLog);
} else {
await execAsync(commandWithLog);
await execAsync(cloneCommandWithLog);
}

// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
commitInfo = await getGitCommitInfo(application);
}

if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "in_progress");
}

let buildCommand = "set -e;";
buildCommand += await getBuildCommand(application);

if (buildCommand) {
const buildCommandWithLog = `(${buildCommand}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, buildCommandWithLog);
} else {
await execAsync(buildCommandWithLog);
}
}

await mechanizeDockerContainer(application);
Expand All @@ -220,6 +245,10 @@ export const deployApplication = async ({
domains: application.domains,
environmentName: application.environment.name,
});

if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "success");
}
} catch (error) {
let command = "";

Expand All @@ -236,6 +265,10 @@ export const deployApplication = async ({
} else {
await execAsync(command);
}

if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "failure");
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");

Expand All @@ -251,16 +284,11 @@ export const deployApplication = async ({

throw error;
} finally {
// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
const commitInfo = await getGitCommitInfo(application);

if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
if (commitInfo) {
await updateDeployment(deployment.deploymentId, {
title: commitInfo.message,
description: `Commit: ${commitInfo.hash}`,
});
}
}
return true;
Expand All @@ -285,7 +313,21 @@ export const rebuildApplication = async ({
description: descriptionLog,
});

let commitInfo: {
message: string;
hash: string;
} | null = null;

try {
// Only extract commit info for non-docker sources
if (application.sourceType !== "docker") {
commitInfo = await getGitCommitInfo(application);
}

if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "in_progress");
}

let command = "set -e;";
// Check case for docker only
command += await getBuildCommand(application);
Expand All @@ -308,6 +350,10 @@ export const rebuildApplication = async ({
domains: application.domains,
environmentName: application.environment.name,
});

if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "success");
}
} catch (error) {
let command = "";

Expand All @@ -324,6 +370,10 @@ export const rebuildApplication = async ({
} else {
await execAsync(command);
}

if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "failure");
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
throw error;
Expand Down Expand Up @@ -366,6 +416,12 @@ export const deployPreviewApplication = async ({
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};

let commitInfo: {
message: string;
hash: string;
} | null = null;

try {
const commentExists = await issueCommentExists({
...issueParams,
Expand Down Expand Up @@ -406,20 +462,34 @@ export const deployPreviewApplication = async ({
application.rollbackRegistry = null;
application.registry = null;

let command = "set -e;";
if (application.sourceType === "github") {
command += await cloneGithubRepository({
let cloneCommand = "set -e;";
cloneCommand += await cloneGithubRepository({
...application,
appName: previewDeployment.appName,
branch: previewDeployment.branch,
});
command += await getBuildCommand(application);
const cloneCommandWithLog = `(${cloneCommand}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, cloneCommandWithLog);
} else {
await execAsync(cloneCommandWithLog);
}

commitInfo = await getGitCommitInfo(application);

const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "in_progress");
}

let buildCommand = "set -e;";
buildCommand += await getBuildCommand(application);

const buildCommandWithLog = `(${buildCommand}) >> ${deployment.logPath} 2>&1`;
if (application.serverId) {
await execAsyncRemote(application.serverId, commandWithLog);
await execAsyncRemote(application.serverId, buildCommandWithLog);
} else {
await execAsync(commandWithLog);
await execAsync(buildCommandWithLog);
}
await mechanizeDockerContainer(application);
}
Expand All @@ -436,12 +506,18 @@ export const deployPreviewApplication = async ({
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "success");
}
} catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
if (commitInfo) {
await setStatusCheck(application, commitInfo.hash, "failure");
}
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
Expand Down
34 changes: 34 additions & 0 deletions packages/server/src/services/checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ApplicationNested } from "../utils/builders";
import { getDokployUrl } from "./admin";
import * as github from "./github";

export type CheckStatus = "queued" | "in_progress" | "success" | "failure";

export async function setStatusCheck(
application: ApplicationNested,
head_sha: string,
status: CheckStatus,
) {
// @TODO: check for preview deployment and update link
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`;

try {
switch (application.sourceType) {
case "github":
return await github.setStatusCheck({
owner: application.owner!,
repository: application.repository!,
githubId: application.githubId!,
head_sha,
name: `Dokploy (${application.environment.project.name}/${application.name})`,
status,
details_url: buildLink,
});
}
} catch (error) {
console.error(
`❌ Failed to write status check to "${application.sourceType}"`,
error,
);
}
}
77 changes: 77 additions & 0 deletions packages/server/src/services/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,83 @@ export const updateIssueComment = async ({
});
};

interface StatusCheckCreate {
owner: string;
repository: string;
issue_number?: string;
head_sha: string;
githubId: string;
name: string;
status: "queued" | "in_progress" | "success" | "failure";
details_url: string;
}

export const setStatusCheck = async ({
owner,
repository,
issue_number,
head_sha,
githubId,
name,
status,
details_url,
}: StatusCheckCreate) => {
const github = await findGithubById(githubId);
const octokit = authGithub(github);

const existing_check = await octokit.rest.checks.listForRef({
owner,
repo: repository,
ref: head_sha,
check_name: name,
});

const data = {
owner,
repo: repository,
issue: issue_number ? Number.parseInt(issue_number) : undefined,
head_sha,
name,
status:
status === "queued"
? "queued"
: status === "in_progress"
? "in_progress"
: "completed",
conclusion:
status === "success"
? "success"
: status === "failure"
? "failure"
: undefined,
details_url,
} as const;

const existing = existing_check.data.check_runs[0];

if (existing) {
if (existing.status === "completed" && data.status !== "completed") {
await octokit.rest.checks.update({
check_run_id: existing.id,
...data,
conclusion: "neutral",
output: {
title: "Obsolete",
summary: "Superseded",
},
});
} else {
await octokit.rest.checks.update({
check_run_id: existing.id,
...data,
});
return;
}
}

await octokit.rest.checks.create(data);
};

interface CommentCreate {
appName: string;
owner: string;
Expand Down