From c1aeb828d8d28a3020e1d9bfc95ab5e5dbbd3bd0 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 8 Mar 2025 23:32:08 -0600 Subject: [PATCH] feat(applications): add watch paths for selective deployments - Implement watch paths feature for GitHub and GitLab applications and compose services - Add ability to specify paths that trigger deployments when changed - Update database schemas to support watch paths - Integrate micromatch for flexible path matching - Enhance deployment triggers with granular file change detection --- .../generic/save-bitbucket-provider.tsx | 91 +- .../general/generic/save-git-provider.tsx | 127 +- .../general/generic/save-github-provider.tsx | 103 +- .../general/generic/save-gitlab-provider.tsx | 92 +- .../save-bitbucket-provider-compose.tsx | 91 +- .../generic/save-git-provider-compose.tsx | 93 +- .../generic/save-github-provider-compose.tsx | 92 +- .../generic/save-gitlab-provider-compose.tsx | 92 +- .../drizzle/0075_young_typhoid_mary.sql | 1 + .../drizzle/0076_young_sharon_ventura.sql | 1 + apps/dokploy/drizzle/meta/0075_snapshot.json | 5144 ++++++++++++++++ apps/dokploy/drizzle/meta/0076_snapshot.json | 5150 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 14 + apps/dokploy/package.json | 2 + .../pages/api/deploy/[refreshToken].ts | 89 +- .../api/deploy/compose/[refreshToken].ts | 59 +- apps/dokploy/pages/api/deploy/github.ts | 22 + .../dokploy/server/api/routers/application.ts | 4 + packages/server/package.json | 2 + packages/server/src/db/schema/application.ts | 6 + packages/server/src/db/schema/compose.ts | 3 +- packages/server/src/index.ts | 2 +- .../src/utils/watch-paths/should-deploy.ts | 9 + pnpm-lock.yaml | 37 +- 24 files changed, 11293 insertions(+), 33 deletions(-) create mode 100644 apps/dokploy/drizzle/0075_young_typhoid_mary.sql create mode 100644 apps/dokploy/drizzle/0076_young_sharon_ventura.sql create mode 100644 apps/dokploy/drizzle/meta/0075_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0076_snapshot.json create mode 100644 packages/server/src/utils/watch-paths/should-deploy.ts diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index 9af040b7..35aba58a 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -29,14 +29,21 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { Badge } from "@/components/ui/badge"; const BitbucketProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), @@ -48,6 +55,7 @@ const BitbucketProviderSchema = z.object({ .required(), branch: z.string().min(1, "Branch is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), + watchPaths: z.array(z.string()).optional(), }); type BitbucketProvider = z.infer; @@ -73,6 +81,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { }, bitbucketId: "", branch: "", + watchPaths: [], }, resolver: zodResolver(BitbucketProviderSchema), }); @@ -118,6 +127,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { }, buildPath: data.bitbucketBuildPath || "/", bitbucketId: data.bitbucketId || "", + watchPaths: data.watchPaths || [], }); } }, [form.reset, data, form]); @@ -130,6 +140,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { bitbucketBuildPath: data.buildPath, bitbucketId: data.bitbucketId, applicationId, + watchPaths: data.watchPaths || [], }) .then(async () => { toast.success("Service Provided Saved"); @@ -363,6 +374,84 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => { )} /> + ( + +
+ Watch Paths + + + +
+ ? +
+
+ +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + form.setValue("watchPaths", newPaths); + }} + /> + + ))} +
+ +
+ { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value) { + const newPaths = [...(field.value || []), value]; + form.setValue("watchPaths", newPaths); + input.value = ""; + } + } + }} + /> + +
+
+ +
+ )} + />
)}
- ( - - Branch - - - - - - )} - /> +
+ ( + + Branch + + + + + + )} + /> +
+ { )} /> + ( + +
+ Watch Paths + + + +
+ ? +
+
+ +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. This + will work only when manual webhook is setup. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + form.setValue("watchPaths", newPaths); + }} + /> + + ))} +
+ +
+ { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value) { + const newPaths = [...(field.value || []), value]; + form.setValue("watchPaths", newPaths); + input.value = ""; + } + } + }} + /> + +
+
+ +
+ )} + />
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index adb44575..d8b6922e 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -28,14 +28,22 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import Link from "next/link"; const GithubProviderSchema = z.object({ buildPath: z.string().min(1, "Path is required").default("/"), @@ -47,6 +55,7 @@ const GithubProviderSchema = z.object({ .required(), branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), + watchPaths: z.array(z.string()).optional(), }); type GithubProvider = z.infer; @@ -113,6 +122,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { }, buildPath: data.buildPath || "/", githubId: data.githubId || "", + watchPaths: data.watchPaths || [], }); } }, [form.reset, data, form]); @@ -125,6 +135,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { owner: data.repository.owner, buildPath: data.buildPath, githubId: data.githubId, + watchPaths: data.watchPaths || [], }) .then(async () => { toast.success("Service Provided Saved"); @@ -350,7 +361,85 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { - + + + )} + /> + ( + +
+ Watch Paths + + + + + + +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + field.onChange(newPaths); + }} + /> + + ))} +
+
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const path = input.value.trim(); + if (path) { + field.onChange([...(field.value || []), path]); + input.value = ""; + } + } + }} + /> + + +
)} @@ -365,6 +454,16 @@ export const SaveGithubProvider = ({ applicationId }: Props) => { Save
+ {/* create github link */} +
+ + Repository + +
diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index 298a9114..2073f1a6 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -29,10 +29,17 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -50,6 +57,7 @@ const GitlabProviderSchema = z.object({ .required(), branch: z.string().min(1, "Branch is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"), + watchPaths: z.array(z.string()).optional(), }); type GitlabProvider = z.infer; @@ -124,6 +132,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { }, buildPath: data.gitlabBuildPath || "/", gitlabId: data.gitlabId || "", + watchPaths: data.watchPaths || [], }); } }, [form.reset, data, form]); @@ -138,6 +147,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { applicationId, gitlabProjectId: data.repository.id, gitlabPathNamespace: data.repository.gitlabPathNamespace, + watchPaths: data.watchPaths || [], }) .then(async () => { toast.success("Service Provided Saved"); @@ -375,7 +385,85 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => { - + + + )} + /> + ( + +
+ Watch Paths + + + + + + +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + field.onChange(newPaths); + }} + /> + + ))} +
+
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const path = input.value.trim(); + if (path) { + field.onChange([...(field.value || []), path]); + input.value = ""; + } + } + }} + /> + + +
)} diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index 87584133..079eeb1b 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -29,14 +29,21 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { Badge } from "@/components/ui/badge"; const BitbucketProviderSchema = z.object({ composePath: z.string().min(1), @@ -48,6 +55,7 @@ const BitbucketProviderSchema = z.object({ .required(), branch: z.string().min(1, "Branch is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), + watchPaths: z.array(z.string()).optional(), }); type BitbucketProvider = z.infer; @@ -73,6 +81,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { }, bitbucketId: "", branch: "", + watchPaths: [], }, resolver: zodResolver(BitbucketProviderSchema), }); @@ -118,6 +127,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { }, composePath: data.composePath, bitbucketId: data.bitbucketId || "", + watchPaths: data.watchPaths || [], }); } }, [form.reset, data, form]); @@ -132,6 +142,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { composeId, sourceType: "bitbucket", composeStatus: "idle", + watchPaths: data.watchPaths, }) .then(async () => { toast.success("Service Provided Saved"); @@ -365,6 +376,84 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => { )} /> + ( + +
+ Watch Paths + + + +
+ ? +
+
+ +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + form.setValue("watchPaths", newPaths); + }} + /> + + ))} +
+ +
+ { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value) { + const newPaths = [...(field.value || []), value]; + form.setValue("watchPaths", newPaths); + input.value = ""; + } + } + }} + /> + +
+
+ +
+ )} + />
+
+ + + + )} + />
diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx index 7787cb3c..6328386a 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Command, @@ -28,10 +29,16 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { CheckIcon, ChevronsUpDown } from "lucide-react"; +import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -47,6 +54,7 @@ const GithubProviderSchema = z.object({ .required(), branch: z.string().min(1, "Branch is required"), githubId: z.string().min(1, "Github Provider is required"), + watchPaths: z.array(z.string()).optional(), }); type GithubProvider = z.infer; @@ -71,6 +79,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { }, githubId: "", branch: "", + watchPaths: [], }, resolver: zodResolver(GithubProviderSchema), }); @@ -113,6 +122,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { }, composePath: data.composePath, githubId: data.githubId || "", + watchPaths: data.watchPaths || [], }); } }, [form.reset, data, form]); @@ -127,6 +137,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { githubId: data.githubId, sourceType: "github", composeStatus: "idle", + watchPaths: data.watchPaths, }) .then(async () => { toast.success("Service Provided Saved"); @@ -183,7 +194,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => { )} /> - { )} /> + ( + +
+ Watch Paths + + + +
+ ? +
+
+ +

+ Add paths to watch for changes. When files in these + paths change, a new deployment will be triggered. +

+
+
+
+
+
+ {field.value?.map((path, index) => ( + + {path} + { + const newPaths = [...(field.value || [])]; + newPaths.splice(index, 1); + form.setValue("watchPaths", newPaths); + }} + /> + + ))} +
+ +
+ { + if (e.key === "Enter") { + e.preventDefault(); + const input = e.currentTarget; + const value = input.value.trim(); + if (value) { + const newPaths = [...(field.value || []), value]; + form.setValue("watchPaths", newPaths); + input.value = ""; + } + } + }} + /> + +
+
+ +
+ )} + />
+
+ + + + )} + />