Merge branch 'canary' into Better-deployment-logs-for-long-commit-message-#2973

This commit is contained in:
Mauricio Siu
2025-11-15 17:34:48 -06:00
41 changed files with 1298 additions and 2824 deletions

View File

@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import {
extractCommitMessage,
extractImageName,
extractImageTag,
extractImageTagFromRequest,
} from "@/pages/api/deploy/[refreshToken]";
describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = {
@@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => {
);
});
});
describe("GitHub Packages Docker Image Tag Extraction", () => {
it("should extract tag from container_metadata", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "v1.0.0",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:v1.0.0",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("v1.0.0");
});
it("should extract tag from package_url when container_metadata tag matches version", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "sha256:abc123...",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("latest");
});
it("should extract tag from package_url when container_metadata is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: "ghcr.io/owner/repo:1.2.3",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("1.2.3");
});
it("should handle different tag formats in package_url", () => {
const headers = { "x-github-event": "registry_package" };
const testCases = [
{ url: "ghcr.io/owner/repo:latest", expected: "latest" },
{ url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" },
{ url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" },
{ url: "ghcr.io/owner/repo:dev", expected: "dev" },
];
for (const testCase of testCases) {
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: testCase.url,
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe(testCase.expected);
}
});
it("should return null for non-registry_package events", () => {
const headers = { "x-github-event": "push" };
const body = {
registry_package: {
package_version: {
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when package_version is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when package_url has no tag", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: "ghcr.io/owner/repo",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when package_url ends with colon (no tag)", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
package_url: "ghcr.io/owner/repo:",
container_metadata: {
tag: {
name: "",
digest: "sha256:abc123...",
},
},
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should return null when tag name is empty string", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBeNull();
});
it("should ignore tag if it matches the version (digest)", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
container_metadata: {
tag: {
name: "sha256:abc123...",
digest: "sha256:abc123...",
},
},
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const tag = extractImageTagFromRequest(headers, body);
expect(tag).toBe("latest");
});
it("should handle registry_package commit message with package_url", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
package_url: "ghcr.io/owner/repo:latest",
},
},
};
const message = extractCommitMessage(headers, body);
expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest");
});
it("should handle registry_package commit message when package_url is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {
package_version: {
version: "sha256:abc123...",
},
},
};
const message = extractCommitMessage(headers, body);
expect(message).toBe("Docker GHCR image pushed");
});
it("should handle registry_package commit message when package_version is missing", () => {
const headers = { "x-github-event": "registry_package" };
const body = {
registry_package: {},
};
const message = extractCommitMessage(headers, body);
expect(message).toBe("NEW COMMIT");
});
});
describe("Docker Image Name and Tag Extraction", () => {
describe("extractImageName", () => {
it("should return image name without tag", () => {
expect(extractImageName("my-image:latest")).toBe("my-image");
expect(extractImageName("my-image:1.0.0")).toBe("my-image");
expect(extractImageName("ghcr.io/owner/repo:latest")).toBe(
"ghcr.io/owner/repo",
);
});
it("should return full image name when no tag is present", () => {
expect(extractImageName("my-image")).toBe("my-image");
expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo");
});
it("should handle images with port numbers correctly", () => {
expect(extractImageName("registry:5000/image:tag")).toBe(
"registry:5000/image",
);
expect(extractImageName("localhost:5000/my-app:latest")).toBe(
"localhost:5000/my-app",
);
});
it("should handle complex image paths", () => {
expect(
extractImageName("myregistryhost:5000/fedora/httpd:version1.0"),
).toBe("myregistryhost:5000/fedora/httpd");
expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe(
"registry.example.com:8080/ns/app",
);
});
it("should return null for invalid inputs", () => {
expect(extractImageName(null)).toBeNull();
expect(extractImageName("")).toBeNull();
});
it("should handle edge cases with multiple colons", () => {
expect(extractImageName("image:tag:extra")).toBe("image:tag");
expect(extractImageName("registry:5000:invalid")).toBe("registry:5000");
});
});
describe("extractImageTag", () => {
it("should extract tag from image with tag", () => {
expect(extractImageTag("my-image:latest")).toBe("latest");
expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0");
expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3");
});
it("should return 'latest' when no tag is present", () => {
expect(extractImageTag("my-image")).toBe("latest");
expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest");
});
it("should handle complex image paths with tags", () => {
expect(
extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"),
).toBe("version1.0");
expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe(
"v1.2.3",
);
});
it("should return null for invalid inputs", () => {
expect(extractImageTag(null)).toBeNull();
expect(extractImageTag("")).toBeNull();
});
it("should handle edge cases with multiple colons", () => {
expect(extractImageTag("image:tag:extra")).toBe("extra");
expect(extractImageTag("registry:5000/image:tag")).toBe("tag");
});
it("should handle numeric tags", () => {
expect(extractImageTag("my-image:123")).toBe("123");
expect(extractImageTag("my-image:1")).toBe("1");
});
});
});

View File

@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { debounce } from "lodash";
import _ from "lodash";
import {
CheckIcon,
ChevronsUpDown,
@@ -236,7 +236,7 @@ export const RestoreBackup = ({
const currentDatabaseType = form.watch("databaseType");
const metadata = form.watch("metadata");
const debouncedSetSearch = debounce((value: string) => {
const debouncedSetSearch = _.debounce((value: string) => {
setDebouncedSearchTerm(value);
}, 350);

View File

@@ -1,5 +1,5 @@
import { FancyAnsi } from "fancy-ansi";
import { escapeRegExp } from "lodash";
import _ from "lodash";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
@@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
}
const htmlContent = fancyAnsi.toHtml(text);
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
const searchRegex = new RegExp(`(${_.escapeRegExp(term)})`, "gi");
const modifiedContent = htmlContent.replace(
searchRegex,

View File

@@ -1,4 +1,3 @@
import type { findEnvironmentById } from "@dokploy/server/index";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -27,12 +26,10 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { api, type RouterOutputs } from "@/utils/api";
type Environment = Omit<
Awaited<ReturnType<typeof findEnvironmentById>>,
"project"
>;
type Project = RouterOutputs["project"]["all"][number];
type Environment = Project["environments"][number];
export type Services = {
appName: string;
@@ -53,17 +50,16 @@ export type Services = {
};
export const extractServices = (data: Environment | undefined) => {
const applications: Services[] =
data?.applications.map((item) => ({
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const applications: Services[] = (data?.applications?.map((item) => ({
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) ?? []) as Services[];
const mariadb: Services[] =
data?.mariadb.map((item) => ({
@@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => {
serverId: item.serverId,
})) || [];
const compose: Services[] =
data?.compose.map((item) => ({
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
})) || [];
const compose: Services[] = (data?.compose?.map((item) => ({
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
})) ?? []) as Services[];
applications.push(
...mysql,

View File

@@ -120,7 +120,7 @@
"next": "^15.3.2",
"next-i18next": "^15.4.2",
"next-themes": "^0.2.1",
"node-os-utils": "1.3.7",
"node-os-utils": "2.0.1",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
@@ -163,7 +163,6 @@
"@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9",
"@types/node": "^18.19.104",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6",
"@types/nodemailer": "^6.4.17",
"@types/qrcode": "^1.5.5",

View File

@@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
/**
* Helper function to get package_version from registry_package events
*/
const getPackageVersion = (headers: any, body: any) => {
const event = headers["x-github-event"];
if (event === "registry_package") {
return body.registry_package?.package_version;
}
return null;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
@@ -46,21 +57,60 @@ export default async function handler(
}
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const sourceType = application.sourceType;
if (sourceType === "docker") {
const applicationImageName = extractImageName(application.dockerImage);
const applicationDockerTag = extractImageTag(application.dockerImage);
const webhookImageName = extractImageNameFromRequest(
req.headers,
req.body,
);
const webhookDockerTag = extractImageTagFromRequest(
req.headers,
req.body,
);
if (
applicationDockerTag &&
webhookDockerTag &&
webhookDockerTag !== applicationDockerTag
) {
if (!applicationImageName) {
res.status(301).json({
message: "Application Docker Image Name Not Found",
});
return;
}
if (!webhookImageName) {
res.status(301).json({
message: "Webhook Docker Image Name Not Found",
});
return;
}
// Validate image name matches
if (webhookImageName !== applicationImageName) {
res.status(301).json({
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
});
return;
}
if (!applicationDockerTag) {
res.status(301).json({
message: "Application Docker Tag Not Found",
});
return;
}
if (!webhookDockerTag) {
res.status(301).json({
message: "Webhook Docker Tag Not Found",
});
return;
}
if (webhookDockerTag !== applicationDockerTag) {
res.status(301).json({
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
});
@@ -191,7 +241,7 @@ export default async function handler(
const jobData: DeploymentJob = {
applicationId: application.applicationId as string,
titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`,
...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }),
type: "deploy",
applicationType: "application",
server: !!application.serverId,
@@ -222,6 +272,39 @@ export default async function handler(
}
}
/**
* Return the image name without the tag
* Example: "my-image" => "my-image"
* Example: "my-image:latest" => "my-image"
* Example: "my-image:1.0.0" => "my-image"
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "myregistryhost:5000/fedora/httpd"
* @link https://docs.docker.com/reference/cli/docker/image/tag/
*/
export function extractImageName(dockerImage: string | null): string | null {
if (!dockerImage || typeof dockerImage !== "string") {
return null;
}
// Handle case where there's no tag (no colon or colon is part of port number)
const lastColonIndex = dockerImage.lastIndexOf(":");
if (lastColonIndex === -1) {
return dockerImage;
}
// Check if the part after the last colon looks like a tag (not a port number)
// Port numbers are typically 1-5 digits, tags are usually longer or contain letters
const afterColon = dockerImage.substring(lastColonIndex + 1);
const isPortNumber = /^\d{1,5}$/.test(afterColon);
// If it's a port number (like registry:5000/image), don't split
if (isPortNumber) {
return dockerImage;
}
// Otherwise, split at the last colon to get image name
return dockerImage.substring(0, lastColonIndex);
}
/**
* Return the last part of the image name, which is the tag
* Example: "my-image" => null
@@ -230,7 +313,7 @@ export default async function handler(
* Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0"
* @link https://docs.docker.com/reference/cli/docker/image/tag/
*/
function extractImageTag(dockerImage: string | null) {
export function extractImageTag(dockerImage: string | null) {
if (!dockerImage || typeof dockerImage !== "string") {
return null;
}
@@ -240,12 +323,78 @@ function extractImageTag(dockerImage: string | null) {
}
/**
* Extract the image name (without tag) from webhook request
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
*/
export const extractImageNameFromRequest = (
headers: any,
body: any,
): string | null => {
// GitHub Packages: registry_package events (container registry)
const packageVersion = getPackageVersion(headers, body);
if (packageVersion?.package_url) {
const packageUrl = packageVersion.package_url;
// Remove tag if present (everything after the last colon)
if (packageUrl.includes(":")) {
const lastColonIndex = packageUrl.lastIndexOf(":");
// Check if it's a port number (like registry:5000/image)
const afterColon = packageUrl.substring(lastColonIndex + 1);
const isPortNumber = /^\d{1,5}$/.test(afterColon);
if (isPortNumber) {
return packageUrl;
}
return packageUrl.substring(0, lastColonIndex);
}
return packageUrl;
}
// Docker Hub
if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.repository) {
const repoName = body.repository.repo_name;
return `${repoName}`;
}
}
return null;
};
/**
* @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload
* @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package
*/
export const extractImageTagFromRequest = (
headers: any,
body: any,
): string | null => {
// GitHub Packages: registry_package events (container registry)
const packageVersion = getPackageVersion(headers, body);
if (packageVersion) {
// Try to get tag from container_metadata first (most reliable)
// Only use it if it's not empty and not the same as the version (digest)
const tagName = packageVersion.container_metadata?.tag?.name?.trim() || "";
if (
tagName &&
tagName !== packageVersion.version &&
!tagName.startsWith("sha256:")
) {
return tagName;
}
// Fallback: extract tag from package_url (e.g., "ghcr.io/owner/repo:tag")
if (packageVersion.package_url) {
const packageUrl = packageVersion.package_url;
// Handle case where package_url ends with colon (no tag)
if (packageUrl.endsWith(":")) {
return null;
}
const tagMatch = packageUrl.match(/:([^:]+)$/);
if (tagMatch?.[1]?.trim()) {
return tagMatch[1].trim();
}
}
}
// Docker Hub
if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.push_data && body.repository) {
return body.push_data.tag;
@@ -255,6 +404,18 @@ export const extractImageTagFromRequest = (
};
export const extractCommitMessage = (headers: any, body: any) => {
// GitHub Packages: registry_package events (container tags)
const githubEvent = headers["x-github-event"];
if (githubEvent === "registry_package") {
const packageVersion = getPackageVersion(headers, body);
if (packageVersion) {
if (packageVersion.package_url) {
return `Docker GHCR image pushed: ${packageVersion.package_url}`;
}
return "Docker GHCR image pushed";
}
// If package_version is missing, fall through to default behavior
}
// GitHub
if (headers["x-github-event"]) {
return body.head_commit ? body.head_commit.message : "NEW COMMIT";
@@ -283,7 +444,7 @@ export const extractCommitMessage = (headers: any, body: any) => {
if (headers["user-agent"]?.includes("Go-http-client")) {
if (body.push_data && body.repository) {
return `Docker image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
return `DockerHub image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`;
}
}

View File

@@ -115,6 +115,7 @@ export type Services = {
id: string;
createdAt: string;
status?: "idle" | "running" | "done" | "error";
lastDeployDate?: Date | null;
};
type Project = Awaited<ReturnType<typeof findProjectById>>;
@@ -128,16 +129,34 @@ export const extractServicesFromEnvironment = (
const allServices: Services[] = [];
const applications: Services[] =
environment.applications?.map((item) => ({
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
})) || [];
environment.applications?.map((item) => {
// Get the most recent deployment date
let lastDeployDate: Date | null = null;
const deployments = (item as any).deployments;
if (deployments && deployments.length > 0) {
for (const deployment of deployments) {
const deployDate = new Date(
deployment.finishedAt ||
deployment.startedAt ||
deployment.createdAt,
);
if (!lastDeployDate || deployDate > lastDeployDate) {
lastDeployDate = deployDate;
}
}
}
return {
appName: item.appName,
name: item.name,
type: "application",
id: item.applicationId,
createdAt: item.createdAt,
status: item.applicationStatus,
description: item.description,
serverId: item.serverId,
lastDeployDate,
};
}) || [];
const mariadb: Services[] =
environment.mariadb?.map((item) => ({
@@ -200,16 +219,34 @@ export const extractServicesFromEnvironment = (
})) || [];
const compose: Services[] =
environment.compose?.map((item) => ({
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
})) || [];
environment.compose?.map((item) => {
// Get the most recent deployment date
let lastDeployDate: Date | null = null;
const deployments = (item as any).deployments;
if (deployments && deployments.length > 0) {
for (const deployment of deployments) {
const deployDate = new Date(
deployment.finishedAt ||
deployment.startedAt ||
deployment.createdAt,
);
if (!lastDeployDate || deployDate > lastDeployDate) {
lastDeployDate = deployDate;
}
}
}
return {
appName: item.appName,
name: item.name,
type: "compose",
id: item.composeId,
createdAt: item.createdAt,
status: item.composeStatus,
description: item.description,
serverId: item.serverId,
lastDeployDate,
};
}) || [];
allServices.push(
...applications,
@@ -237,9 +274,9 @@ const EnvironmentPage = (
const { data: auth } = api.user.get.useQuery();
const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("servicesSort") || "createdAt-desc";
return localStorage.getItem("servicesSort") || "lastDeploy-desc";
}
return "createdAt-desc";
return "lastDeploy-desc";
});
useEffect(() => {
@@ -261,10 +298,45 @@ const EnvironmentPage = (
comparison =
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "lastDeploy": {
const aLastDeploy = a.lastDeployDate;
const bLastDeploy = b.lastDeployDate;
if (direction === "desc") {
// For "desc" (newest first): services with deployments first, then those without
if (!aLastDeploy && !bLastDeploy) {
comparison = 0;
} else if (!aLastDeploy) {
comparison = 1; // a (no deploy) goes after b (has deploy)
} else if (!bLastDeploy) {
comparison = -1; // a (has deploy) goes before b (no deploy)
} else {
// Both have deployments: newest first (negative if a is newer)
comparison = bLastDeploy.getTime() - aLastDeploy.getTime();
}
} else {
// For "asc" (oldest first): services with deployments first, then those without
if (!aLastDeploy && !bLastDeploy) {
comparison = 0;
} else if (!aLastDeploy) {
comparison = 1; // a (no deploy) goes after b (has deploy)
} else if (!bLastDeploy) {
comparison = -1; // a (has deploy) goes before b (no deploy)
} else {
// Both have deployments: oldest first
comparison = aLastDeploy.getTime() - bLastDeploy.getTime();
}
}
break;
}
default:
comparison = 0;
}
return direction === "asc" ? comparison : -comparison;
// For other fields, apply direction normally
if (field !== "lastDeploy") {
return direction === "asc" ? comparison : -comparison;
}
return comparison;
});
};
@@ -1217,6 +1289,9 @@ const EnvironmentPage = (
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="lastDeploy-desc">
Recently deployed
</SelectItem>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>

View File

@@ -3,13 +3,14 @@ import {
addNewService,
checkServiceAccess,
cloneCompose,
cloneComposeRemote,
createCommand,
createCompose,
createComposeByTemplate,
createDomain,
createMount,
deleteMount,
execAsync,
execAsyncRemote,
findComposeById,
findDomainsByComposeId,
findEnvironmentById,
@@ -302,10 +303,12 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to fetch this compose",
});
}
const command = await cloneCompose(compose);
if (compose.serverId) {
await cloneComposeRemote(compose);
await execAsyncRemote(compose.serverId, command);
} else {
await cloneCompose(compose);
await execAsync(command);
}
return compose.sourceType;
} catch (err) {

View File

@@ -4,6 +4,7 @@ import {
findNotificationById,
findOrganizationById,
findUserById,
getDokployUrl,
getUserByToken,
IS_CLOUD,
removeUserById,
@@ -419,11 +420,10 @@ export const userRouter = createTRPCRouter({
});
}
const admin = await findAdmin();
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: admin.user.host;
: await getDokployUrl();
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
const organization = await findOrganizationById(

View File

@@ -2,13 +2,8 @@ import {
deployApplication,
deployCompose,
deployPreviewApplication,
deployRemoteApplication,
deployRemoteCompose,
deployRemotePreviewApplication,
rebuildApplication,
rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
updatePreviewDeployment,
@@ -24,91 +19,48 @@ export const deploymentWorker = new Worker(
if (job.data.applicationType === "application") {
await updateApplicationStatus(job.data.applicationId, "running");
if (job.data.server) {
if (job.data.type === "redeploy") {
await rebuildRemoteApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployRemoteApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} else {
if (job.data.type === "redeploy") {
await rebuildApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
if (job.data.type === "redeploy") {
await rebuildApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} else if (job.data.applicationType === "compose") {
await updateCompose(job.data.composeId, {
composeStatus: "running",
});
if (job.data.server) {
if (job.data.type === "redeploy") {
await rebuildRemoteCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "deploy") {
await deployRemoteCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} else {
if (job.data.type === "deploy") {
await deployCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "redeploy") {
await rebuildCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
if (job.data.type === "deploy") {
await deployCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
} else if (job.data.type === "redeploy") {
await rebuildCompose({
composeId: job.data.composeId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
});
}
} else if (job.data.applicationType === "application-preview") {
await updatePreviewDeployment(job.data.previewDeploymentId, {
previewStatus: "running",
});
if (job.data.server) {
if (job.data.type === "deploy") {
await deployRemotePreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
}
} else {
if (job.data.type === "deploy") {
await deployPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
}
if (job.data.type === "deploy") {
await deployPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
}
}
} catch (error) {

View File

@@ -2,6 +2,7 @@ import type http from "node:http";
import {
docker,
execAsync,
getHostSystemStats,
getLastAdvancedStatsFile,
recordAdvancedStats,
validateRequest,
@@ -49,6 +50,21 @@ export const setupDockerStatsMonitoringSocketServer = (
}
const intervalId = setInterval(async () => {
try {
// Special case: when monitoring "dokploy", get host system stats instead of container stats
if (appName === "dokploy") {
const stat = await getHostSystemStats();
await recordAdvancedStats(stat, appName);
const data = await getLastAdvancedStatsFile(appName);
ws.send(
JSON.stringify({
data,
}),
);
return;
}
const filter = {
status: ["running"],
...(appType === "application" && {

View File

@@ -22,7 +22,7 @@ import {
await initializeNetwork();
createDefaultTraefikConfig();
createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.5.0");
await execAsync("docker pull traefik:v3.6.1");
await initializeStandaloneTraefik();
await initializeRedis();
await initializePostgres();