Compare commits

..

2 Commits

Author SHA1 Message Date
Mauricio Siu
80d5313dd8 Merge branch 'canary' into 1365-create-preview-deployment-using-api 2025-07-13 20:49:12 -06:00
Mauricio Siu
da0e726326 feat(preview-deployment): enhance external deployment support
- Add support for external preview deployments with optional GitHub comment handling
- Modify deployment services to conditionally update GitHub issue comments
- Update queue types and deployment worker to handle external deployment flag
- Refactor preview deployment creation to support external deployments
- Improve preview deployment router with more flexible deployment creation logic
2025-03-08 17:07:07 -06:00
74 changed files with 711 additions and 7083 deletions

View File

@@ -2,8 +2,7 @@ name: Build Docker images
on: on:
push: push:
branches: [main, canary] branches: ["canary", "main", "feat/monitoring"]
workflow_dispatch:
jobs: jobs:
build-and-push-cloud-image: build-and-push-cloud-image:

View File

@@ -2,8 +2,7 @@ name: Dokploy Docker Build
on: on:
push: push:
branches: [main, canary] branches: [main, canary, "1061-custom-docker-service-hostname"]
workflow_dispatch:
env: env:
IMAGE_NAME: dokploy/dokploy IMAGE_NAME: dokploy/dokploy

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Setup biomeJs - name: Setup biomeJs
uses: biomejs/setup-biome@v2 uses: biomejs/setup-biome@v2
- name: Run Biome formatter - name: Run Biome formatter
run: biome format --write run: biome format . --write
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["biomejs.biome"]
}

View File

@@ -1,8 +0,0 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}

View File

@@ -29,9 +29,5 @@
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0"
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
} }

View File

@@ -29,7 +29,6 @@ const baseApp: ApplicationNested = {
herokuVersion: "", herokuVersion: "",
giteaBranch: "", giteaBranch: "",
giteaBuildPath: "", giteaBuildPath: "",
previewRequireCollaboratorPermissions: false,
giteaId: "", giteaId: "",
giteaOwner: "", giteaOwner: "",
giteaRepository: "", giteaRepository: "",

View File

@@ -18,7 +18,6 @@ const baseApp: ApplicationNested = {
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false, enableSubmodules: false,
previewRequireCollaboratorPermissions: false,
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",

View File

@@ -151,7 +151,7 @@ export const HandleSecurity = ({
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input placeholder="test" type="password" {...field} /> <Input placeholder="test" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -7,9 +7,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react"; import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -61,18 +58,19 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<div className="flex flex-col gap-6 "> <div className="flex flex-col gap-6 ">
{data?.security.map((security) => ( {data?.security.map((security) => (
<div key={security.securityId}> <div key={security.securityId}>
<div className="flex w-full flex-col md:flex-row justify-between md:items-center gap-4 md:gap-10 border rounded-lg p-4"> <div className="flex w-full flex-col sm:flex-row justify-between sm:items-center gap-4 sm:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 flex-col gap-4 md:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-1">
<Label>Username</Label> <span className="font-medium">Username</span>
<Input disabled value={security.username} /> <span className="text-sm text-muted-foreground">
{security.username}
</span>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-1">
<Label>Password</Label> <span className="font-medium">Password</span>
<ToggleVisibilityInput <span className="text-sm text-muted-foreground">
value={security.password} {security.password}
disabled </span>
/>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">

View File

@@ -1,5 +1,4 @@
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -11,13 +10,14 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api"; import { type RouterOutputs, api } from "@/utils/api";
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues"; import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token"; import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment"; import { ShowDeployment } from "./show-deployment";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { DialogAction } from "@/components/shared/dialog-action";
import { toast } from "sonner";
interface Props { interface Props {
id: string; id: string;

View File

@@ -46,7 +46,6 @@ const schema = z
previewPath: z.string(), previewPath: z.string(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
previewCustomCertResolver: z.string().optional(), previewCustomCertResolver: z.string().optional(),
previewRequireCollaboratorPermissions: z.boolean(),
}) })
.superRefine((input, ctx) => { .superRefine((input, ctx) => {
if ( if (
@@ -84,7 +83,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewHttps: false, previewHttps: false,
previewPath: "/", previewPath: "/",
previewCertificateType: "none", previewCertificateType: "none",
previewRequireCollaboratorPermissions: true,
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
@@ -107,8 +105,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: data.previewPath || "/", previewPath: data.previewPath || "/",
previewCertificateType: data.previewCertificateType || "none", previewCertificateType: data.previewCertificateType || "none",
previewCustomCertResolver: data.previewCustomCertResolver || "", previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
data.previewRequireCollaboratorPermissions || true,
}); });
} }
}, [data]); }, [data]);
@@ -125,8 +121,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: formData.previewPath, previewPath: formData.previewPath,
previewCertificateType: formData.previewCertificateType, previewCertificateType: formData.previewCertificateType,
previewCustomCertResolver: formData.previewCustomCertResolver, previewCustomCertResolver: formData.previewCustomCertResolver,
previewRequireCollaboratorPermissions:
formData.previewRequireCollaboratorPermissions,
}) })
.then(() => { .then(() => {
toast.success("Preview Deployments settings updated"); toast.success("Preview Deployments settings updated");
@@ -318,37 +312,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
</div> </div>
</div> </div>
<div className="grid gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="previewRequireCollaboratorPermissions"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
<div className="space-y-0.5">
<FormLabel>
Require Collaborator Permissions
</FormLabel>
<FormDescription>
Require collaborator permissions to preview
deployments, valid roles are:
<ul>
<li>Admin</li>
<li>Maintain</li>
<li>Write</li>
</ul>
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="env" name="env"

View File

@@ -1,4 +1,3 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DrawerLogs } from "@/components/shared/drawer-logs"; import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -43,8 +42,9 @@ import { 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";
import { formatBytes } from "../../database/backups/restore-backup";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { formatBytes } from "../../database/backups/restore-backup";
import { AlertBlock } from "@/components/shared/alert-block";
interface Props { interface Props {
id: string; id: string;

View File

@@ -23,8 +23,8 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleVolumeBackups } from "./handle-volume-backups"; import { HandleVolumeBackups } from "./handle-volume-backups";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { RestoreVolumeBackups } from "./restore-volume-backups"; import { RestoreVolumeBackups } from "./restore-volume-backups";
interface Props { interface Props {

View File

@@ -1,4 +1,3 @@
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { import {
BitbucketIcon, BitbucketIcon,
GitIcon, GitIcon,
@@ -12,7 +11,6 @@ import { api } from "@/utils/api";
import { CodeIcon, GitBranch, Loader2 } from "lucide-react"; import { CodeIcon, GitBranch, Loader2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
import { ComposeFileEditor } from "../compose-file-editor"; import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose"; import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose"; import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
@@ -20,6 +18,8 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose"; import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose"; import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { toast } from "sonner";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea"; type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props { interface Props {

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -43,6 +37,12 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddTemplateSchema = z.object({ const AddTemplateSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -75,8 +75,6 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation(); api.application.create.useMutation();
@@ -157,64 +155,62 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && ( <FormField
<FormField control={form.control}
control={form.control} name="serverId"
name="serverId" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> Select a Server {!isCloud ? "(Optional)" : ""}
Select a Server {!isCloud ? "(Optional)" : ""} <HelpCircle className="size-4 text-muted-foreground" />
<HelpCircle className="size-4 text-muted-foreground" /> </FormLabel>
</FormLabel> </TooltipTrigger>
</TooltipTrigger> <TooltipContent
<TooltipContent className="z-[999] w-[300px]"
className="z-[999] w-[300px]" align="start"
align="start" side="top"
side="top" >
> <span>
<span> If no server is selected, the application will be
If no server is selected, the application will be deployed on the server where the user is logged in.
deployed on the server where the user is logged in. </span>
</span> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</SelectItem> </span>
))} </SelectItem>
<SelectLabel>Servers ({servers?.length})</SelectLabel> ))}
</SelectGroup> <SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
)} />
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -43,6 +37,12 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddComposeSchema = z.object({ const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,8 +78,6 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation(); api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({ const form = useForm<AddCompose>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -165,64 +163,62 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)} )}
/> />
</div> </div>
{hasServers && ( <FormField
<FormField control={form.control}
control={form.control} name="serverId"
name="serverId" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> Select a Server {!isCloud ? "(Optional)" : ""}
Select a Server {!isCloud ? "(Optional)" : ""} <HelpCircle className="size-4 text-muted-foreground" />
<HelpCircle className="size-4 text-muted-foreground" /> </FormLabel>
</FormLabel> </TooltipTrigger>
</TooltipTrigger> <TooltipContent
<TooltipContent className="z-[999] w-[300px]"
className="z-[999] w-[300px]" align="start"
align="start" side="top"
side="top" >
> <span>
<span> If no server is selected, the application will be
If no server is selected, the application will be deployed on the server where the user is logged in.
deployed on the server where the user is logged in. </span>
</span> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</SelectItem> </span>
))} </SelectItem>
<SelectLabel>Servers ({servers?.length})</SelectLabel> ))}
</SelectGroup> <SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
)} />
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,9 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -45,6 +39,12 @@ import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
type DbType = typeof mySchema._type.type; type DbType = typeof mySchema._type.type;
@@ -163,8 +163,6 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation(); const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({ const form = useForm<AddDatabase>({
defaultValues: { defaultValues: {
type: "postgres", type: "postgres",
@@ -376,41 +374,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && ( <FormField
<FormField control={form.control}
control={form.control} name="serverId"
name="serverId" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>Select a Server</FormLabel>
<FormLabel>Select a Server</FormLabel> <Select
<Select onValueChange={field.onChange}
onValueChange={field.onChange} defaultValue={field.value || ""}
defaultValue={field.value || ""} >
> <SelectTrigger>
<SelectTrigger> <SelectValue placeholder="Select a Server" />
<SelectValue placeholder="Select a Server" /> </SelectTrigger>
</SelectTrigger> <SelectContent>
<SelectContent> <SelectGroup>
<SelectGroup> {servers?.map((server) => (
{servers?.map((server) => ( <SelectItem
<SelectItem key={server.serverId}
key={server.serverId} value={server.serverId}
value={server.serverId} >
> {server.name}
{server.name} </SelectItem>
</SelectItem> ))}
))} <SelectLabel>
<SelectLabel> Servers ({servers?.length})
Servers ({servers?.length}) </SelectLabel>
</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/>
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,18 +1,3 @@
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { import {
@@ -69,6 +54,21 @@ 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 {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
@@ -137,8 +137,6 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery; return matchesTags && matchesQuery;
}) || []; }) || [];
const hasServers = servers && servers.length > 0;
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
@@ -427,62 +425,60 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project. project.
</AlertDialogDescription> </AlertDialogDescription>
{hasServers && ( <div>
<div> <TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={0}> <Tooltip>
<Tooltip> <TooltipTrigger asChild>
<TooltipTrigger asChild> <Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5"> Select a Server{" "}
Select a Server{" "} {!isCloud ? "(Optional)" : ""}
{!isCloud ? "(Optional)" : ""} <HelpCircle className="size-4 text-muted-foreground" />
<HelpCircle className="size-4 text-muted-foreground" /> </Label>
</Label> </TooltipTrigger>
</TooltipTrigger> <TooltipContent
<TooltipContent className="z-[999] w-[300px]"
className="z-[999] w-[300px]" align="start"
align="start" side="top"
side="top" >
> <span>
<span> If no server is selected, the application
If no server is selected, the will be deployed on the server where the
application will be deployed on the user is logged in.
server where the user is logged in. </span>
</span> </TooltipContent>
</TooltipContent> </Tooltip>
</Tooltip> </TooltipProvider>
</TooltipProvider>
<Select <Select
onValueChange={(e) => { onValueChange={(e) => {
setServerId(e); setServerId(e);
}} }}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</SelectItem> </span>
))} </SelectItem>
<SelectLabel> ))}
Servers ({servers?.length}) <SelectLabel>
</SelectLabel> Servers ({servers?.length})
</SelectGroup> </SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
</div> </Select>
)} </div>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>

View File

@@ -25,7 +25,6 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => { export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API // Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => { const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example }); setTemplateInfo({ ...templateInfo, userInput: example });
@@ -48,39 +47,37 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/> />
</div> </div>
{hasServers && ( <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor="server-deploy">
<Label htmlFor="server-deploy"> Select the server where you want to deploy (optional)
Select the server where you want to deploy (optional) </Label>
</Label> <Select
<Select value={templateInfo.server?.serverId}
value={templateInfo.server?.serverId} onValueChange={(value) => {
onValueChange={(value) => { const server = servers?.find((s) => s.serverId === value);
const server = servers?.find((s) => s.serverId === value); if (server) {
if (server) { setTemplateInfo({
setTemplateInfo({ ...templateInfo,
...templateInfo, server: server,
server: server, });
}); }
} }}
}} >
> <SelectTrigger className="w-full">
<SelectTrigger className="w-full"> <SelectValue placeholder="Select a server" />
<SelectValue placeholder="Select a server" /> </SelectTrigger>
</SelectTrigger> <SelectContent>
<SelectContent> <SelectGroup>
<SelectGroup> {servers?.map((server) => (
{servers?.map((server) => ( <SelectItem key={server.serverId} value={server.serverId}>
<SelectItem key={server.serverId} value={server.serverId}> {server.name}
{server.name} </SelectItem>
</SelectItem> ))}
))} <SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>Servers ({servers?.length})</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> </div>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>

View File

@@ -199,7 +199,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Generating template suggestions based on your input... Generating template suggestions based on your input...
</p> </p>
<pre className="whitespace-normal">{templateInfo.userInput}</pre> <pre>{templateInfo.userInput}</pre>
</div> </div>
); );
} }

View File

@@ -70,7 +70,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
}, },
{ {
enabled: !!destinationId, enabled: !!destinationId,
refetchOnWindowFocus: false,
}, },
); );
const { const {

View File

@@ -33,7 +33,6 @@ import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider"; import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider"; import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider"; import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { Badge } from "@/components/ui/badge";
export const ShowGitProviders = () => { export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery(); const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
@@ -159,13 +158,7 @@ export const ShowGitProviders = () => {
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
{!haveGithubRequirements && isGithub && ( {!haveGithubRequirements && isGithub && (
<div className="flex flex-row gap-1 items-center"> <div className="flex flex-col gap-1">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`} href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({ className={buttonVariants({
@@ -192,13 +185,7 @@ export const ShowGitProviders = () => {
</div> </div>
)} )}
{!haveGitlabRequirements && isGitlab && ( {!haveGitlabRequirements && isGitlab && (
<div className="flex flex-row gap-1 items-center"> <div className="flex flex-col gap-1">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={getGitlabUrl( href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "", gitProvider.gitlab?.applicationId || "",

View File

@@ -36,7 +36,6 @@ const profileSchema = z.object({
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),
name: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false), allowImpersonation: z.boolean().optional().default(false),
}); });
@@ -85,7 +84,6 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: "", currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false, allowImpersonation: data?.user?.allowImpersonation || false,
name: data?.user?.name || "",
}, },
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema),
}); });
@@ -99,7 +97,6 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "", currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation, allowImpersonation: data?.user?.allowImpersonation,
name: data?.user?.name || "",
}, },
{ {
keepValues: true, keepValues: true,
@@ -122,7 +119,6 @@ export const ProfileForm = () => {
image: values.image, image: values.image,
currentPassword: values.currentPassword || undefined, currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation, allowImpersonation: values.allowImpersonation,
name: values.name || undefined,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@@ -132,7 +128,6 @@ export const ProfileForm = () => {
password: "", password: "",
image: values.image, image: values.image,
currentPassword: "", currentPassword: "",
name: values.name || "",
}); });
}) })
.catch(() => { .catch(() => {
@@ -172,19 +167,6 @@ export const ProfileForm = () => {
className="grid gap-4" className="grid gap-4"
> >
<div className="space-y-4"> <div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"

View File

@@ -20,9 +20,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
}, },
); );
const enabled = serverId const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup;
? server?.enableDockerCleanup
: data?.user.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();

View File

@@ -126,7 +126,7 @@ export const UpdateServer = ({
</TooltipProvider> </TooltipProvider>
)} )}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-lg"> <DialogContent className="max-w-lg p-6">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<DialogTitle className="text-2xl font-semibold"> <DialogTitle className="text-2xl font-semibold">
Web Server Update Web Server Update
@@ -253,7 +253,7 @@ export const UpdateServer = ({
<ToggleAutoCheckUpdates disabled={isLoading} /> <ToggleAutoCheckUpdates disabled={isLoading} />
</div> </div>
<div className="space-y-4 flex items-center justify-end mt-4 "> <div className="space-y-4 flex items-center justify-end">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => onOpenChange?.(false)}> <Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel Cancel

View File

@@ -96,7 +96,10 @@ type SingleNavItem = {
title: string; title: string;
url: string; url: string;
icon?: LucideIcon; icon?: LucideIcon;
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; isEnabled?: (opts: {
auth?: AuthQueryOutput;
isCloud: boolean;
}) => boolean;
}; };
// NavItem type // NavItem type
@@ -122,7 +125,10 @@ type ExternalLink = {
name: string; name: string;
url: string; url: string;
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }>;
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; isEnabled?: (opts: {
auth?: AuthQueryOutput;
isCloud: boolean;
}) => boolean;
}; };
// Menu type // Menu type

View File

@@ -1 +0,0 @@
ALTER TABLE "application" ADD COLUMN "previewRequireCollaboratorPermissions" boolean DEFAULT true;

File diff suppressed because it is too large Load Diff

View File

@@ -722,13 +722,6 @@
"when": 1751848685503, "when": 1751848685503,
"tag": "0102_opposite_grandmaster", "tag": "0102_opposite_grandmaster",
"breakpoints": true "breakpoints": true
},
{
"idx": 103,
"version": "7",
"when": 1752465764072,
"tag": "0103_cultured_pestilence",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.24.4", "version": "v0.24.2",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -187,10 +187,10 @@
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.25.2" "initVersion": "7.25.2"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {
"*": [ "*": [
@@ -198,8 +198,6 @@
] ]
}, },
"commitlint": { "commitlint": {
"extends": [ "extends": ["@commitlint/config-conventional"]
"@commitlint/config-conventional"
]
} }
} }

View File

@@ -5,10 +5,7 @@ import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { import {
IS_CLOUD, IS_CLOUD,
checkUserRepositoryPermissions,
createPreviewDeployment, createPreviewDeployment,
createSecurityBlockedComment,
findGithubById,
findPreviewDeploymentByApplicationId, findPreviewDeploymentByApplicationId,
findPreviewDeploymentsByPullRequestId, findPreviewDeploymentsByPullRequestId,
removePreviewDeployment, removePreviewDeployment,
@@ -349,18 +346,6 @@ export default async function handler(
const deploymentHash = githubBody?.pull_request?.head?.sha; const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref; const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login; const owner = githubBody?.repository?.owner?.login;
const prAuthor = githubBody?.pull_request?.user?.login;
// Validate PR author information is present
if (!prAuthor) {
console.warn(
"⚠️ SECURITY: PR author information missing in webhook payload",
);
res.status(400).json({
message: "PR author information missing",
});
return;
}
const apps = await db.query.applications.findMany({ const apps = await db.query.applications.findMany({
where: and( where: and(
@@ -376,72 +361,13 @@ export default async function handler(
}, },
}); });
// SECURITY: Check collaborator permissions per application setting
const secureApps: typeof apps = [];
const blockedApps: string[] = [];
let userPermission: string | null = null;
for (const app of apps) {
// If the app requires collaborator permissions, verify them
if (app.previewRequireCollaboratorPermissions !== false) {
try {
const githubProvider = await findGithubById(githubResult.githubId);
const { hasWriteAccess, permission } =
await checkUserRepositoryPermissions(
githubProvider,
owner,
repository,
prAuthor,
);
userPermission = permission; // Store permission for comment
if (!hasWriteAccess) {
console.warn(
`🚨 SECURITY: Blocked preview deployment for ${app.name} from unauthorized user ${prAuthor} on ${owner}/${repository}. Permission: ${permission || "none"}`,
);
blockedApps.push(app.name);
continue;
}
console.log(
`✅ SECURITY: Preview deployment authorized for ${app.name} from user ${prAuthor} on ${owner}/${repository}. Permission: ${permission}`,
);
} catch (error) {
console.error(
`Error validating PR author permissions for ${app.name}:`,
error,
);
blockedApps.push(app.name);
continue; // Skip this app on error
}
} else {
console.warn(
`⚠️ SECURITY: Preview deployment for ${app.name} allows deployment from any PR author (security check disabled)`,
);
}
secureApps.push(app);
}
const prBranch = githubBody?.pull_request?.head?.ref; const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number; const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title; const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url; const prURL = githubBody?.pull_request?.html_url;
// Create security notification comment if any apps were blocked for (const app of apps) {
if (blockedApps.length > 0) {
await createSecurityBlockedComment({
owner,
repository,
prNumber: Number.parseInt(prNumber),
prAuthor,
permission: userPermission,
githubId: githubResult.githubId,
});
}
for (const app of secureApps) {
const previewLimit = app?.previewLimit || 0; const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) { if (app?.previewDeployments?.length > previewLimit) {
continue; continue;

View File

@@ -37,6 +37,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";

View File

@@ -33,6 +33,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";

View File

@@ -28,8 +28,6 @@ import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects"; import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis"; import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry"; import { registryRouter } from "./routers/registry";
import { rollbackRouter } from "./routers/rollbacks";
import { scheduleRouter } from "./routers/schedule";
import { securityRouter } from "./routers/security"; import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server"; import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings"; import { settingsRouter } from "./routers/settings";
@@ -37,6 +35,8 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe"; import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm"; import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user"; import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
import { volumeBackupsRouter } from "./routers/volume-backups"; import { volumeBackupsRouter } from "./routers/volume-backups";
/** /**
* This is the primary router for your server. * This is the primary router for your server.

View File

@@ -20,8 +20,8 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure

View File

@@ -8,9 +8,9 @@ import {
getServiceContainersByAppName, getServiceContainersByAppName,
getStackContainersByAppName, getStackContainersByAppName,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/; export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;

View File

@@ -12,8 +12,8 @@ import {
getServiceContainer, getServiceContainer,
updateMount, updateMount,
} from "@dokploy/server"; } from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const mountRouter = createTRPCRouter({ export const mountRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure

View File

@@ -1,13 +1,23 @@
import { apiFindAllByApplication } from "@/server/db/schema"; import { db } from "@/server/db";
import { apiFindAllByApplication, applications } from "@/server/db/schema";
import { import {
createPreviewDeployment,
findApplicationById, findApplicationById,
findPreviewDeploymentByApplicationId,
findPreviewDeploymentById, findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId, findPreviewDeploymentsByApplicationId,
findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment, removePreviewDeployment,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { eq } from "drizzle-orm";
import { and } from "drizzle-orm";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import type { DeploymentJob } from "@/server/queues/queue-types";
export const previewDeploymentRouter = createTRPCRouter({ export const previewDeploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
} }
return previewDeployment; return previewDeployment;
}), }),
create: protectedProcedure
.input(
z.object({
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
pullRequestId: z.string(),
repository: z.string(),
owner: z.string(),
branch: z.string(),
deploymentHash: z.string(),
prBranch: z.string(),
prNumber: z.any(),
prTitle: z.string(),
prURL: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const organizationId = ctx.session.activeOrganizationId;
const action = input.action;
const prId = input.pullRequestId;
if (action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
(previewDeployment) =>
previewDeployment.application.project.organizationId ===
organizationId,
);
if (filteredPreviewDeploymentResult.length > 0) {
for (const previewDeployment of filteredPreviewDeploymentResult) {
try {
await removePreviewDeployment(
previewDeployment.previewDeploymentId,
);
} catch (error) {
console.log(error);
}
}
}
return {
message: "Preview Deployments Closed",
};
}
if (
action === "opened" ||
action === "synchronize" ||
action === "reopened"
) {
const deploymentHash = input.deploymentHash;
const prBranch = input.prBranch;
const prNumber = input.prNumber;
const prTitle = input.prTitle;
const prURL = input.prURL;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.repository, input.repository),
eq(applications.branch, input.branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, input.owner),
),
with: {
previewDeployments: true,
project: true,
},
});
const filteredApps = apps.filter(
(app) => app.project.organizationId === organizationId,
);
console.log(filteredApps);
for (const app of filteredApps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
try {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
console.log(previewDeployment);
previewDeploymentId = previewDeployment.previewDeploymentId;
} catch (error) {
console.log(error);
}
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
isExternal: true,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
}
return {
message: "Preview Deployments Created",
};
}),
}); });

View File

@@ -361,7 +361,6 @@ export const projectRouter = createTRPCRouter({
previewDeployments, previewDeployments,
mounts, mounts,
appName, appName,
refreshToken,
...application ...application
} = await findApplicationById(id); } = await findApplicationById(id);
const newAppName = appName.substring( const newAppName = appName.substring(
@@ -604,14 +603,8 @@ export const projectRouter = createTRPCRouter({
break; break;
} }
case "compose": { case "compose": {
const { const { composeId, mounts, domains, appName, ...compose } =
composeId, await findComposeById(id);
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring( const newAppName = appName.substring(
0, 0,

View File

@@ -641,16 +641,10 @@ export const settingsRouter = createTRPCRouter({
}, },
}) })
.input( .input(
z z.object({
.object({ start: z.string().optional(),
dateRange: z end: z.string().optional(),
.object({ }),
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {

View File

@@ -4,10 +4,10 @@ import {
getNodeInfo, getNodeInfo,
getSwarmNodes, getSwarmNodes,
} from "@dokploy/server"; } from "@dokploy/server";
import { findServerById } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { findServerById } from "@dokploy/server";
import { containerIdRegex } from "./docker"; import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({ export const swarmRouter = createTRPCRouter({

View File

@@ -1,30 +1,30 @@
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { import {
IS_CLOUD, IS_CLOUD,
createVolumeBackup,
findVolumeBackupById,
removeVolumeBackup,
removeVolumeBackupJob,
restoreVolume,
runVolumeBackup,
scheduleVolumeBackup,
updateVolumeBackup, updateVolumeBackup,
removeVolumeBackup,
createVolumeBackup,
runVolumeBackup,
findVolumeBackupById,
restoreVolume,
scheduleVolumeBackup,
removeVolumeBackupJob,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { import {
createVolumeBackupSchema, createVolumeBackupSchema,
updateVolumeBackupSchema, updateVolumeBackupSchema,
volumeBackups, volumeBackups,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@dokploy/server/db";
import { eq } from "drizzle-orm";
import { observable } from "@trpc/server/observable";
import { import {
execAsyncRemote, execAsyncRemote,
execAsyncStream, execAsyncStream,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({ export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure list: protectedProcedure

View File

@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
}); });
} }
} else { } else {
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
titleLog: job.data.titleLog, titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog, descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId, previewDeploymentId: job.data.previewDeploymentId,
isExternal: job.data.isExternal,
}); });
} }
} }

View File

@@ -26,6 +26,7 @@ type DeployJob =
applicationType: "application-preview"; applicationType: "application-preview";
previewDeploymentId: string; previewDeploymentId: string;
serverId?: string; serverId?: string;
isExternal?: boolean;
}; };
export type DeploymentJob = DeployJob; export type DeploymentJob = DeployJob;

View File

@@ -5,7 +5,6 @@ import {
initializeTraefik, initializeTraefik,
} from "@dokploy/server/setup/traefik-setup"; } from "@dokploy/server/setup/traefik-setup";
import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/setup/redis-setup"; import { initializeRedis } from "@dokploy/server/setup/redis-setup";
@@ -13,6 +12,7 @@ import {
initializeNetwork, initializeNetwork,
initializeSwarm, initializeSwarm,
} from "@dokploy/server/setup/setup"; } from "@dokploy/server/setup/setup";
import { execAsync } from "@dokploy/server";
(async () => { (async () => {
try { try {
setupDirectories(); setupDirectories();

View File

@@ -13,7 +13,10 @@ declare global {
baseDomain?: string; baseDomain?: string;
}; };
chatwootSDK?: { chatwootSDK?: {
run: (config: { websiteToken: string; baseUrl: string }) => void; run: (config: {
websiteToken: string;
baseUrl: string;
}) => void;
}; };
$chatwoot?: { $chatwoot?: {
setUser: ( setUser: (

View File

@@ -29,9 +29,5 @@
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0"
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
} }

View File

@@ -1,17 +1,18 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": { "files": {
"includes": [ "ignore": [
"**", "node_modules/**",
"!**/.docker", ".next/**",
"!**/.next/**", "drizzle/**",
"!**/dist", ".docker",
"!**/drizzle/**", "dist",
"!node_modules/**", "packages/server/package.json"
"!packages/server/package.json"
] ]
}, },
"assist": { "actions": { "source": { "organizeImports": "on" } } }, "organizeImports": {
"enabled": true
},
"linter": { "linter": {
"rules": { "rules": {
"security": { "security": {
@@ -29,17 +30,7 @@
"noUnusedVariables": "error" "noUnusedVariables": "error"
}, },
"style": { "style": {
"noNonNullAssertion": "off", "noNonNullAssertion": "off"
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}, },
"suspicious": { "suspicious": {
"noArrayIndexKey": "off", "noArrayIndexKey": "off",

View File

@@ -1,10 +1,7 @@
{ {
"name": "dokploy", "name": "dokploy",
"private": true, "private": true,
"workspaces": [ "workspaces": ["apps/*", "packages/*"],
"apps/*",
"packages/*"
],
"scripts": { "scripts": {
"dokploy:setup": "pnpm --filter=dokploy run setup", "dokploy:setup": "pnpm --filter=dokploy run setup",
"dokploy:dev": "pnpm --filter=dokploy run dev", "dokploy:dev": "pnpm --filter=dokploy run dev",
@@ -23,7 +20,7 @@
"format-and-lint:fix": "biome check . --write" "format-and-lint:fix": "biome check . --write"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.1.1", "@biomejs/biome": "1.9.4",
"@commitlint/cli": "^19.8.1", "@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1", "@commitlint/config-conventional": "^19.8.1",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",
@@ -33,10 +30,10 @@
"lint-staged": "^15.5.2", "lint-staged": "^15.5.2",
"tsx": "4.16.2" "tsx": "4.16.2"
}, },
"packageManager": "pnpm@9.12.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {
"*": [ "*": [
@@ -44,9 +41,7 @@
] ]
}, },
"commitlint": { "commitlint": {
"extends": [ "extends": ["@commitlint/config-conventional"]
"@commitlint/config-conventional"
]
}, },
"resolutions": { "resolutions": {
"@types/react": "18.3.5", "@types/react": "18.3.5",

View File

@@ -105,10 +105,5 @@
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"packageManager": "pnpm@9.12.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
} }
} }

View File

@@ -131,10 +131,6 @@ export const applications = pgTable("application", {
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
false, false,
), ),
// Security: Require collaborator permissions for preview deployments
previewRequireCollaboratorPermissions: boolean(
"previewRequireCollaboratorPermissions",
).default(true),
rollbackActive: boolean("rollbackActive").default(false), rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"), buildArgs: text("buildArgs"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
@@ -432,7 +428,6 @@ const createSchema = createInsertSchema(applications, {
previewHttps: z.boolean().optional(), previewHttps: z.boolean().optional(),
previewPath: z.string().optional(), previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(), cleanCache: z.boolean().optional(),
}); });

View File

@@ -13,9 +13,9 @@ import { applications } from "./application";
import { backups } from "./backups"; import { backups } from "./backups";
import { compose } from "./compose"; import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { rollbacks } from "./rollbacks";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { server } from "./server"; import { server } from "./server";
import { rollbacks } from "./rollbacks";
import { volumeBackups } from "./volume-backups"; import { volumeBackups } from "./volume-backups";
export const deploymentStatus = pgEnum("deploymentStatus", [ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",

View File

@@ -1,14 +1,14 @@
import type { Application } from "@dokploy/server/services/application";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project";
import type { Registry } from "@dokploy/server/services/registry";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core"; import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import type { Application } from "@dokploy/server/services/application";
import type { Project } from "@dokploy/server/services/project";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Registry } from "@dokploy/server/services/registry";
export const rollbacks = pgTable("rollback", { export const rollbacks = pgTable("rollback", {
rollbackId: text("rollbackId") rollbackId: text("rollbackId")

View File

@@ -1,4 +1,3 @@
import { paths } from "@dokploy/server/constants";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
@@ -16,6 +15,7 @@ import { backups } from "./backups";
import { projects } from "./project"; import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
import { paths } from "@dokploy/server/constants";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -323,7 +323,6 @@ export const apiUpdateWebServerMonitoring = z.object({
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(), password: z.string().optional(),
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(),
metricsConfig: z metricsConfig: z
.object({ .object({
server: z.object({ server: z.object({

View File

@@ -3,16 +3,16 @@ import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { applications } from "./application";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { serviceType } from "./mount"; import { serviceType } from "./mount";
import { applications } from "./application";
import { mongo } from "./mongo";
import { mysql } from "./mysql"; import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis"; import { redis } from "./redis";
import { compose } from "./compose";
import { postgres } from "./postgres";
import { mariadb } from "./mariadb";
import { destinations } from "./destination";
import { deployments } from "./deployment";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const volumeBackups = pgTable("volume_backup", { export const volumeBackups = pgTable("volume_backup", {

View File

@@ -45,7 +45,6 @@ export * from "./setup/traefik-setup";
export * from "./setup/server-validate"; export * from "./setup/server-validate";
export * from "./setup/server-audit"; export * from "./setup/server-audit";
export * from "./utils/watch-paths/should-deploy"; export * from "./utils/watch-paths/should-deploy";
export * from "./utils/providers/github";
export * from "./utils/backups/index"; export * from "./utils/backups/index";
export * from "./utils/backups/mariadb"; export * from "./utils/backups/mariadb";
export * from "./utils/backups/mongo"; export * from "./utils/backups/mongo";

View File

@@ -298,7 +298,11 @@ export const validateRequest = async (request: IncomingMessage) => {
const mockSession = { const mockSession = {
session: { session: {
userId: apiKeyRecord.user.id, user: {
id: apiKeyRecord.user.id,
email: apiKeyRecord.user.email,
name: apiKeyRecord.user.name,
},
activeOrganizationId: organizationId || "", activeOrganizationId: organizationId || "",
}, },
user: { user: {

View File

@@ -404,11 +404,13 @@ export const deployPreviewApplication = async ({
titleLog = "Preview Deployment", titleLog = "Preview Deployment",
descriptionLog = "", descriptionLog = "",
previewDeploymentId, previewDeploymentId,
isExternal = false,
}: { }: {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string; descriptionLog: string;
previewDeploymentId: string; previewDeploymentId: string;
isExternal?: boolean;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
@@ -434,36 +436,39 @@ export const deployPreviewApplication = async ({
githubId: application?.githubId || "", githubId: application?.githubId || "",
}; };
try { try {
const commentExists = await issueCommentExists({ if (!isExternal) {
...issueParams, const commentExists = await issueCommentExists({
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams, ...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
if (!commentExists) {
if (!result) { const result = await createPreviewDeploymentComment({
throw new TRPCError({ ...issueParams,
code: "NOT_FOUND", previewDomain,
message: "Pull request comment not found", appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
} }
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName; application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
@@ -477,25 +482,31 @@ export const deployPreviewApplication = async ({
}); });
await buildApplication(application, deployment.logPath); await buildApplication(application, deployment.logPath);
} }
const successComment = getIssueComment(
application.name, if (!isExternal) {
"success", const successComment = getIssueComment(
previewDomain, application.name,
); "success",
await updateIssueComment({ previewDomain,
...issueParams, );
body: `### Dokploy Preview Deployment\n\n${successComment}`, await updateIssueComment({
}); ...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done", previewStatus: "done",
}); });
} catch (error) { } catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain); if (!isExternal) {
await updateIssueComment({ const comment = getIssueComment(application.name, "error", previewDomain);
...issueParams, await updateIssueComment({
body: `### Dokploy Preview Deployment\n\n${comment}`, ...issueParams,
}); body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error", previewStatus: "error",
@@ -511,11 +522,13 @@ export const deployRemotePreviewApplication = async ({
titleLog = "Preview Deployment", titleLog = "Preview Deployment",
descriptionLog = "", descriptionLog = "",
previewDeploymentId, previewDeploymentId,
isExternal = false,
}: { }: {
applicationId: string; applicationId: string;
titleLog: string; titleLog: string;
descriptionLog: string; descriptionLog: string;
previewDeploymentId: string; previewDeploymentId: string;
isExternal?: boolean;
}) => { }) => {
const application = await findApplicationById(applicationId); const application = await findApplicationById(applicationId);
@@ -541,36 +554,39 @@ export const deployRemotePreviewApplication = async ({
githubId: application?.githubId || "", githubId: application?.githubId || "",
}; };
try { try {
const commentExists = await issueCommentExists({ if (!isExternal) {
...issueParams, const commentExists = await issueCommentExists({
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams, ...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
if (!commentExists) {
if (!result) { const result = await createPreviewDeploymentComment({
throw new TRPCError({ ...issueParams,
code: "NOT_FOUND", previewDomain,
message: "Pull request comment not found", appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
}); });
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
} }
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
application.appName = previewDeployment.appName; application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
@@ -592,25 +608,29 @@ export const deployRemotePreviewApplication = async ({
await mechanizeDockerContainer(application); await mechanizeDockerContainer(application);
} }
const successComment = getIssueComment( if (!isExternal) {
application.name, const successComment = getIssueComment(
"success", application.name,
previewDomain, "success",
); previewDomain,
await updateIssueComment({ );
...issueParams, await updateIssueComment({
body: `### Dokploy Preview Deployment\n\n${successComment}`, ...issueParams,
}); body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done", previewStatus: "done",
}); });
} catch (error) { } catch (error) {
const comment = getIssueComment(application.name, "error", previewDomain); if (!isExternal) {
await updateIssueComment({ const comment = getIssueComment(application.name, "error", previewDomain);
...issueParams, await updateIssueComment({
body: `### Dokploy Preview Deployment\n\n${comment}`, ...issueParams,
}); body: `### Dokploy Preview Deployment\n\n${comment}`,
});
}
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, { await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error", previewStatus: "error",

View File

@@ -31,8 +31,8 @@ import {
findPreviewDeploymentById, findPreviewDeploymentById,
updatePreviewDeployment, updatePreviewDeployment,
} from "./preview-deployment"; } from "./preview-deployment";
import { removeRollbackById } from "./rollbacks";
import { findScheduleById } from "./schedule"; import { findScheduleById } from "./schedule";
import { removeRollbackById } from "./rollbacks";
import { findVolumeBackupById } from "./volume-backups"; import { findVolumeBackupById } from "./volume-backups";
export type Deployment = typeof deployments.$inferSelect; export type Deployment = typeof deployments.$inferSelect;

View File

@@ -192,156 +192,3 @@ export const createPreviewDeploymentComment = async ({
pullRequestCommentId: `${issue.data.id}`, pullRequestCommentId: `${issue.data.id}`,
}).then((response) => response[0]); }).then((response) => response[0]);
}; };
/**
* Generate security notification message for blocked PR deployments
*/
export const getSecurityBlockedMessage = (
prAuthor: string,
repositoryName: string,
permission: string | null,
) => {
return `### 🚨 Preview Deployment Blocked - Security Protection
**Your pull request was blocked from triggering preview deployments**
#### Why was this blocked?
- **User**: \`${prAuthor}\`
- **Repository**: \`${repositoryName}\`
- **Permission Level**: \`${permission || "none"}\`
- **Required Level**: \`write\`, \`maintain\`, or \`admin\`
#### How to resolve this:
**Option 1: Get Collaborator Access (Recommended)**
Ask a repository maintainer to invite you as a collaborator with **write permissions** or higher.
**Option 2: Request Permission Override**
Ask a repository administrator to disable security validation for this specific application if appropriate.
#### For Repository Administrators:
To disable this security check (⚠️ **not recommended for public repositories**):
Enter to preview settings and disable the security check.
---
*This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.*
<details>
<summary>🛡️ Learn more about this security feature</summary>
This protection prevents unauthorized users from:
- Executing malicious code on the deployment server
- Accessing environment variables and secrets
- Potentially compromising the infrastructure
Preview deployments are powerful but require trust. Only users with repository write access can trigger them.
</details>`;
};
/**
* Check if a security notification comment already exists on a GitHub PR
* This prevents creating duplicate security comments on subsequent pushes
*/
export const hasExistingSecurityComment = async ({
owner,
repository,
prNumber,
githubId,
}: {
owner: string;
repository: string;
prNumber: number;
githubId: string;
}): Promise<boolean> => {
try {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
// Get all comments for this PR
const { data: comments } = await octokit.rest.issues.listComments({
owner,
repo: repository,
issue_number: prNumber,
});
// Check if any comment contains our security notification marker
const securityCommentExists = comments.some((comment) =>
comment.body?.includes(
"🚨 Preview Deployment Blocked - Security Protection",
),
);
return securityCommentExists;
} catch (error) {
console.error(
`❌ Failed to check existing comments on PR #${prNumber}:`,
error,
);
// If we can't check, assume no comment exists to avoid blocking functionality
return false;
}
};
/**
* Create a security notification comment on a GitHub PR
*/
export const createSecurityBlockedComment = async ({
owner,
repository,
prNumber,
prAuthor,
permission,
githubId,
}: {
owner: string;
repository: string;
prNumber: number;
prAuthor: string;
permission: string | null;
githubId: string;
}) => {
try {
// Check if a security comment already exists to prevent duplicates
const commentExists = await hasExistingSecurityComment({
owner,
repository,
prNumber,
githubId,
});
if (commentExists) {
console.log(
` Security notification comment already exists on PR #${prNumber}, skipping duplicate`,
);
return null;
}
const github = await findGithubById(githubId);
const octokit = authGithub(github);
const securityMessage = getSecurityBlockedMessage(
prAuthor,
repository,
permission,
);
const issue = await octokit.rest.issues.createComment({
owner,
repo: repository,
issue_number: prNumber,
body: securityMessage,
});
console.log(
`✅ Security notification comment created on PR #${prNumber}: ${issue.data.html_url}`,
);
return issue.data;
} catch (error) {
console.error(
`❌ Failed to create security comment on PR #${prNumber}:`,
error,
);
// Don't throw error - security comment is nice-to-have, not critical
return null;
}
};

View File

@@ -152,6 +152,7 @@ export const findPreviewDeploymentsByApplicationId = async (
export const createPreviewDeployment = async ( export const createPreviewDeployment = async (
schema: typeof apiCreatePreviewDeployment._type, schema: typeof apiCreatePreviewDeployment._type,
isExternal = false,
) => { ) => {
const application = await findApplicationById(schema.applicationId); const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`; const appName = `preview-${application.appName}-${generatePassword(6)}`;
@@ -166,27 +167,32 @@ export const createPreviewDeployment = async (
org?.ownerId || "", org?.ownerId || "",
); );
const octokit = authGithub(application?.github as Github); let issueId = "";
if (!isExternal) {
const octokit = authGithub(application?.github as Github);
const runningComment = getIssueComment( const runningComment = getIssueComment(
application.name, application.name,
"initializing", "initializing",
`${application.previewHttps ? "https" : "http"}://${generateDomain}`, `${application.previewHttps ? "https" : "http"}://${generateDomain}`,
); );
const issue = await octokit.rest.issues.createComment({ const issue = await octokit.rest.issues.createComment({
owner: application?.owner || "", owner: application?.owner || "",
repo: application?.repository || "", repo: application?.repository || "",
issue_number: Number.parseInt(schema.pullRequestNumber), issue_number: Number.parseInt(schema.pullRequestNumber),
body: `### Dokploy Preview Deployment\n\n${runningComment}`, body: `### Dokploy Preview Deployment\n\n${runningComment}`,
}); });
issueId = `${issue.data.id}`;
}
const previewDeployment = await db const previewDeployment = await db
.insert(previewDeployments) .insert(previewDeployments)
.values({ .values({
...schema, ...schema,
appName: appName, appName: appName,
pullRequestCommentId: `${issue.data.id}`, pullRequestCommentId: issueId,
}) })
.returning() .returning()
.then((value) => value[0]); .then((value) => value[0]);
@@ -233,6 +239,13 @@ export const findPreviewDeploymentsByPullRequestId = async (
) => { ) => {
const previewDeploymentResult = await db.query.previewDeployments.findMany({ const previewDeploymentResult = await db.query.previewDeployments.findMany({
where: eq(previewDeployments.pullRequestId, pullRequestId), where: eq(previewDeployments.pullRequestId, pullRequestId),
with: {
application: {
with: {
project: true,
},
},
},
}); });
return previewDeploymentResult; return previewDeploymentResult;

View File

@@ -1,27 +1,27 @@
import type { CreateServiceOptions } from "dockerode";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { z } from "zod";
import { db } from "../db"; import { db } from "../db";
import { import {
type createRollbackSchema, type createRollbackSchema,
deployments as deploymentsSchema,
rollbacks, rollbacks,
deployments as deploymentsSchema,
} from "../db/schema"; } from "../db/schema";
import { type ApplicationNested, getAuthConfig } from "../utils/builders"; import type { z } from "zod";
import { type Application, findApplicationById } from "./application";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { getAuthConfig, type ApplicationNested } from "../utils/builders";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import type { CreateServiceOptions } from "dockerode";
import { findDeploymentById } from "./deployment";
import { import {
prepareEnvironmentVariables,
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer, generateConfigContainer,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables,
} from "../utils/docker/utils"; } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import type { Project } from "./project";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment";
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";
export const createRollback = async ( export const createRollback = async (
input: z.infer<typeof createRollbackSchema>, input: z.infer<typeof createRollbackSchema>,

View File

@@ -1,12 +1,12 @@
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { z } from "zod";
import { db } from "../db";
import { import {
type createVolumeBackupSchema, type createVolumeBackupSchema,
type updateVolumeBackupSchema, type updateVolumeBackupSchema,
volumeBackups, volumeBackups,
} from "../db/schema"; } from "../db/schema";
import { db } from "../db";
import { TRPCError } from "@trpc/server";
import type { z } from "zod";
export const findVolumeBackupById = async (volumeBackupId: string) => { export const findVolumeBackupById = async (volumeBackupId: string) => {
const volumeBackup = await db.query.volumeBackups.findFirst({ const volumeBackup = await db.query.volumeBackups.findFirst({

View File

@@ -191,9 +191,6 @@ export const createDefaultServerTraefikConfig = () => {
export const getDefaultTraefikConfig = () => { export const getDefaultTraefikConfig = () => {
const configObject: MainTraefikConfig = { const configObject: MainTraefikConfig = {
global: {
sendAnonymousUsage: false,
},
providers: { providers: {
...(process.env.NODE_ENV === "development" ...(process.env.NODE_ENV === "development"
? { ? {

View File

@@ -1,8 +1,8 @@
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network"; import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume"; import { addSuffixToAllVolumes } from "./compose/volume";
import { generateRandomHash } from "./compose";
import type { ComposeSpecification } from "./types"; import type { ComposeSpecification } from "./types";
export const addAppNameToPreventCollision = ( export const addAppNameToPreventCollision = (

View File

@@ -45,49 +45,6 @@ export const getGithubToken = async (
return installation.token; return installation.token;
}; };
/**
* Check if a GitHub user has write/admin permissions on a repository
* This is used to validate PR authors before allowing preview deployments
*/
export const checkUserRepositoryPermissions = async (
githubProvider: Github,
owner: string,
repo: string,
username: string,
): Promise<{ hasWriteAccess: boolean; permission: string | null }> => {
try {
const octokit = authGithub(githubProvider);
// Check if user is a collaborator with write permissions
const { data: permission } =
await octokit.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
// Allow only users with 'write', 'admin', or 'maintain' permissions
// Currently exists Read, Triage, Write, Maintain, Admin
const allowedPermissions = ["write", "admin", "maintain"];
const hasWriteAccess = allowedPermissions.includes(permission.permission);
return {
hasWriteAccess,
permission: permission.permission,
};
} catch (error) {
// If user is not a collaborator, GitHub API returns 404
console.warn(
`User ${username} is not a collaborator of ${owner}/${repo}:`,
error,
);
return {
hasWriteAccess: false,
permission: null,
};
}
};
export const haveGithubRequirements = (githubProvider: Github) => { export const haveGithubRequirements = (githubProvider: Github) => {
return !!( return !!(
githubProvider?.githubAppId && githubProvider?.githubAppId &&

View File

@@ -1,6 +1,6 @@
import fs, { writeFileSync } from "node:fs"; import fs, { writeFileSync } from "node:fs";
import { createReadStream } from "node:fs";
import path from "node:path"; import path from "node:path";
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline"; import { createInterface } from "node:readline";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain"; import type { Domain } from "@dokploy/server/services/domain";
@@ -237,6 +237,7 @@ export const writeTraefikConfigInPath = async (
} else { } else {
fs.writeFileSync(configPath, traefikConfig, "utf8"); fs.writeFileSync(configPath, traefikConfig, "utf8");
} }
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) { } catch (e) {
console.error("Error saving the YAML config file:", e); console.error("Error saving the YAML config file:", e);
} }

View File

@@ -1,12 +1,12 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import type { ApplicationNested } from "../builders"; import type { ApplicationNested } from "../builders";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { writeTraefikConfigRemote } from "./application"; import { writeTraefikConfigRemote } from "./application";
import type { FileConfig } from "./file-types"; import type { FileConfig } from "./file-types";
import type { Domain } from "@dokploy/server/services/domain";
export const addMiddleware = (config: FileConfig, middlewareName: string) => { export const addMiddleware = (config: FileConfig, middlewareName: string) => {
if (config.http?.routers) { if (config.http?.routers) {

View File

@@ -1,9 +1,9 @@
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils"; import { normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils"; import { getS3Credentials } from "../backups/utils";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose";
export const backupVolume = async ( export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>, volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import { import {
findApplicationById, findApplicationById,
findComposeById, findComposeById,
@@ -6,6 +5,7 @@ import {
getS3Credentials, getS3Credentials,
paths, paths,
} from "../.."; } from "../..";
import path from "node:path";
export const restoreVolume = async ( export const restoreVolume = async (
id: string, id: string,

View File

@@ -1,5 +1,4 @@
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { import {
createDeploymentVolumeBackup, createDeploymentVolumeBackup,
execAsync, execAsync,
@@ -7,6 +6,7 @@ import {
updateDeploymentStatus, updateDeploymentStatus,
} from "../.."; } from "../..";
import { backupVolume } from "./backup"; import { backupVolume } from "./backup";
import { scheduleJob, scheduledJobs } from "node-schedule";
export const scheduleVolumeBackup = async (volumeBackupId: string) => { export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId); const volumeBackup = await findVolumeBackupById(volumeBackupId);

74
pnpm-lock.yaml generated
View File

@@ -13,8 +13,8 @@ importers:
.: .:
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: 2.1.1 specifier: 1.9.4
version: 2.1.1 version: 1.9.4
'@commitlint/cli': '@commitlint/cli':
specifier: ^19.8.1 specifier: ^19.8.1
version: 19.8.1(@types/node@18.19.104)(typescript@5.8.3) version: 19.8.1(@types/node@18.19.104)(typescript@5.8.3)
@@ -933,55 +933,55 @@ packages:
'@better-fetch/fetch@1.1.18': '@better-fetch/fetch@1.1.18':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
'@biomejs/biome@2.1.1': '@biomejs/biome@1.9.4':
resolution: {integrity: sha512-HFGYkxG714KzG+8tvtXCJ1t1qXQMzgWzfvQaUjxN6UeKv+KvMEuliInnbZLJm6DXFXwqVi6446EGI0sGBLIYng==} resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
'@biomejs/cli-darwin-arm64@2.1.1': '@biomejs/cli-darwin-arm64@1.9.4':
resolution: {integrity: sha512-2Muinu5ok4tWxq4nu5l19el48cwCY/vzvI7Vjbkf3CYIQkjxZLyj0Ad37Jv2OtlXYaLvv+Sfu1hFeXt/JwRRXQ==} resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@2.1.1': '@biomejs/cli-darwin-x64@1.9.4':
resolution: {integrity: sha512-cC8HM5lrgKQXLAK+6Iz2FrYW5A62pAAX6KAnRlEyLb+Q3+Kr6ur/sSuoIacqlp1yvmjHJqjYfZjPvHWnqxoEIA==} resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.1.1': '@biomejs/cli-linux-arm64-musl@1.9.4':
resolution: {integrity: sha512-/7FBLnTswu4jgV9ttI3AMIdDGqVEPIZd8I5u2D4tfCoj8rl9dnjrEQbAIDlWhUXdyWlFSz8JypH3swU9h9P+2A==} resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-arm64@2.1.1': '@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-tw4BEbhAUkWPe4WBr6IX04DJo+2jz5qpPzpW/SWvqMjb9QuHY8+J0M23V8EPY/zWU4IG8Ui0XESapR1CB49Q7g==} resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64-musl@2.1.1': '@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-kUu+loNI3OCD2c12cUt7M5yaaSjDnGIksZwKnueubX6c/HWUyi/0mPbTBHR49Me3F0KKjWiKM+ZOjsmC+lUt9g==} resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64@2.1.1': '@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-3WJ1GKjU7NzZb6RTbwLB59v9cTIlzjbiFLDB0z4376TkDqoNYilJaC37IomCr/aXwuU8QKkrYoHrgpSq5ffJ4Q==} resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-win32-arm64@2.1.1': '@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-vEHK0v0oW+E6RUWLoxb2isI3rZo57OX9ZNyyGH701fZPj6Il0Rn1f5DMNyCmyflMwTnIQstEbs7n2BxYSqQx4Q==} resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@biomejs/cli-win32-x64@2.1.1': '@biomejs/cli-win32-x64@1.9.4':
resolution: {integrity: sha512-i2PKdn70kY++KEF/zkQFvQfX1e8SkA8hq4BgC+yE9dZqyLzB/XStY2MvwI3qswlRgnGpgncgqe0QYKVS1blksg==} resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -7281,39 +7281,39 @@ snapshots:
'@better-fetch/fetch@1.1.18': {} '@better-fetch/fetch@1.1.18': {}
'@biomejs/biome@2.1.1': '@biomejs/biome@1.9.4':
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.1.1 '@biomejs/cli-darwin-arm64': 1.9.4
'@biomejs/cli-darwin-x64': 2.1.1 '@biomejs/cli-darwin-x64': 1.9.4
'@biomejs/cli-linux-arm64': 2.1.1 '@biomejs/cli-linux-arm64': 1.9.4
'@biomejs/cli-linux-arm64-musl': 2.1.1 '@biomejs/cli-linux-arm64-musl': 1.9.4
'@biomejs/cli-linux-x64': 2.1.1 '@biomejs/cli-linux-x64': 1.9.4
'@biomejs/cli-linux-x64-musl': 2.1.1 '@biomejs/cli-linux-x64-musl': 1.9.4
'@biomejs/cli-win32-arm64': 2.1.1 '@biomejs/cli-win32-arm64': 1.9.4
'@biomejs/cli-win32-x64': 2.1.1 '@biomejs/cli-win32-x64': 1.9.4
'@biomejs/cli-darwin-arm64@2.1.1': '@biomejs/cli-darwin-arm64@1.9.4':
optional: true optional: true
'@biomejs/cli-darwin-x64@2.1.1': '@biomejs/cli-darwin-x64@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-arm64-musl@2.1.1': '@biomejs/cli-linux-arm64-musl@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-arm64@2.1.1': '@biomejs/cli-linux-arm64@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-x64-musl@2.1.1': '@biomejs/cli-linux-x64-musl@1.9.4':
optional: true optional: true
'@biomejs/cli-linux-x64@2.1.1': '@biomejs/cli-linux-x64@1.9.4':
optional: true optional: true
'@biomejs/cli-win32-arm64@2.1.1': '@biomejs/cli-win32-arm64@1.9.4':
optional: true optional: true
'@biomejs/cli-win32-x64@2.1.1': '@biomejs/cli-win32-x64@1.9.4':
optional: true optional: true
'@codemirror/autocomplete@6.18.6': '@codemirror/autocomplete@6.18.6':

View File

@@ -1,5 +1,6 @@
packages: packages:
- "apps/api" - "apps/api"
- "apps/dokploy" - "apps/dokploy"
- "apps/monitoring"
- "apps/schedules" - "apps/schedules"
- "packages/server" - "packages/server"