Compare commits

...

28 Commits

Author SHA1 Message Date
Mauricio Siu
9f5c2dbe92 chore: update version to v0.25.4 in package.json 2025-09-28 22:32:35 -06:00
Mauricio Siu
0f9505327f Merge pull request #2710 from SimonLoir/canary
fix: add environment in buildLink for docker compose deploy notifications
2025-09-27 15:14:48 -06:00
Simon Loir
dd2902a57c fix: fix buildLink in docker compose deploy notifications 2025-09-27 16:50:25 +02:00
Mauricio Siu
0138a7c011 Merge pull request #2532 from monntterro/feat/gitea-http-support
feat: support cloning repositories over HTTP in Gitea integration
2025-09-27 03:17:08 -06:00
autofix-ci[bot]
845d2a3ac5 [autofix.ci] apply automated fixes 2025-09-27 09:15:31 +00:00
Mauricio Siu
4033bb84b2 Merge pull request #2640 from amirparsadd/patch-1
feat: support Arvancloud CDN detection
2025-09-27 03:14:12 -06:00
Mauricio Siu
43e96edcdd Merge pull request #2668 from alsmadi99/canary
feat(scheduler): auto-switch to 'Custom' on manual input
2025-09-27 03:13:00 -06:00
Mauricio Siu
2db388536f Merge pull request #2700 from dennisimoo/compose-alert
feat: add unsaved changes tracking and UI indication
2025-09-27 03:09:33 -06:00
Mauricio Siu
43876efc79 Merge pull request #2677 from dennisimoo/fix-position
style: move Deployments tab after Domains tab
2025-09-27 03:07:02 -06:00
Mauricio Siu
e7c7545c02 Merge pull request #2706 from Dokploy/2673-bitbucket-deployments-are-broken-auth-token-wont-work
fix(bitbucket): enhance Bitbucket authentication handling
2025-09-27 02:58:49 -06:00
autofix-ci[bot]
77705381cd [autofix.ci] apply automated fixes 2025-09-27 08:56:28 +00:00
Mauricio Siu
5fdf82a27f refactor(bitbucket): remove debug console logs from repository cloning process
- Removed console logs for clone URL and repository information to clean up the output during the cloning process.
2025-09-27 02:55:42 -06:00
Mauricio Siu
6bd5b1f71f fix(bitbucket): enhance Bitbucket authentication handling
- Added support for Bitbucket email and workspace name in the authentication process.
- Updated the clone URL generation to use the correct format for API tokens.
- Improved error handling to ensure required fields are provided for both API tokens and app passwords.
- Added console logs for debugging clone URL and repository information during cloning.
2025-09-27 02:55:06 -06:00
Mauricio Siu
17d6830b66 Merge pull request #2705 from Dokploy/2670-bug-deployments-are-mark-as-running-when-they-never-ended-vps-shutdown
2670 bug deployments are mark as running when they never ended vps shutdown
2025-09-27 02:23:53 -06:00
Mauricio Siu
a845eba320 Merge pull request #2696 from Harikrishnan1367709/Most-services-has-no-effect-#2691
Feat: "Most services" sorting to count total services across environments -2691
2025-09-27 02:22:58 -06:00
Mauricio Siu
2f4ec9f35f fix(deployment): reintroduce deployment cancellation during server initialization
- Added the call to initCancelDeployments back into the server initialization process to ensure that deployment cancellations are handled correctly in all environments.
2025-09-27 02:21:02 -06:00
autofix-ci[bot]
b725861b55 [autofix.ci] apply automated fixes 2025-09-27 08:20:36 +00:00
Mauricio Siu
6fa8f63277 fix(deployment): correct deployment cancellation logic and ensure proper status update
- Updated the initCancelDeployments function to set the status of running deployments to 'cancelled' instead of 'error'.
- Reintroduced the call to initCancelDeployments in the server initialization process to ensure cancellations are handled correctly.
2025-09-27 02:20:07 -06:00
Mauricio Siu
ac6bdf60ec feat(deployment): add 'cancelled' status to deployment and implement cancellation logic
- Updated the deployment status enum to include 'cancelled'.
- Added a new utility function to handle the cancellation of deployments, setting their status to 'error'.
- Enhanced the status tooltip component to display 'Cancelled' when the status is 'cancelled'.
- Created a new SQL migration to add the 'cancelled' value to the deploymentStatus type.
2025-09-27 02:15:43 -06:00
randomperson12344
db292e6949 feat: add unsaved changes tracking and UI indication 2025-09-26 20:13:09 -07:00
montero
085f6bbbb7 refactor(gitea): extract clone URL construction into a reusable function 2025-09-26 22:01:54 +03:00
autofix-ci[bot]
cbdc4e4a20 [autofix.ci] apply automated fixes 2025-09-26 08:48:23 +00:00
HarikrishnanD
ee3ff18feb fix: correct "Most services" sorting to count total services across environments - Fix sorting logic to count actual services instead of environment count - Projects now properly sort by total service count in descending order - Resolves issue where "Most services" showed ascending order instead of descending -#2691 2025-09-26 14:15:58 +05:30
randomperson12344
f5084dd5fb feat(ui): move Deployments tab to position 4 after Domains tab 2025-09-23 19:23:43 -07:00
autofix-ci[bot]
1b603d84d7 [autofix.ci] apply automated fixes 2025-09-22 19:11:08 +00:00
Mohammad Alsmadi
cf2c89d136 feat(scheduler): auto-switch to 'Custom' on manual input 2025-09-22 13:35:52 +04:00
Amirparsa Baghdadi
95de98e94d close string 2025-09-22 12:37:21 +03:30
Amirparsa Baghdadi
4416ca9cd2 Add arvancloud to CDNs 2025-09-19 15:58:22 +03:30
19 changed files with 6857 additions and 211 deletions

View File

@@ -7,7 +7,7 @@ import {
RefreshCw, RefreshCw,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { type Control, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" }, { label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
{ label: "Every 15 minutes", value: "*/15 * * * *" }, { label: "Every 15 minutes", value: "*/15 * * * *" },
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" }, { label: "Every weekday at midnight", value: "0 0 * * 1-5" },
{ label: "Custom", value: "custom" },
]; ];
const formSchema = z const formSchema = z
@@ -115,10 +116,91 @@ interface Props {
scheduleType?: "application" | "compose" | "server" | "dokploy-server"; scheduleType?: "application" | "compose" | "server" | "dokploy-server";
} }
export const ScheduleFormField = ({
name,
formControl,
}: {
name: string;
formControl: Control<any>;
}) => {
const [selectedOption, setSelectedOption] = useState("");
return (
<FormField
control={formControl}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>Cron expression format: minute hour day month weekday</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value);
field.onChange(value === "custom" ? "" : value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label}
{expr.value !== "custom" && ` (${expr.value})`}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
onChange={(e) => {
const value = e.target.value;
const commonExpression = commonCronExpressions.find(
(expression) => expression.value === value,
);
if (commonExpression) {
setSelectedOption(commonExpression.value);
} else {
setSelectedOption("custom");
}
field.onChange(e);
}}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
);
};
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils(); const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)} )}
/> />
<FormField <ScheduleFormField
control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( formControl={form.control}
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
{(scheduleTypeForm === "application" || {(scheduleTypeForm === "application" ||

View File

@@ -1,11 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -47,7 +41,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { CacheType } from "../domains/handle-domain"; import type { CacheType } from "../domains/handle-domain";
import { commonCronExpressions } from "../schedules/handle-schedules"; import { ScheduleFormField } from "../schedules/handle-schedules";
const formSchema = z const formSchema = z
.object({ .object({
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
</FormItem> </FormItem>
)} )}
/> />
<ScheduleFormField
<FormField
control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( formControl={form.control}
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<FormField <FormField

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
); );
const { mutateAsync, isLoading } = api.compose.update.useMutation(); const { mutateAsync, isLoading } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm<AddComposeFile>({ const form = useForm<AddComposeFile>({
defaultValues: { defaultValues: {
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
useEffect(() => {
if (data?.composeFile !== undefined) {
setHasUnsavedChanges(composeFile !== data.composeFile);
}
}, [composeFile, data?.composeFile]);
const onSubmit = async (data: AddComposeFile) => { const onSubmit = async (data: AddComposeFile) => {
const { valid, error } = validateAndFormatYAML(data.composeFile); const { valid, error } = validateAndFormatYAML(data.composeFile);
if (!valid) { if (!valid) {
@@ -71,6 +78,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Compose config Updated"); toast.success("Compose config Updated");
setHasUnsavedChanges(false);
refetch(); refetch();
await utils.compose.getConvertedCompose.invalidate({ await utils.compose.getConvertedCompose.invalidate({
composeId, composeId,
@@ -99,6 +107,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return ( return (
<> <>
<div className="w-full flex flex-col gap-4 "> <div className="w-full flex flex-col gap-4 ">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">Compose File</h3>
<p className="text-sm text-muted-foreground">
Configure your Docker Compose file for this service.
{hasUnsavedChanges && (
<span className="text-yellow-500 ml-2">
(You have unsaved changes)
</span>
)}
</p>
</div>
</div>
<Form {...form}> <Form {...form}>
<form <form
id="hook-form-save-compose-file" id="hook-form-save-compose-file"

View File

@@ -3,7 +3,6 @@ import {
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
DatabaseZap, DatabaseZap,
Info,
PenBoxIcon, PenBoxIcon,
PlusIcon, PlusIcon,
RefreshCw, RefreshCw,
@@ -62,7 +61,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { commonCronExpressions } from "../../application/schedules/handle-schedules"; import { ScheduleFormField } from "../../application/schedules/handle-schedules";
type CacheType = "cache" | "fetch"; type CacheType = "cache" | "fetch";
@@ -579,66 +578,9 @@ export const HandleBackup = ({
); );
}} }}
/> />
<FormField
control={form.control} <ScheduleFormField name="schedule" formControl={form.control} />
name="schedule"
render={({ field }) => {
return (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField <FormField
control={form.control} control={form.control}
name="prefix" name="prefix"

View File

@@ -96,8 +96,30 @@ export const ShowProjects = () => {
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break; break;
case "services": { case "services": {
const aTotalServices = a.environments.length; const aTotalServices = a.environments.reduce((total, env) => {
const bTotalServices = b.environments.length; return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
const bTotalServices = b.environments.reduce((total, env) => {
return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
comparison = aTotalServices - bTotalServices; comparison = aTotalServices - bTotalServices;
break; break;
} }

View File

@@ -7,7 +7,14 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface Props { interface Props {
status: "running" | "error" | "done" | "idle" | undefined | null; status:
| "running"
| "error"
| "done"
| "idle"
| "cancelled"
| undefined
| null;
className?: string; className?: string;
} }
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
className={cn("size-3.5 rounded-full bg-green-500", className)} className={cn("size-3.5 rounded-full bg-green-500", className)}
/> />
)} )}
{status === "cancelled" && (
<div
className={cn(
"size-3.5 rounded-full bg-muted-foreground",
className,
)}
/>
)}
{status === "running" && ( {status === "running" && (
<div <div
className={cn("size-3.5 rounded-full bg-yellow-500", className)} className={cn("size-3.5 rounded-full bg-yellow-500", className)}
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
{status === "error" && "Error"} {status === "error" && "Error"}
{status === "done" && "Done"} {status === "done" && "Done"}
{status === "running" && "Running"} {status === "running" && "Running"}
{status === "cancelled" && "Cancelled"}
</span> </span>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';

File diff suppressed because it is too large Load Diff

View File

@@ -792,6 +792,13 @@
"when": 1758483520214, "when": 1758483520214,
"tag": "0112_freezing_skrulls", "tag": "0112_freezing_skrulls",
"breakpoints": true "breakpoints": true
},
{
"idx": 113,
"version": "7",
"when": 1758960816504,
"tag": "0113_complete_rafael_vega",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.25.3", "version": "v0.25.4",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -226,6 +226,7 @@ const Service = (
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger> <TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="preview-deployments"> <TabsTrigger value="preview-deployments">
Preview Deployments Preview Deployments
</TabsTrigger> </TabsTrigger>
@@ -233,7 +234,6 @@ const Service = (
<TabsTrigger value="volume-backups"> <TabsTrigger value="volume-backups">
Volume Backups Volume Backups
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && ( {((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger> <TabsTrigger value="monitoring">Monitoring</TabsTrigger>

View File

@@ -8,6 +8,7 @@ import {
initializeNetwork, initializeNetwork,
initSchedules, initSchedules,
initVolumeBackupsCronJobs, initVolumeBackupsCronJobs,
initCancelDeployments,
sendDokployRestartNotifications, sendDokployRestartNotifications,
setupDirectories, setupDirectories,
} from "@dokploy/server"; } from "@dokploy/server";
@@ -52,6 +53,7 @@ void app.prepare().then(async () => {
await migration(); await migration();
await initCronJobs(); await initCronJobs();
await initSchedules(); await initSchedules();
await initCancelDeployments();
await initVolumeBackupsCronJobs(); await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications(); await sendDokployRestartNotifications();
} }

View File

@@ -21,6 +21,7 @@ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",
"done", "done",
"error", "error",
"cancelled",
]); ]);
export const deployments = pgTable("deployment", { export const deployments = pgTable("deployment", {

View File

@@ -68,6 +68,7 @@ export * from "./utils/backups/postgres";
export * from "./utils/backups/utils"; export * from "./utils/backups/utils";
export * from "./utils/backups/web-server"; export * from "./utils/backups/web-server";
export * from "./utils/builders/compose"; export * from "./utils/builders/compose";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/builders/docker-file"; export * from "./utils/builders/docker-file";
export * from "./utils/builders/drop"; export * from "./utils/builders/drop";
export * from "./utils/builders/heroku"; export * from "./utils/builders/heroku";

View File

@@ -603,6 +603,21 @@ const BUNNY_CDN_IPS = new Set([
"89.187.184.176", "89.187.184.176",
]); ]);
// Arvancloud IP ranges
// https://www.arvancloud.ir/fa/ips.txt
const ARVANCLOUD_IP_RANGES = [
"185.143.232.0/22",
"188.229.116.16/29",
"94.101.182.0/27",
"2.144.3.128/28",
"89.45.48.64/28",
"37.32.16.0/27",
"37.32.17.0/27",
"37.32.18.0/27",
"37.32.19.0/27",
"185.215.232.0/22",
];
const CDN_PROVIDERS: CDNProvider[] = [ const CDN_PROVIDERS: CDNProvider[] = [
{ {
name: "cloudflare", name: "cloudflare",
@@ -627,6 +642,14 @@ const CDN_PROVIDERS: CDNProvider[] = [
warningMessage: warningMessage:
"Domain is behind Fastly - actual IP is masked by CDN proxy", "Domain is behind Fastly - actual IP is masked by CDN proxy",
}, },
{
name: "arvancloud",
displayName: "Arvancloud",
checkIp: (ip: string) =>
ARVANCLOUD_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
warningMessage:
"Domain is behind Arvancloud - actual IP is masked by CDN proxy",
},
]; ];
export const detectCDNProvider = (ip: string): CDNProvider | null => { export const detectCDNProvider = (ip: string): CDNProvider | null => {

View File

@@ -227,7 +227,7 @@ export const deployCompose = async ({
const buildLink = `${await getDokployUrl()}/dashboard/project/${ const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`; }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,
@@ -335,7 +335,7 @@ export const deployRemoteCompose = async ({
const buildLink = `${await getDokployUrl()}/dashboard/project/${ const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`; }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,

View File

@@ -31,29 +31,51 @@ export const getBitbucketCloneUrl = (
apiToken?: string | null; apiToken?: string | null;
bitbucketUsername?: string | null; bitbucketUsername?: string | null;
appPassword?: string | null; appPassword?: string | null;
bitbucketEmail?: string | null;
bitbucketWorkspaceName?: string | null;
} | null, } | null,
repoClone: string, repoClone: string,
) => { ) => {
if (!bitbucketProvider) { if (!bitbucketProvider) {
throw new Error("Bitbucket provider is required"); throw new Error("Bitbucket provider is required");
} }
return bitbucketProvider.apiToken
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}` if (bitbucketProvider.apiToken) {
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`; return `https://x-bitbucket-api-token-auth:${bitbucketProvider.apiToken}@${repoClone}`;
}
// For app passwords, use username:app_password format
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
throw new Error(
"Username and app password are required when not using API token",
);
}
return `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
}; };
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => { export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
if (bitbucketProvider.apiToken) { if (bitbucketProvider.apiToken) {
// For API tokens, use HTTP Basic auth with email and token // According to Bitbucket official docs, for API calls with API tokens:
// According to Bitbucket docs: email:token for API calls // "You will need both your Atlassian account email and an API token"
const email = // Use: {atlassian_account_email}:{api_token}
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
if (!bitbucketProvider.bitbucketEmail) {
throw new Error(
"Atlassian account email is required when using API token for API calls",
);
}
return { return {
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`, Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketEmail}:${bitbucketProvider.apiToken}`).toString("base64")}`,
}; };
} }
// For app passwords, use HTTP Basic auth with username and app password // For app passwords, use HTTP Basic auth with username and app password
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
throw new Error(
"Username and app password are required when not using API token",
);
}
return { return {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`, Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
}; };

View File

@@ -99,6 +99,19 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {
} }
}; };
const buildGiteaCloneUrl = (
giteaUrl: string,
accessToken: string,
owner: string,
repository: string,
) => {
const protocol = giteaUrl.startsWith("http://") ? "http" : "https";
const baseUrl = giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${owner}/${repository}.git`;
const cloneUrl = `${protocol}://oauth2:${accessToken}@${baseUrl}/${repoClone}`;
return cloneUrl;
};
export type ApplicationWithGitea = InferResultType< export type ApplicationWithGitea = InferResultType<
"applications", "applications",
{ gitea: true } { gitea: true }
@@ -148,9 +161,13 @@ export const getGiteaCloneCommand = async (
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`; const cloneUrl = buildGiteaCloneUrl(
gitea?.giteaUrl!,
gitea?.accessToken!,
giteaOwner!,
giteaRepository!,
);
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
@@ -205,8 +222,12 @@ export const cloneGiteaRepository = async (
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); const cloneUrl = buildGiteaCloneUrl(
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; giteaProvider.giteaUrl,
giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`); writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
@@ -269,9 +290,12 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const cloneUrl = buildGiteaCloneUrl(
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); giteaProvider.giteaUrl,
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
try { try {
await spawnAsync("git", [ await spawnAsync("git", [
@@ -317,9 +341,13 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
const basePath = COMPOSE_PATH; const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const cloneUrl = buildGiteaCloneUrl(
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); giteaProvider.giteaUrl,
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};

View File

@@ -0,0 +1,21 @@
import { deployments } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { db } from "../../db/index";
export const initCancelDeployments = async () => {
try {
console.log("Setting up cancel deployments....");
const result = await db
.update(deployments)
.set({
status: "cancelled",
})
.where(eq(deployments.status, "running"))
.returning();
console.log(`Cancelled ${result.length} deployments`);
} catch (error) {
console.error(error);
}
};