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
This commit is contained in:
Mauricio Siu
2025-03-08 23:32:08 -06:00
parent 1ad25ca6d1
commit c1aeb828d8
24 changed files with 11293 additions and 33 deletions

View File

@@ -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<typeof BitbucketProviderSchema>;
@@ -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) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
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 = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -17,23 +17,31 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon } from "lucide-react";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import { useRouter } from "next/router";
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 GitProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),
repositoryURL: z.string().min(1, {
message: "Repository URL is required",
}),
branch: z.string().min(1, "Branch required"),
buildPath: z.string().min(1, "Build Path required"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -56,6 +64,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
buildPath: "/",
repositoryURL: "",
sshKey: undefined,
watchPaths: [],
},
resolver: zodResolver(GitProviderSchema),
});
@@ -67,6 +76,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
branch: data.customGitBranch || "",
buildPath: data.customGitBuildPath || "/",
repositoryURL: data.customGitUrl || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
@@ -78,6 +88,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
customGitUrl: values.repositoryURL,
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
applicationId,
watchPaths: values.watchPaths || [],
})
.then(async () => {
toast.success("Git Provider Saved");
@@ -102,7 +113,9 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
name="repositoryURL"
render={({ field }) => (
<FormItem>
<FormLabel>Repository URL</FormLabel>
<FormLabel className="flex flex-row justify-between">
Repository URL
</FormLabel>
<FormControl>
<Input placeholder="git@bitbucket.org" {...field} />
</FormControl>
@@ -160,19 +173,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</Button>
)}
</div>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem>
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem>
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="buildPath"
@@ -186,6 +202,85 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
<p>
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.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
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 = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">

View File

@@ -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<typeof GithubProviderSchema>;
@@ -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) => {
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1"
>
{path}
<X
className="size-3 cursor-pointer hover:text-destructive"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
/>
</Badge>
))}
</div>
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
field.onChange([...(field.value || []), path]);
input.value = "";
}
}
}}
/>
</FormControl>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
@@ -365,6 +454,16 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
Save
</Button>
</div>
{/* create github link */}
<div className="flex w-full justify-end">
<Link
href={`https://github.com/${repository?.owner}/${repository?.repo}`}
target="_blank"
className="w-fit"
>
Repository
</Link>
</div>
</form>
</Form>
</div>

View File

@@ -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<typeof GitlabProviderSchema>;
@@ -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) => {
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1"
>
{path}
<X
className="size-3 cursor-pointer hover:text-destructive"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
field.onChange(newPaths);
}}
/>
</Badge>
))}
</div>
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const path = input.value.trim();
if (path) {
field.onChange([...(field.value || []), path]);
input.value = "";
}
}
}}
/>
</FormControl>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a path"]',
) as HTMLInputElement;
const path = input.value.trim();
if (path) {
field.onChange([...(field.value || []), path]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}

View File

@@ -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<typeof BitbucketProviderSchema>;
@@ -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) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
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 = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -17,9 +18,15 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon } from "lucide-react";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -33,6 +40,7 @@ const GitProviderSchema = z.object({
}),
branch: z.string().min(1, "Branch required"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
});
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -54,6 +62,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
repositoryURL: "",
composePath: "./docker-compose.yml",
sshKey: undefined,
watchPaths: [],
},
resolver: zodResolver(GitProviderSchema),
});
@@ -65,6 +74,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
branch: data.customGitBranch || "",
repositoryURL: data.customGitUrl || "",
composePath: data.composePath,
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
@@ -77,6 +87,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
composeId,
sourceType: "git",
composePath: values.composePath,
composeStatus: "idle",
watchPaths: values.watchPaths || [],
})
.then(async () => {
toast.success("Git Provider Saved");
@@ -191,6 +203,85 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent className="max-w-[300px]">
<p>
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.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
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 = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end">

View File

@@ -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<typeof GithubProviderSchema>;
@@ -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) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
@@ -357,6 +367,84 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
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 = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -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 GitlabProviderSchema = z.object({
composePath: z.string().min(1),
@@ -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<typeof GitlabProviderSchema>;
@@ -76,6 +84,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
},
gitlabId: "",
branch: "",
watchPaths: [],
},
resolver: zodResolver(GitlabProviderSchema),
});
@@ -124,6 +133,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
},
composePath: data.composePath,
gitlabId: data.gitlabId || "",
watchPaths: data.watchPaths || [],
});
}
}, [form.reset, data, form]);
@@ -140,6 +150,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
gitlabPathNamespace: data.repository.gitlabPathNamespace,
sourceType: "gitlab",
composeStatus: "idle",
watchPaths: data.watchPaths,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -199,7 +210,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
@@ -382,6 +392,84 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Add paths to watch for changes. When files in these
paths change, a new deployment will be triggered.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((path, index) => (
<Badge key={index} variant="secondary">
{path}
<X
className="ml-1 size-3 cursor-pointer"
onClick={() => {
const newPaths = [...(field.value || [])];
newPaths.splice(index, 1);
form.setValue("watchPaths", newPaths);
}}
/>
</Badge>
))}
</div>
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
onKeyDown={(e) => {
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 = "";
}
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {
const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths);
input.value = "";
}
}}
>
Add
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "watchPaths" text[];

View File

@@ -0,0 +1 @@
ALTER TABLE "compose" ADD COLUMN "watchPaths" text[];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -526,6 +526,20 @@
"when": 1741490064139,
"tag": "0074_black_quasar",
"breakpoints": true
},
{
"idx": 75,
"version": "7",
"when": 1741491527516,
"tag": "0075_young_typhoid_mary",
"breakpoints": true
},
{
"idx": 76,
"version": "7",
"when": 1741493754270,
"tag": "0076_young_sharon_ventura",
"breakpoints": true
}
]
}

View File

@@ -36,6 +36,7 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"micromatch": "4.0.8",
"ai": "^4.0.23",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
@@ -154,6 +155,7 @@
"@tailwindcss/typography": "0.5.16"
},
"devDependencies": {
"@types/micromatch": "4.0.9",
"@types/qrcode": "^1.5.5",
"@types/nodemailer": "^6.4.15",
"@types/node-os-utils": "1.3.4",

View File

@@ -3,7 +3,7 @@ import { applications } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
@@ -21,6 +21,7 @@ export default async function handler(
where: eq(applications.refreshToken, refreshToken as string),
with: {
project: true,
bitbucket: true,
},
});
@@ -57,6 +58,20 @@ export default async function handler(
return;
}
} else if (sourceType === "github") {
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.branch) {
res.status(301).json({ message: "Branch Not Match" });
@@ -64,22 +79,55 @@ export default async function handler(
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== application.gitlabBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "bitbucket") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.bitbucketBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
const commitedPaths = await extractCommitedPaths(
req.body,
application.bitbucketOwner,
application.bitbucket?.appPassword || "",
application.bitbucketRepository || "",
);
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
commitedPaths,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
}
try {
@@ -231,3 +279,42 @@ export const extractBranchName = (headers: any, body: any) => {
return null;
};
export const extractCommitedPaths = async (
body: any,
bitbucketUsername: string | null,
bitbucketAppPassword: string | null,
repository: string | null,
) => {
const changes = body.push?.changes || [];
const commitHashes = changes
.map((change: any) => change.new?.target?.hash)
.filter(Boolean);
const commitedPaths: string[] = [];
for (const commit of commitHashes) {
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`;
try {
const response = await fetch(url, {
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
},
});
const data = await response.json();
for (const value of data.values) {
commitedPaths.push(value.new?.path);
}
} catch (error) {
console.error(
`Error fetching Bitbucket diffstat for commit ${commit}:`,
error instanceof Error ? error.message : "Unknown error",
);
return [];
}
}
return commitedPaths;
};

View File

@@ -3,11 +3,12 @@ import { compose } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import {
extractBranchName,
extractCommitedPaths,
extractCommitMessage,
extractHash,
} from "../[refreshToken]";
@@ -26,6 +27,7 @@ export default async function handler(
where: eq(compose.refreshToken, refreshToken as string),
with: {
project: true,
bitbucket: true,
},
});
@@ -46,16 +48,71 @@ export default async function handler(
if (sourceType === "github") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== composeResult.branch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
const normalizedCommits = req.body?.commits?.flatMap(
(commit: any) => commit.modified,
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
if (!branchName || branchName !== composeResult.gitlabBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "bitbucket") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.bitbucketBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "git") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== composeResult.customGitBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
const commitedPaths = await extractCommitedPaths(
req.body,
composeResult.bitbucketOwner,
composeResult.bitbucket?.appPassword || "",
composeResult.bitbucketRepository || "",
);
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
commitedPaths,
);
if (!shouldDeployPaths) {
res.status(301).json({ message: "Watch Paths Not Match" });
return;
}
}
try {

View File

@@ -9,6 +9,7 @@ import {
findPreviewDeploymentByApplicationId,
findPreviewDeploymentsByPullRequestId,
removePreviewDeployment,
shouldDeploy,
} from "@dokploy/server";
import { Webhooks } from "@octokit/webhooks";
import { and, eq } from "drizzle-orm";
@@ -90,11 +91,15 @@ export default async function handler(
if (req.headers["x-github-event"] === "push") {
try {
console.log("githubBody", githubBody.commits);
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
const normalizedCommits = githubBody?.commits?.flatMap(
(commit: any) => commit.modified,
);
const apps = await db.query.applications.findMany({
where: and(
@@ -116,6 +121,15 @@ export default async function handler(
server: !!app.serverId,
};
const shouldDeployPaths = shouldDeploy(
app.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
continue;
}
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
@@ -151,6 +165,14 @@ export default async function handler(
server: !!composeApp.serverId,
};
const shouldDeployPaths = shouldDeploy(
composeApp.watchPaths,
normalizedCommits,
);
if (!shouldDeployPaths) {
continue;
}
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);

View File

@@ -344,6 +344,7 @@ export const applicationRouter = createTRPCRouter({
buildPath: input.buildPath,
applicationStatus: "idle",
githubId: input.githubId,
watchPaths: input.watchPaths,
});
return true;
@@ -370,6 +371,7 @@ export const applicationRouter = createTRPCRouter({
gitlabId: input.gitlabId,
gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace,
watchPaths: input.watchPaths,
});
return true;
@@ -394,6 +396,7 @@ export const applicationRouter = createTRPCRouter({
sourceType: "bitbucket",
applicationStatus: "idle",
bitbucketId: input.bitbucketId,
watchPaths: input.watchPaths,
});
return true;
@@ -440,6 +443,7 @@ export const applicationRouter = createTRPCRouter({
customGitSSHKeyId: input.customGitSSHKeyId,
sourceType: "git",
applicationStatus: "idle",
watchPaths: input.watchPaths,
});
return true;

View File

@@ -28,6 +28,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"micromatch":"4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -79,6 +80,7 @@
"zod": "^3.23.4"
},
"devDependencies": {
"@types/micromatch": "4.0.9",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",

View File

@@ -116,6 +116,7 @@ export const applications = pgTable("application", {
description: text("description"),
env: text("env"),
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
@@ -406,6 +407,7 @@ const createSchema = createInsertSchema(applications, {
previewHttps: z.boolean().optional(),
previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
watchPaths: z.array(z.string()).optional(),
});
export const apiCreateApplication = createSchema.pick({
@@ -449,6 +451,7 @@ export const apiSaveGithubProvider = createSchema
owner: true,
buildPath: true,
githubId: true,
watchPaths: true,
})
.required();
@@ -462,6 +465,7 @@ export const apiSaveGitlabProvider = createSchema
gitlabId: true,
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
})
.required();
@@ -473,6 +477,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketRepository: true,
bitbucketId: true,
applicationId: true,
watchPaths: true,
})
.required();
@@ -492,6 +497,7 @@ export const apiSaveGitProvider = createSchema
applicationId: true,
customGitBuildPath: true,
customGitUrl: true,
watchPaths: true,
})
.required()
.merge(

View File

@@ -77,7 +77,7 @@ export const compose = pgTable("compose", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
watchPaths: text("watchPaths").array(),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
@@ -132,6 +132,7 @@ const createSchema = createInsertSchema(compose, {
command: z.string().optional(),
composePath: z.string().min(1),
composeType: z.enum(["docker-compose", "stack"]).optional(),
watchPaths: z.array(z.string()).optional(),
});
export const apiCreateCompose = createSchema.pick({

View File

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

View File

@@ -0,0 +1,9 @@
import micromatch from "micromatch";
export const shouldDeploy = (
watchPaths: string[] | null,
modifiedFiles: string[],
): boolean => {
if (!watchPaths || watchPaths?.length === 0) return true;
return micromatch.some(modifiedFiles, watchPaths);
};

37
pnpm-lock.yaml generated
View File

@@ -334,6 +334,9 @@ importers:
lucide-react:
specifier: ^0.469.0
version: 0.469.0(react@18.2.0)
micromatch:
specifier: 4.0.8
version: 4.0.8
nanoid:
specifier: '3'
version: 3.3.7
@@ -461,6 +464,9 @@ importers:
'@types/lodash':
specifier: 4.17.4
version: 4.17.4
'@types/micromatch':
specifier: 4.0.9
version: 4.0.9
'@types/node':
specifier: ^18.17.0
version: 18.19.42
@@ -675,6 +681,9 @@ importers:
lucia:
specifier: ^3.0.1
version: 3.2.0
micromatch:
specifier: 4.0.8
version: 4.0.8
nanoid:
specifier: '3'
version: 3.3.7
@@ -745,6 +754,9 @@ importers:
'@types/lodash':
specifier: 4.17.4
version: 4.17.4
'@types/micromatch':
specifier: 4.0.9
version: 4.0.9
'@types/node':
specifier: ^18.17.0
version: 18.19.42
@@ -3576,6 +3588,9 @@ packages:
'@types/body-parser@1.19.5':
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
'@types/braces@3.0.5':
resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==}
'@types/btoa-lite@1.0.2':
resolution: {integrity: sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==}
@@ -3675,6 +3690,9 @@ packages:
'@types/mdast@4.0.4':
resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
'@types/micromatch@4.0.9':
resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==}
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
@@ -5937,6 +5955,10 @@ packages:
resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==}
engines: {node: '>=8.6'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -10588,6 +10610,8 @@ snapshots:
'@types/connect': 3.4.38
'@types/node': 20.14.10
'@types/braces@3.0.5': {}
'@types/btoa-lite@1.0.2': {}
'@types/connect@3.4.38':
@@ -10701,6 +10725,10 @@ snapshots:
dependencies:
'@types/unist': 2.0.10
'@types/micromatch@4.0.9':
dependencies:
'@types/braces': 3.0.5
'@types/mime@1.3.5': {}
'@types/ms@0.7.34': {}
@@ -12071,7 +12099,7 @@ snapshots:
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.7
micromatch: 4.0.8
fast-json-patch@3.1.1: {}
@@ -13237,6 +13265,11 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
@@ -14690,7 +14723,7 @@ snapshots:
is-glob: 4.0.3
jiti: 1.21.6
lilconfig: 2.1.0
micromatch: 4.0.7
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.0.1