Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
982f4d7
Merge pull request #4890 from SteadEXE/canary
Siumauricio Jul 22, 2026
b024c9c
Merge pull request #4626 from veksen/veksen/textarea-resets-while-edi…
Siumauricio Jul 22, 2026
6e84f39
Merge pull request #4929 from Dokploy/fix/remove-buttons-not-persisting
Siumauricio Jul 28, 2026
c6607f3
Merge pull request #4924 from dmtrTm/fix/rollback-query-arg-limit
Siumauricio Jul 28, 2026
43229c8
Merge pull request #4931 from Dokploy/fix/postgres-100-arg-limit-finders
Siumauricio Jul 28, 2026
7f41b09
Merge pull request #4911 from AbiRaditya/fix/cloudflare-badge-tooltip…
Siumauricio Jul 28, 2026
d4e087e
Merge pull request #4937 from Dokploy/fix/persist-trigger-type-provid…
Siumauricio Jul 30, 2026
c7a96ed
Merge pull request #4782 from imrja8/fix/watchpath-badge-removal
Siumauricio Jul 31, 2026
2f89981
Merge pull request #4948 from azizbecha/fix/collapsed-sidebar-avatar
Siumauricio Aug 2, 2026
e88d13f
Merge pull request #4947 from azizbecha/fix/4945-deployment-delete-lo…
Siumauricio Aug 2, 2026
49ee74c
Merge pull request #4953 from Dokploy/fix/dropdown-dialog-window-blur
Siumauricio Aug 2, 2026
cbe7895
Merge pull request #4954 from Dokploy/fix/error-page-status-code
Siumauricio Aug 2, 2026
0f0102f
Merge pull request #4933 from CyrilBIENNE/fix/preview-deployment-gith…
Siumauricio Aug 2, 2026
8300820
Merge pull request #4955 from Dokploy/fix/schedule-run-manually-deplo…
Siumauricio Aug 2, 2026
13b897c
Merge pull request #4959 from Dokploy/fix/cleanup-preview-deployments…
Siumauricio Aug 3, 2026
b7fdbd4
Merge pull request #4557 from rnkp755/fix/env-update-issue
Siumauricio Aug 3, 2026
112b2c8
Merge pull request #4923 from dmtrTm/fix/rollback-environment-variables
narcisonunez Aug 3, 2026
3e4ed0c
Merge pull request #4966 from Dokploy/fix/requests-hostname-filter-crash
Siumauricio Aug 4, 2026
e19d7bb
Merge pull request #4971 from Dokploy/fix/restore-dialog-width
Siumauricio Aug 4, 2026
bcf97f9
Merge pull request #4772 from rifatdinc/fix-watch-paths-added-removed…
narcisonunez Aug 6, 2026
dd54299
chore: release v0.29.14
Siumauricio Aug 6, 2026
57fae73
Update packages/server/src/utils/schedules/utils.ts
Siumauricio Aug 6, 2026
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
104 changes: 104 additions & 0 deletions apps/dokploy/__test__/deploy/railpack.command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { ApplicationNested } from "@dokploy/server/utils/builders";
import { getRailpackCommand } from "@dokploy/server/utils/builders/railpack";
import { describe, expect, it } from "vitest";

const createApplication = (
overrides: Partial<ApplicationNested> = {},
): ApplicationNested =>
({
appName: "test-app",
buildType: "railpack",
sourceType: "git",
buildPath: "/",
railpackVersion: "0.15.4",
env: "TEST_VAR=one",
cleanCache: false,
environment: {
project: {
env: "",
},
env: "",
},
...overrides,
}) as unknown as ApplicationNested;

const getSecretsHash = (command: string) => {
const match = command.match(/secrets-hash=([a-f0-9]{64})/);
if (!match?.[1]) {
throw new Error("secrets-hash build arg was not found");
}

return match[1];
};

describe("getRailpackCommand", () => {
it("includes secrets-hash without clean cache", () => {
const command = getRailpackCommand(createApplication());

expect(command).toContain("--build-arg secrets-hash=");
expect(command).not.toContain("cache-key=");
});

it("includes cache-key only when clean cache is enabled", () => {
const command = getRailpackCommand(
createApplication({
cleanCache: true,
}),
);

expect(command).toContain("--build-arg secrets-hash=");
expect(command).toContain("--build-arg cache-key=");
});

it("changes secrets-hash when an environment value changes", () => {
const firstCommand = getRailpackCommand(
createApplication({
env: "TEST_VAR=one",
}),
);
const secondCommand = getRailpackCommand(
createApplication({
env: "TEST_VAR=two",
}),
);

expect(getSecretsHash(firstCommand)).not.toEqual(
getSecretsHash(secondCommand),
);
});

it("changes secrets-hash when referenced project or environment values change", () => {
const firstCommand = getRailpackCommand(
createApplication({
env: [
"PROJECT_VALUE=${{project.SHARED_VALUE}}",
"ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
].join("\n"),
environment: {
project: {
env: "SHARED_VALUE=one",
},
env: "SHARED_VALUE=alpha",
},
} as Partial<ApplicationNested>),
);
const secondCommand = getRailpackCommand(
createApplication({
env: [
"PROJECT_VALUE=${{project.SHARED_VALUE}}",
"ENVIRONMENT_VALUE=${{environment.SHARED_VALUE}}",
].join("\n"),
environment: {
project: {
env: "SHARED_VALUE=two",
},
env: "SHARED_VALUE=beta",
},
} as Partial<ApplicationNested>),
);

expect(getSecretsHash(firstCommand)).not.toEqual(
getSecretsHash(secondCommand),
);
});
});
108 changes: 108 additions & 0 deletions apps/dokploy/__test__/env/rollback-environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { prepareEnvironmentVariables } from "@dokploy/server/index";
import { describe, expect, it } from "vitest";

const projectEnv = `
ENVIRONMENT=staging
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
`;

const environmentEnv = `
NODE_ENV=production
POSTGRES_HOST=postgres.internal
POSTGRES_PORT=5432
REDIS_URL=redis://redis.internal:6379
`;

const serviceEnv = `
NODE_ENV=\${{environment.NODE_ENV}}
REDIS_URL=\${{environment.REDIS_URL}}
PORT=3000
`;

/**
* A rollback replays the snapshot stored in `rollbacks.fullContext`, which keeps
* the service env, the environment env and the project env captured at deploy time.
*/
const fullContext = {
env: serviceEnv,
environment: {
env: environmentEnv,
project: {
env: projectEnv,
},
},
};

describe("prepareEnvironmentVariables for application rollback", () => {
it("resolves environment variables from the rollback snapshot", () => {
const result = prepareEnvironmentVariables(
fullContext.env,
fullContext.environment.project.env,
fullContext.environment.env,
);

expect(result).toEqual([
"NODE_ENV=production",
"REDIS_URL=redis://redis.internal:6379",
"PORT=3000",
]);
});

it("resolves project and environment variables together on rollback", () => {
const rollbackEnv = `
DATABASE_URL=\${{project.DATABASE_URL}}
POSTGRES_URL=postgres://\${{environment.POSTGRES_HOST}}:\${{environment.POSTGRES_PORT}}/app
ENVIRONMENT=\${{project.ENVIRONMENT}}
`;

const result = prepareEnvironmentVariables(
rollbackEnv,
projectEnv,
environmentEnv,
);

expect(result).toEqual([
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
"POSTGRES_URL=postgres://postgres.internal:5432/app",
"ENVIRONMENT=staging",
]);
});

it("throws when the environment env of the snapshot is not passed", () => {
expect(() =>
prepareEnvironmentVariables(fullContext.env, projectEnv),
).toThrow("Invalid environment variable: environment.NODE_ENV");
});

it("maintains precedence: service > environment > project on rollback", () => {
const conflictingProjectEnv = `
NODE_ENV=project
API_URL=https://project.api.com
`;

const conflictingEnvironmentEnv = `
NODE_ENV=environment
API_URL=https://environment.api.com
`;

const rollbackEnv = `
NODE_ENV=service
PROJECT_API_URL=\${{project.API_URL}}
ENVIRONMENT_API_URL=\${{environment.API_URL}}
SELF_REFERENCE=\${{NODE_ENV}}
`;

const result = prepareEnvironmentVariables(
rollbackEnv,
conflictingProjectEnv,
conflictingEnvironmentEnv,
);

expect(result).toEqual([
"NODE_ENV=service",
"PROJECT_API_URL=https://project.api.com",
"ENVIRONMENT_API_URL=https://environment.api.com",
"SELF_REFERENCE=service",
]);
});
});
22 changes: 22 additions & 0 deletions apps/dokploy/__test__/requests/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,28 @@ describe("processLogs", () => {
expect(result.data).toHaveLength(2);
});

it("should not throw when filtering by hostname and an entry has no RequestHost", () => {
const entryWithoutRequestHost = sampleLogEntry.replace(
/"RequestHost":"[^"]*",/,
"",
);

const mixedEntries = `${sampleLogEntry}\n${entryWithoutRequestHost}`;

expect(() =>
parseRawConfig(mixedEntries, undefined, undefined, "traefik.me"),
).not.toThrow();

const result = parseRawConfig(
mixedEntries,
undefined,
undefined,
"traefik.me",
);
expect(result.totalCount).toBe(1);
expect(result.data[0]?.RequestHost).toBe("s222-umami-c381af.traefik.me");
});

it("should filter out Dokploy dashboard requests", () => {
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export const ShowDeployments = ({
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
Set<string>
>(new Set());
const { data: deployments, isPending: isLoadingDeployments } =
api.deployment.allByType.useQuery(
{
Expand All @@ -81,7 +84,7 @@ export const ShowDeployments = ({
api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isPending: isKillingProcess } =
api.deployment.killProcess.useMutation();
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } =
const { mutateAsync: removeDeployment } =
api.deployment.removeDeployment.useMutation();

// Cancel deployment mutations
Expand Down Expand Up @@ -408,20 +411,37 @@ export const ShowDeployments = ({
description="Are you sure you want to delete this deployment? This action cannot be undone."
type="default"
onClick={async () => {
setRemovingDeploymentIds((deploymentIds) => {
const nextDeploymentIds = new Set(deploymentIds);
nextDeploymentIds.add(deployment.deploymentId);
return nextDeploymentIds;
});
try {
await removeDeployment({
deploymentId: deployment.deploymentId,
});
toast.success("Deployment deleted successfully");
} catch (error) {
toast.error("Error deleting deployment");
} finally {
setRemovingDeploymentIds((deploymentIds) => {
const nextDeploymentIds = new Set(
deploymentIds,
);
nextDeploymentIds.delete(
deployment.deploymentId,
);
return nextDeploymentIds;
});
}
}}
>
<Button
variant="destructive"
size="sm"
isLoading={isRemovingDeployment}
isLoading={removingDeploymentIds.has(
deployment.deploymentId,
)}
>
Delete
<Trash2 className="size-4" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ export const createColumns = ({
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
{validationState?.error ? (
{validationState?.isValid && validationState?.message ? (
<p>{validationState.message}</p>
) : validationState?.error ? (
<div className="flex flex-col gap-1">
<p className="font-medium text-red-500">Error:</p>
<p>{validationState.error}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
{validationState?.error ? (
{validationState?.isValid &&
validationState?.message ? (
<p>{validationState.message}</p>
) : validationState?.error ? (
<div className="flex flex-col gap-1">
<p className="font-medium text-red-500">
Error:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,24 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
const currentBuildArgs = form.watch("buildArgs");
const currentBuildSecrets = form.watch("buildSecrets");
const currentCreateEnvFile = form.watch("createEnvFile");
const { isDirty } = form.formState;
const hasChanges =
currentEnv !== (data?.env || "") ||
currentBuildArgs !== (data?.buildArgs || "") ||
currentBuildSecrets !== (data?.buildSecrets || "") ||
currentCreateEnvFile !== (data?.createEnvFile ?? true);

// Skip reset while editing so background refetches don't wipe edits
useEffect(() => {
if (data) {
if (data && !isDirty) {
form.reset({
env: data.env || "",
buildArgs: data.buildArgs || "",
buildSecrets: data.buildSecrets || "",
createEnvFile: data.createEnvFile ?? true,
});
}
}, [data, form]);
}, [data, isDirty, form]);

const onSubmit = async (formData: EnvironmentSchema) => {
mutateAsync({
Expand All @@ -87,6 +89,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
})
.then(async () => {
toast.success("Environments Added");
form.reset(formData);
await refetch();
})
.catch(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,14 +447,18 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
<button
type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
>
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge>
))}
</div>
Expand Down Expand Up @@ -502,14 +506,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
control={form.control}
name="enableSubmodules"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
<FormLabel>Enable Submodules</FormLabel>
</FormItem>
)}
/>
Expand Down
Loading
Loading