mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 06:05:25 +02:00
Merge pull request #3104 from Dokploy/fix/error-parsing
feat: enhance error handling in deployment processes
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||
import {
|
||||
ExecError,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
@@ -28,6 +29,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeployment,
|
||||
@@ -228,7 +230,16 @@ export const deployApplication = async ({
|
||||
environmentName: application.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
} else {
|
||||
@@ -317,6 +328,21 @@ export const rebuildApplication = async ({
|
||||
environmentName: application.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (application.serverId) {
|
||||
await execAsyncRemote(application.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
|
||||
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
|
||||
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
|
||||
import {
|
||||
ExecError,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
@@ -32,6 +33,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
|
||||
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeploymentCompose,
|
||||
@@ -270,6 +272,21 @@ export const deployCompose = async ({
|
||||
environmentName: compose.environment.name,
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
@@ -342,6 +359,21 @@ export const rebuildCompose = async ({
|
||||
composeStatus: "done",
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateCompose(composeId, {
|
||||
composeStatus: "error",
|
||||
|
||||
@@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Docker service might not exist locally
|
||||
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
||||
// https://docs.dokploy.com/docs/core/manual-installation
|
||||
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
|
||||
55
packages/server/src/utils/process/ExecError.ts
Normal file
55
packages/server/src/utils/process/ExecError.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export interface ExecErrorDetails {
|
||||
command: string;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
exitCode?: number;
|
||||
originalError?: Error;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export class ExecError extends Error {
|
||||
public readonly command: string;
|
||||
public readonly stdout?: string;
|
||||
public readonly stderr?: string;
|
||||
public readonly exitCode?: number;
|
||||
public readonly originalError?: Error;
|
||||
public readonly serverId?: string | null;
|
||||
|
||||
constructor(message: string, details: ExecErrorDetails) {
|
||||
super(message);
|
||||
this.name = "ExecError";
|
||||
this.command = details.command;
|
||||
this.stdout = details.stdout;
|
||||
this.stderr = details.stderr;
|
||||
this.exitCode = details.exitCode;
|
||||
this.originalError = details.originalError;
|
||||
this.serverId = details.serverId;
|
||||
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, ExecError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a formatted error message with all details
|
||||
*/
|
||||
getDetailedMessage(): string {
|
||||
const parts = [
|
||||
`Command: ${this.command}`,
|
||||
this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null,
|
||||
this.serverId ? `Server ID: ${this.serverId}` : "Location: Local",
|
||||
this.stderr ? `Stderr: ${this.stderr}` : null,
|
||||
this.stdout ? `Stdout: ${this.stdout}` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
return `${this.message}\n${parts.join("\n")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error is from a remote execution
|
||||
*/
|
||||
isRemote(): boolean {
|
||||
return !!this.serverId;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process";
|
||||
import util from "node:util";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { Client } from "ssh2";
|
||||
import { ExecError } from "./ExecError";
|
||||
|
||||
export const execAsync = util.promisify(exec);
|
||||
// Re-export ExecError for easier imports
|
||||
export { ExecError } from "./ExecError";
|
||||
|
||||
const execAsyncBase = util.promisify(exec);
|
||||
|
||||
export const execAsync = async (
|
||||
command: string,
|
||||
options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string },
|
||||
): Promise<{ stdout: string; stderr: string }> => {
|
||||
try {
|
||||
const result = await execAsyncBase(command, options);
|
||||
return {
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// @ts-ignore - exec error has these properties
|
||||
const exitCode = error.code;
|
||||
// @ts-ignore
|
||||
const stdout = error.stdout?.toString() || "";
|
||||
// @ts-ignore
|
||||
const stderr = error.stderr?.toString() || "";
|
||||
|
||||
throw new ExecError(`Command execution failed: ${error.message}`, {
|
||||
command,
|
||||
stdout,
|
||||
stderr,
|
||||
exitCode,
|
||||
originalError: error,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
interface ExecOptions {
|
||||
cwd?: string;
|
||||
@@ -21,7 +56,16 @@ export const execAsyncStream = (
|
||||
|
||||
const childProcess = exec(command, options, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
reject(
|
||||
new ExecError(`Command execution failed: ${error.message}`, {
|
||||
command,
|
||||
stdout: stdoutComplete,
|
||||
stderr: stderrComplete,
|
||||
// @ts-ignore
|
||||
exitCode: error.code,
|
||||
originalError: error,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
|
||||
@@ -45,7 +89,14 @@ export const execAsyncStream = (
|
||||
|
||||
childProcess.on("error", (error) => {
|
||||
console.log(error);
|
||||
reject(error);
|
||||
reject(
|
||||
new ExecError(`Command execution error: ${error.message}`, {
|
||||
command,
|
||||
stdout: stdoutComplete,
|
||||
stderr: stderrComplete,
|
||||
originalError: error,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -108,7 +159,14 @@ export const execAsyncRemote = async (
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
onData?.(err.message);
|
||||
throw err;
|
||||
reject(
|
||||
new ExecError(`Remote command execution failed: ${err.message}`, {
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
stream
|
||||
.on("close", (code: number, _signal: string) => {
|
||||
@@ -116,7 +174,18 @@ export const execAsyncRemote = async (
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`Error occurred ❌: ${stderr}`));
|
||||
reject(
|
||||
new ExecError(
|
||||
`Remote command failed with exit code ${code}`,
|
||||
{
|
||||
command,
|
||||
stdout,
|
||||
stderr,
|
||||
exitCode: code,
|
||||
serverId,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
@@ -132,17 +201,25 @@ export const execAsyncRemote = async (
|
||||
.on("error", (err) => {
|
||||
conn.end();
|
||||
if (err.level === "client-authentication") {
|
||||
onData?.(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
|
||||
onData?.(errorMsg);
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
),
|
||||
new ExecError(errorMsg, {
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
onData?.(`SSH connection error: ${err.message}`);
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
const errorMsg = `SSH connection error: ${err.message}`;
|
||||
onData?.(errorMsg);
|
||||
reject(
|
||||
new ExecError(errorMsg, {
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
}),
|
||||
);
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
|
||||
@@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
// await recreateDirectory(outputPath);
|
||||
command += `rm -rf ${outputPath};`;
|
||||
command += `mkdir -p ${outputPath};`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
Reference in New Issue
Block a user