Merge pull request #4978 from Dokploy/hotfix/v0.29.14

Hotfix v0.29.14: backport 20 bug fixes
This commit is contained in:
Mauricio Siu
2026-08-06 00:00:20 -06:00
committed by GitHub
40 changed files with 620 additions and 226 deletions
@@ -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
View File
@@ -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",
]);
});
});
@@ -55,6 +55,28 @@ describe("processLogs", () => {
expect(result.data).toHaveLength(2); 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", () => { 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"}`; 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"}`;
@@ -63,6 +63,9 @@ export const ShowDeployments = ({
const [activeLog, setActiveLog] = useState< const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null RouterOutputs["deployment"]["all"][number] | null
>(null); >(null);
const [removingDeploymentIds, setRemovingDeploymentIds] = useState<
Set<string>
>(new Set());
const { data: deployments, isPending: isLoadingDeployments } = const { data: deployments, isPending: isLoadingDeployments } =
api.deployment.allByType.useQuery( api.deployment.allByType.useQuery(
{ {
@@ -81,7 +84,7 @@ export const ShowDeployments = ({
api.rollback.rollback.useMutation(); api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isPending: isKillingProcess } = const { mutateAsync: killProcess, isPending: isKillingProcess } =
api.deployment.killProcess.useMutation(); api.deployment.killProcess.useMutation();
const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } = const { mutateAsync: removeDeployment } =
api.deployment.removeDeployment.useMutation(); api.deployment.removeDeployment.useMutation();
// Cancel deployment mutations // Cancel deployment mutations
@@ -408,6 +411,11 @@ export const ShowDeployments = ({
description="Are you sure you want to delete this deployment? This action cannot be undone." description="Are you sure you want to delete this deployment? This action cannot be undone."
type="default" type="default"
onClick={async () => { onClick={async () => {
setRemovingDeploymentIds((deploymentIds) => {
const nextDeploymentIds = new Set(deploymentIds);
nextDeploymentIds.add(deployment.deploymentId);
return nextDeploymentIds;
});
try { try {
await removeDeployment({ await removeDeployment({
deploymentId: deployment.deploymentId, deploymentId: deployment.deploymentId,
@@ -415,13 +423,25 @@ export const ShowDeployments = ({
toast.success("Deployment deleted successfully"); toast.success("Deployment deleted successfully");
} catch (error) { } catch (error) {
toast.error("Error deleting deployment"); toast.error("Error deleting deployment");
} finally {
setRemovingDeploymentIds((deploymentIds) => {
const nextDeploymentIds = new Set(
deploymentIds,
);
nextDeploymentIds.delete(
deployment.deploymentId,
);
return nextDeploymentIds;
});
} }
}} }}
> >
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
isLoading={isRemovingDeployment} isLoading={removingDeploymentIds.has(
deployment.deploymentId,
)}
> >
Delete Delete
<Trash2 className="size-4" /> <Trash2 className="size-4" />
@@ -209,7 +209,9 @@ export const createColumns = ({
</Badge> </Badge>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-xs"> <TooltipContent className="max-w-xs">
{validationState?.error ? ( {validationState?.isValid && validationState?.message ? (
<p>{validationState.message}</p>
) : validationState?.error ? (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<p className="font-medium text-red-500">Error:</p> <p className="font-medium text-red-500">Error:</p>
<p>{validationState.error}</p> <p>{validationState.error}</p>
@@ -626,7 +626,10 @@ export const ShowDomains = ({ id, type }: Props) => {
</Badge> </Badge>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-xs"> <TooltipContent className="max-w-xs">
{validationState?.error ? ( {validationState?.isValid &&
validationState?.message ? (
<p>{validationState.message}</p>
) : validationState?.error ? (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<p className="font-medium text-red-500"> <p className="font-medium text-red-500">
Error: Error:
@@ -60,14 +60,16 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
const currentBuildArgs = form.watch("buildArgs"); const currentBuildArgs = form.watch("buildArgs");
const currentBuildSecrets = form.watch("buildSecrets"); const currentBuildSecrets = form.watch("buildSecrets");
const currentCreateEnvFile = form.watch("createEnvFile"); const currentCreateEnvFile = form.watch("createEnvFile");
const { isDirty } = form.formState;
const hasChanges = const hasChanges =
currentEnv !== (data?.env || "") || currentEnv !== (data?.env || "") ||
currentBuildArgs !== (data?.buildArgs || "") || currentBuildArgs !== (data?.buildArgs || "") ||
currentBuildSecrets !== (data?.buildSecrets || "") || currentBuildSecrets !== (data?.buildSecrets || "") ||
currentCreateEnvFile !== (data?.createEnvFile ?? true); currentCreateEnvFile !== (data?.createEnvFile ?? true);
// Skip reset while editing so background refetches don't wipe edits
useEffect(() => { useEffect(() => {
if (data) { if (data && !isDirty) {
form.reset({ form.reset({
env: data.env || "", env: data.env || "",
buildArgs: data.buildArgs || "", buildArgs: data.buildArgs || "",
@@ -75,7 +77,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
createEnvFile: data.createEnvFile ?? true, createEnvFile: data.createEnvFile ?? true,
}); });
} }
}, [data, form]); }, [data, isDirty, form]);
const onSubmit = async (formData: EnvironmentSchema) => { const onSubmit = async (formData: EnvironmentSchema) => {
mutateAsync({ mutateAsync({
@@ -87,6 +89,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Environments Added"); toast.success("Environments Added");
form.reset(formData);
await refetch(); await refetch();
}) })
.catch(() => { .catch(() => {
@@ -447,14 +447,18 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -502,14 +506,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -242,14 +242,18 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -298,14 +302,14 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -477,14 +477,18 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
className="flex items-center gap-1" className="flex items-center gap-1"
> >
{path} {path}
<X <button
className="size-3 cursor-pointer hover:text-destructive" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
field.onChange(newPaths); field.onChange(newPaths);
}} }}
/> >
<X className="size-3 cursor-pointer hover:text-destructive" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -531,14 +535,14 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -438,8 +438,12 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</TooltipProvider> </TooltipProvider>
</div> </div>
<Select <Select
onValueChange={field.onChange} onValueChange={(value) => {
defaultValue={field.value} if (!value) {
return;
}
field.onChange(value);
}}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -487,14 +491,18 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
className="flex items-center gap-1" className="flex items-center gap-1"
> >
{path} {path}
<X <button
className="size-3 cursor-pointer hover:text-destructive" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
field.onChange(newPaths); field.onChange(newPaths);
}} }}
/> >
<X className="size-3 cursor-pointer hover:text-destructive" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -543,14 +551,14 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -467,14 +467,18 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
className="flex items-center gap-1" className="flex items-center gap-1"
> >
{path} {path}
<X <button
className="size-3 cursor-pointer hover:text-destructive" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
field.onChange(newPaths); field.onChange(newPaths);
}} }}
/> >
<X className="size-3 cursor-pointer hover:text-destructive" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -521,14 +525,14 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -58,8 +58,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
const handleRunManually = async (scheduleId: string) => { const handleRunManually = async (scheduleId: string) => {
setRunningSchedules((prev) => new Set(prev).add(scheduleId)); setRunningSchedules((prev) => new Set(prev).add(scheduleId));
try { try {
await runManually({ scheduleId }); const result = await runManually({ scheduleId });
toast.success("Schedule run successfully"); if (result.status === "error") {
toast.error("Schedule run failed, check the deployment logs");
} else {
toast.success("Schedule run successfully");
}
await refetchSchedules(); await refetchSchedules();
} catch { } catch {
toast.error("Error running schedule"); toast.error("Error running schedule");
@@ -451,14 +451,18 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -506,14 +510,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -251,14 +251,18 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -306,14 +310,14 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -442,14 +442,18 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -497,14 +501,14 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -432,8 +432,12 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</TooltipProvider> </TooltipProvider>
</div> </div>
<Select <Select
onValueChange={field.onChange} onValueChange={(value) => {
defaultValue={field.value} if (!value) {
return;
}
field.onChange(value);
}}
value={field.value} value={field.value}
> >
<FormControl> <FormControl>
@@ -479,14 +483,18 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -538,14 +546,14 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -468,14 +468,18 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
{field.value?.map((path, index) => ( {field.value?.map((path, index) => (
<Badge key={index} variant="secondary"> <Badge key={index} variant="secondary">
{path} {path}
<X <button
className="ml-1 size-3 cursor-pointer" type="button"
aria-label="Remove watch path"
className="inline-flex items-center focus-visible:ring-2"
onClick={() => { onClick={() => {
const newPaths = [...(field.value || [])]; const newPaths = [...(field.value || [])];
newPaths.splice(index, 1); newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
}} }}
/> >
<X className="ml-1 size-3 cursor-pointer" />
</button>
</Badge> </Badge>
))} ))}
</div> </div>
@@ -523,14 +527,14 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex items-center space-x-2"> <FormItem className="flex flex-row items-center space-x-2 space-y-0">
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={field.onChange}
/> />
</FormControl> </FormControl>
<FormLabel className="mt-0!">Enable Submodules</FormLabel> <FormLabel>Enable Submodules</FormLabel>
</FormItem> </FormItem>
)} )}
/> />
@@ -316,7 +316,7 @@ export const RestoreBackup = ({
Restore Backup Restore Backup
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-lg"> <DialogContent className="sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center"> <DialogTitle className="flex items-center">
<RotateCcw className="mr-2 size-4" /> <RotateCcw className="mr-2 size-4" />
+1 -1
View File
@@ -5,7 +5,7 @@ import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", "group/badge inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2.5 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg:not(.cursor-pointer)]:pointer-events-none [&>svg]:size-3!",
{ {
variants: { variants: {
variant: { variant: {
+20 -4
View File
@@ -1,21 +1,37 @@
import { CheckIcon, ChevronRightIcon } from "lucide-react"; import { CheckIcon, ChevronRightIcon } from "lucide-react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"; import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
import type * as React from "react"; import * as React from "react";
import { markNestedPopupClosed } from "@/components/ui/nested-popup-context"; import { markNestedPopupClosed } from "@/components/ui/nested-popup-context";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function DropdownMenu({ function DropdownMenu({
open,
defaultOpen,
onOpenChange, onOpenChange,
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
defaultOpen ?? false,
);
const isControlled = open !== undefined;
const isOpen = isControlled ? open : uncontrolledOpen;
return ( return (
<DropdownMenuPrimitive.Root <DropdownMenuPrimitive.Root
data-slot="dropdown-menu" data-slot="dropdown-menu"
onOpenChange={(open) => { open={isOpen}
if (!open) { onOpenChange={(nextOpen) => {
// Radix closes menus on window blur, unmounting any dialog rendered inside
if (!nextOpen && !document.hasFocus()) {
return;
}
if (!isControlled) {
setUncontrolledOpen(nextOpen);
}
if (!nextOpen) {
markNestedPopupClosed(); markNestedPopupClosed();
} }
onOpenChange?.(open); onOpenChange?.(nextOpen);
}} }}
{...props} {...props}
/> />
+3 -3
View File
@@ -464,7 +464,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
} }
const sidebarMenuButtonVariants = cva( const sidebarMenuButtonVariants = cva(
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
{ {
variants: { variants: {
variant: { variant: {
@@ -473,8 +473,8 @@ const sidebarMenuButtonVariants = cva(
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]", "bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
}, },
size: { size: {
default: "h-8 text-sm", default: "h-8 text-sm group-data-[collapsible=icon]:p-2!",
sm: "h-7 text-xs", sm: "h-7 text-xs group-data-[collapsible=icon]:p-2!",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
}, },
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.29.13", "version": "v0.29.14",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
+6 -14
View File
@@ -5,12 +5,11 @@ import { buttonVariants } from "@/components/ui/button";
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling"; import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
interface Props { interface Props {
statusCode: number; statusCode?: number;
error?: Error;
} }
export default function Custom404({ statusCode, error }: Props) { export default function ErrorPage({ statusCode }: Props) {
const displayStatusCode = statusCode || 400; const displayStatusCode = statusCode || 500;
const { config: whitelabeling } = useWhitelabelingPublic(); const { config: whitelabeling } = useWhitelabelingPublic();
const appName = whitelabeling?.appName || "Dokploy"; const appName = whitelabeling?.appName || "Dokploy";
const logoUrl = whitelabeling?.logoUrl || undefined; const logoUrl = whitelabeling?.logoUrl || undefined;
@@ -45,12 +44,6 @@ export default function Custom404({ statusCode, error }: Props) {
{errorDescription} {errorDescription}
</p> </p>
)} )}
{error && (
<div className="mt-3 text-red-500">
<p>{error.message}</p>
</div>
)}
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3"> <div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
<Link <Link
href="/dashboard/home" href="/dashboard/home"
@@ -101,8 +94,7 @@ export default function Custom404({ statusCode, error }: Props) {
); );
} }
// @ts-ignore ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
Error.getInitialProps = ({ res, err }: NextPageContext) => { const statusCode = res ? res.statusCode : err ? (err.statusCode ?? 500) : 404;
const statusCode = res ? res.statusCode : err ? err.statusCode : 404; return { statusCode };
return { statusCode, error: err };
}; };
+35 -21
View File
@@ -119,9 +119,11 @@ export default async function handler(
} }
// If webhook doesn't provide image info, we'll use the configured image (old behavior) // If webhook doesn't provide image info, we'll use the configured image (old behavior)
} else if (sourceType === "github") { } else if (sourceType === "github") {
const normalizedCommits = req.body?.commits?.flatMap( const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
application.watchPaths, application.watchPaths,
@@ -150,21 +152,29 @@ export default async function handler(
let normalizedCommits: string[] = []; let normalizedCommits: string[] = [];
if (provider === "github") { if (provider === "github") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} else if (provider === "gitlab") { } else if (provider === "gitlab") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} else if (provider === "gitea") { } else if (provider === "gitea") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} else if (provider === "soft-serve") { } else if (provider === "soft-serve") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} }
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
@@ -179,9 +189,11 @@ export default async function handler(
} else if (sourceType === "gitlab") { } else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body); const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap( const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
application.watchPaths, application.watchPaths,
@@ -225,9 +237,11 @@ export default async function handler(
} else if (sourceType === "gitea") { } else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body); const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap( const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
application.watchPaths, application.watchPaths,
@@ -54,9 +54,11 @@ export default async function handler(
if (sourceType === "github") { if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body); const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap( const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths, composeResult.watchPaths,
@@ -74,9 +76,11 @@ export default async function handler(
} }
} else if (sourceType === "gitlab") { } else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body); const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap( const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths, composeResult.watchPaths,
@@ -125,17 +129,23 @@ export default async function handler(
let normalizedCommits: string[] = []; let normalizedCommits: string[] = [];
if (provider === "github") { if (provider === "github") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} else if (provider === "gitlab") { } else if (provider === "gitlab") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} else if (provider === "gitea") { } else if (provider === "gitea") {
normalizedCommits = req.body?.commits?.flatMap( normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
} }
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
@@ -150,9 +160,11 @@ export default async function handler(
} else if (sourceType === "gitea") { } else if (sourceType === "gitea") {
const branchName = extractBranchName(req.headers, req.body); const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap( const normalizedCommits = req.body?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths, composeResult.watchPaths,
+5 -3
View File
@@ -223,9 +223,11 @@ export default async function handler(
const deploymentTitle = extractCommitMessage(req.headers, req.body); const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body); const deploymentHash = extractHash(req.headers, req.body);
const owner = getGithubRepositoryOwner(githubBody); const owner = getGithubRepositoryOwner(githubBody);
const normalizedCommits = githubBody?.commits?.flatMap( const normalizedCommits = githubBody?.commits?.flatMap((commit: any) => [
(commit: any) => commit.modified, ...(commit.added || []),
); ...(commit.modified || []),
...(commit.removed || []),
]);
const apps = await db.query.applications.findMany({ const apps = await db.query.applications.findMany({
where: and( where: and(
@@ -4,6 +4,7 @@ import {
deleteAllMiddlewares, deleteAllMiddlewares,
findApplicationById, findApplicationById,
findEnvironmentById, findEnvironmentById,
findPreviewDeploymentsByApplicationId,
findProjectById, findProjectById,
getAccessibleServerIds, getAccessibleServerIds,
getApplicationStats, getApplicationStats,
@@ -16,6 +17,7 @@ import {
removeDeployments, removeDeployments,
removeDirectoryCode, removeDirectoryCode,
removeMonitoringDirectory, removeMonitoringDirectory,
removePreviewDeployment,
removeService, removeService,
removeTraefikConfig, removeTraefikConfig,
startService, startService,
@@ -234,6 +236,15 @@ export const applicationRouter = createTRPCRouter({
}); });
} }
const previewDeploymentsList =
await findPreviewDeploymentsByApplicationId(input.applicationId);
for (const previewDeployment of previewDeploymentsList) {
try {
await removePreviewDeployment(previewDeployment.previewDeploymentId);
} catch (_) {}
}
const result = await db const result = await db
.delete(applications) .delete(applications)
.where(eq(applications.applicationId, input.applicationId)) .where(eq(applications.applicationId, input.applicationId))
+6 -2
View File
@@ -315,13 +315,17 @@ export const scheduleRouter = createTRPCRouter({
await checkPermission(ctx, { schedule: ["create"] }); await checkPermission(ctx, { schedule: ["create"] });
} }
try { try {
await runCommand(input.scheduleId); const deployment = await runCommand(input.scheduleId);
await audit(ctx, { await audit(ctx, {
action: "run", action: "run",
resourceType: "schedule", resourceType: "schedule",
resourceId: input.scheduleId, resourceId: input.scheduleId,
}); });
return true; return {
status: deployment.status,
deploymentId: deployment.deploymentId,
logPath: deployment.logPath,
};
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
+6
View File
@@ -269,6 +269,12 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
/* Cursor pointer on buttons */
button:not([disabled]),
[role="button"]:not([disabled]) {
cursor: pointer;
}
/* Custom scrollbar styling */ /* Custom scrollbar styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.3125rem; width: 0.3125rem;
+6
View File
@@ -180,6 +180,9 @@ export const initializeJobs = async () => {
where: eq(schedules.enabled, true), where: eq(schedules.enabled, true),
with: { with: {
application: { application: {
columns: {
applicationId: true,
},
with: { with: {
server: true, server: true,
}, },
@@ -227,6 +230,9 @@ export const initializeJobs = async () => {
where: eq(volumeBackups.enabled, true), where: eq(volumeBackups.enabled, true),
with: { with: {
application: { application: {
columns: {
applicationId: true,
},
with: { with: {
server: true, server: true,
}, },
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Application } from "@dokploy/server/services/application"; import type { Application } from "@dokploy/server/services/application";
import type { Environment } from "@dokploy/server/services/environment";
import type { Mount } from "@dokploy/server/services/mount"; import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port"; import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project"; import type { Project } from "@dokploy/server/services/project";
@@ -27,7 +28,7 @@ export const rollbacks = pgTable("rollback", {
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext").$type< fullContext: jsonb("fullContext").$type<
Application & { Application & {
environment: { environment: Environment & {
project: Project; project: Project;
}; };
mounts: Mount[]; mounts: Mount[];
+2 -6
View File
@@ -30,12 +30,8 @@ export const finPortById = async (portId: string) => {
where: eq(ports.portId, portId), where: eq(ports.portId, portId),
with: { with: {
application: { application: {
with: { columns: {
environment: { applicationId: true,
with: {
project: true,
},
},
}, },
}, },
}, },
@@ -17,7 +17,7 @@ import { manageDomain } from "../utils/traefik/domain";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment"; import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain"; import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github"; import { findGithubById, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings"; import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect; export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -142,7 +142,17 @@ export const createPreviewDeployment = async (
org?.ownerId || "", org?.ownerId || "",
); );
const octokit = authGithub(application?.github as Github); if (!application.githubId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Github Account not configured correctly",
});
}
// `findApplicationById` redacts `githubPrivateKey` from the `github`
// relation, so the provider must be refetched to authenticate.
const githubProvider = await findGithubById(application.githubId);
const octokit = authGithub(githubProvider);
const runningComment = getIssueComment( const runningComment = getIssueComment(
application.name, application.name,
+4 -14
View File
@@ -19,6 +19,7 @@ import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker"; import { getRemoteDocker } from "../utils/servers/remote-docker";
import { type Application, findApplicationById } from "./application"; import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment"; import { findDeploymentById } from "./deployment";
import type { Environment } from "./environment";
import type { Mount } from "./mount"; import type { Mount } from "./mount";
import type { Port } from "./port"; import type { Port } from "./port";
import type { Project } from "./project"; import type { Project } from "./project";
@@ -105,19 +106,7 @@ export const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({ const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId), where: eq(rollbacks.rollbackId, rollbackId),
with: { with: {
deployment: { deployment: true,
with: {
application: {
with: {
environment: {
with: {
project: true,
},
},
},
},
},
},
}, },
}); });
@@ -213,7 +202,7 @@ const rollbackApplication = async (
image: string, image: string,
serverId?: string | null, serverId?: string | null,
fullContext?: Application & { fullContext?: Application & {
environment: { environment: Environment & {
project: Project; project: Project;
}; };
mounts: Mount[]; mounts: Mount[];
@@ -275,6 +264,7 @@ const rollbackApplication = async (
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
fullContext.environment.project.env, fullContext.environment.project.env,
fullContext.environment.env,
); );
let rollbackImage = image; let rollbackImage = image;
+5
View File
@@ -81,6 +81,11 @@ export const findScheduleById = async (scheduleId: string) => {
where: eq(schedules.scheduleId, scheduleId), where: eq(schedules.scheduleId, scheduleId),
with: { with: {
application: { application: {
columns: {
applicationId: true,
appName: true,
serverId: true,
},
with: { with: {
environment: { environment: {
with: { with: {
@@ -13,6 +13,11 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
where: eq(volumeBackups.volumeBackupId, volumeBackupId), where: eq(volumeBackups.volumeBackupId, volumeBackupId),
with: { with: {
application: { application: {
columns: {
applicationId: true,
appName: true,
serverId: true,
},
with: { with: {
environment: { environment: {
with: { with: {
@@ -120,7 +120,7 @@ export function parseRawConfig(
if (search) { if (search) {
parsedLogs = parsedLogs.filter((log) => parsedLogs = parsedLogs.filter((log) =>
log.RequestHost.toLowerCase().includes(search.toLowerCase()), (log.RequestHost ?? "").toLowerCase().includes(search.toLowerCase()),
); );
} }
@@ -53,14 +53,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
"build", "build",
"--builder", "--builder",
builderName, builderName,
...(cacheKey "--build-arg",
? [ `secrets-hash=${secretsHash}`,
"--build-arg", ...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg", "--build-arg",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`, `BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f", "-f",
+79 -64
View File
@@ -55,25 +55,46 @@ export const runCommand = async (scheduleId: string) => {
description: "Schedule", description: "Schedule",
}); });
if (scheduleType === "application" || scheduleType === "compose") { try {
let containerId = ""; if (scheduleType === "application" || scheduleType === "compose") {
let serverId = ""; let containerId = "";
if (scheduleType === "application" && application) { let serverId = "";
const container = await getServiceContainer( if (scheduleType === "application" && application) {
application.appName, const container = await getServiceContainer(
application.serverId, application.appName,
); application.serverId,
containerId = container?.Id || ""; );
serverId = application.serverId || ""; containerId = container?.Id || "";
} serverId = application.serverId || "";
if (scheduleType === "compose" && compose) { }
const container = await getComposeContainer(compose, serviceName || ""); if (scheduleType === "compose" && compose) {
containerId = container?.Id || ""; const container = await getComposeContainer(compose, serviceName || "");
serverId = compose.serverId || ""; containerId = container?.Id || "";
} serverId = compose.serverId || "";
}
if (serverId) { if (!containerId) {
try { const target =
scheduleType === "compose"
? `service '${serviceName}' of compose '${compose?.name}'`
: `application '${application?.appName}'`;
const message = `Container not found for ${target}, make sure the service is running`;
if (serverId) {
await execAsyncRemote(
serverId,
`echo ${quote([`❌ ${message}`])} >> ${quote([deployment.logPath])}`,
);
} else {
const writeStream = createWriteStream(deployment.logPath, {
flags: "a",
});
writeStream.write(`❌ ${message}\n`);
writeStream.end();
}
throw new Error(message);
}
if (serverId) {
await execAsyncRemote( await execAsyncRemote(
serverId, serverId,
` `
@@ -86,47 +107,45 @@ export const runCommand = async (scheduleId: string) => {
echo "✅ Command executed successfully" >> ${quote([deployment.logPath])}; echo "✅ Command executed successfully" >> ${quote([deployment.logPath])};
`, `,
); );
} catch (error) { } else {
await updateDeploymentStatus(deployment.deploymentId, "error"); const writeStream = createWriteStream(deployment.logPath, {
throw error; flags: "a",
} });
} else {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
try { try {
if (IS_CLOUD) { if (IS_CLOUD) {
writeStream.write(
"This feature is not available in the cloud version.",
);
writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "error");
return { ...deployment, status: "error" as const };
}
writeStream.write( writeStream.write(
"This feature is not available in the cloud version.", `docker exec ${containerId} ${shellType} -c ${command}\n`,
);
await spawnAsync(
"docker",
["exec", containerId, shellType, "-c", command],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write("✅ Command executed successfully\n");
writeStream.end();
} catch (error) {
writeStream.write("❌ Command failed\n");
writeStream.write(
error instanceof Error ? error.message : "Unknown error",
); );
writeStream.end(); writeStream.end();
return; throw error;
} }
writeStream.write(
`docker exec ${containerId} ${shellType} -c ${command}\n`,
);
await spawnAsync(
"docker",
["exec", containerId, shellType, "-c", command],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write("✅ Command executed successfully\n");
} catch (error) {
writeStream.write("❌ Command failed\n");
writeStream.write(
error instanceof Error ? error.message : "Unknown error",
);
writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
} }
} } else if (scheduleType === "dokploy-server") {
} else if (scheduleType === "dokploy-server") {
try {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
const { SCHEDULES_PATH } = paths(); const { SCHEDULES_PATH } = paths();
const fullPath = path.join(SCHEDULES_PATH, appName || ""); const fullPath = path.join(SCHEDULES_PATH, appName || "");
@@ -151,18 +170,13 @@ export const runCommand = async (scheduleId: string) => {
cwd: fullPath, cwd: fullPath,
}, },
); );
} catch (error) { } else if (scheduleType === "server") {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
} else if (scheduleType === "server") {
try {
const { SCHEDULES_PATH } = paths(true); const { SCHEDULES_PATH } = paths(true);
const fullPath = path.join(SCHEDULES_PATH, appName || ""); const fullPath = path.join(SCHEDULES_PATH, appName || "");
const command = ` const command = `
set -e set -e
echo "Running script" >> ${deployment.logPath}; echo "Running script" >> ${deployment.logPath};
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || { bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath}; echo "❌ Command failed" >> ${deployment.logPath};
exit 1; exit 1;
} }
@@ -177,10 +191,11 @@ export const runCommand = async (scheduleId: string) => {
}); });
} }
}); });
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
} }
await updateDeploymentStatus(deployment.deploymentId, "done");
return { ...deployment, status: "done" as const };
} catch {
await updateDeploymentStatus(deployment.deploymentId, "error");
return { ...deployment, status: "error" as const };
} }
await updateDeploymentStatus(deployment.deploymentId, "done");
}; };