Merge pull request #1613 from TheoD02/feat/github-triggerType
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled

feat(github): add triggerType field to GitHub provider and handle tag creation events
This commit is contained in:
Mauricio Siu
2025-04-27 18:43:28 -06:00
committed by GitHub
12 changed files with 5753 additions and 117 deletions

View File

@@ -36,6 +36,7 @@ const baseApp: ApplicationNested = {
watchPaths: [], watchPaths: [],
enableSubmodules: false, enableSubmodules: false,
applicationStatus: "done", applicationStatus: "done",
triggerType: "push",
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
serverId: "", serverId: "",

View File

@@ -25,6 +25,7 @@ const baseApp: ApplicationNested = {
buildArgs: null, buildArgs: null,
isPreviewDeploymentsActive: false, isPreviewDeploymentsActive: false,
previewBuildArgs: null, previewBuildArgs: null,
triggerType: "push",
previewCertificateType: "none", previewCertificateType: "none",
previewEnv: null, previewEnv: null,
previewHttps: false, previewHttps: false,

View File

@@ -58,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),
}); });
@@ -83,6 +84,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
}, },
githubId: "", githubId: "",
branch: "", branch: "",
triggerType: "push",
enableSubmodules: false, enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
@@ -90,6 +92,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
const repository = form.watch("repository"); const repository = form.watch("repository");
const githubId = form.watch("githubId"); const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
const { data: repositories, isLoading: isLoadingRepositories } = const { data: repositories, isLoading: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery( api.github.getGithubRepositories.useQuery(
@@ -127,6 +130,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath || "/", buildPath: data.buildPath || "/",
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
triggerType: data.triggerType || "push",
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
@@ -141,6 +145,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
buildPath: data.buildPath, buildPath: data.buildPath,
githubId: data.githubId, githubId: data.githubId,
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
triggerType: data.triggerType,
enableSubmodules: data.enableSubmodules, enableSubmodules: data.enableSubmodules,
}) })
.then(async () => { .then(async () => {
@@ -386,11 +391,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="watchPaths" name="triggerType"
render={({ field }) => ( render={({ field }) => (
<FormItem className="md:col-span-2"> <FormItem className="md:col-span-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 ">
<FormLabel>Watch Paths</FormLabel> <FormLabel>Trigger Type</FormLabel>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -398,71 +403,114 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> <p>
Add paths to watch for changes. When files in these Choose when to trigger deployments: on push to the
paths change, a new deployment will be triggered. selected branch or when a new tag is created.
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
<div className="flex flex-wrap gap-2 mb-2"> <Select
{field.value?.map((path, index) => ( onValueChange={field.onChange}
<Badge defaultValue={field.value}
key={index} value={field.value}
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> <FormControl>
<Input <SelectTrigger>
placeholder="Enter a path to watch (e.g., src/*, dist/*)" <SelectValue placeholder="Select a trigger type" />
onKeyDown={(e) => { </SelectTrigger>
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> </FormControl>
<Button <SelectContent>
type="button" <SelectItem value="push">On Push</SelectItem>
variant="outline" <SelectItem value="tag">On Tag</SelectItem>
size="icon" </SelectContent>
onClick={() => { </Select>
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 /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{triggerType === "push" && (
<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>
)}
/>
)}
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -40,7 +40,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -58,6 +58,7 @@ const GithubProviderSchema = z.object({
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),
}); });
@@ -84,6 +85,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
githubId: "", githubId: "",
branch: "", branch: "",
watchPaths: [], watchPaths: [],
triggerType: "push",
enableSubmodules: false, enableSubmodules: false,
}, },
resolver: zodResolver(GithubProviderSchema), resolver: zodResolver(GithubProviderSchema),
@@ -91,7 +93,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository"); const repository = form.watch("repository");
const githubId = form.watch("githubId"); const githubId = form.watch("githubId");
const triggerType = form.watch("triggerType");
const { data: repositories, isLoading: isLoadingRepositories } = const { data: repositories, isLoading: isLoadingRepositories } =
api.github.getGithubRepositories.useQuery( api.github.getGithubRepositories.useQuery(
{ {
@@ -128,6 +130,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composePath: data.composePath, composePath: data.composePath,
githubId: data.githubId || "", githubId: data.githubId || "",
watchPaths: data.watchPaths || [], watchPaths: data.watchPaths || [],
triggerType: data.triggerType || "push",
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
@@ -145,6 +148,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
composeStatus: "idle", composeStatus: "idle",
watchPaths: data.watchPaths, watchPaths: data.watchPaths,
enableSubmodules: data.enableSubmodules, enableSubmodules: data.enableSubmodules,
triggerType: data.triggerType,
}) })
.then(async () => { .then(async () => {
toast.success("Service Provided Saved"); toast.success("Service Provided Saved");
@@ -389,82 +393,128 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
/> />
<FormField <FormField
control={form.control} control={form.control}
name="watchPaths" name="triggerType"
render={({ field }) => ( render={({ field }) => (
<FormItem className="md:col-span-2"> <FormItem className="md:col-span-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel> <FormLabel>Trigger Type</FormLabel>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger asChild>
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold"> <HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
?
</div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> <p>
Add paths to watch for changes. When files in these Choose when to trigger deployments: on push to the
paths change, a new deployment will be triggered. selected branch or when a new tag is created.
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
<div className="flex flex-wrap gap-2 mb-2"> <Select
{field.value?.map((path, index) => ( onValueChange={field.onChange}
<Badge key={index} variant="secondary"> defaultValue={field.value}
{path} value={field.value}
<X >
className="ml-1 size-3 cursor-pointer" <FormControl>
onClick={() => { <SelectTrigger>
const newPaths = [...(field.value || [])]; <SelectValue placeholder="Select a trigger type" />
newPaths.splice(index, 1); </SelectTrigger>
form.setValue("watchPaths", newPaths); </FormControl>
<SelectContent>
<SelectItem value="push">On Push</SelectItem>
<SelectItem value="tag">On Tag</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{triggerType === "push" && (
<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 = "";
}
}
}} }}
/> />
</Badge> <Button
))} type="button"
</div> variant="secondary"
<FormControl> onClick={() => {
<div className="flex gap-2"> const input = document.querySelector(
<Input 'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
placeholder="Enter a path to watch (e.g., src/*, dist/*)" ) as HTMLInputElement;
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim(); const value = input.value.trim();
if (value) { if (value) {
const newPaths = [...(field.value || []), value]; const newPaths = [...(field.value || []), value];
form.setValue("watchPaths", newPaths); form.setValue("watchPaths", newPaths);
input.value = ""; input.value = "";
} }
} }}
}} >
/> Add
<Button </Button>
type="button" </div>
variant="secondary" </FormControl>
onClick={() => { <FormMessage />
const input = document.querySelector( </FormItem>
'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>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="enableSubmodules" name="enableSubmodules"

View File

@@ -0,0 +1,3 @@
CREATE TYPE "public"."triggerType" AS ENUM('push', 'tag');--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "triggerType" "triggerType" DEFAULT 'push';--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "triggerType" "triggerType" DEFAULT 'push';

File diff suppressed because it is too large Load Diff

View File

@@ -610,6 +610,13 @@
"when": 1745706676004, "when": 1745706676004,
"tag": "0086_rainy_gertrude_yorkes", "tag": "0086_rainy_gertrude_yorkes",
"breakpoints": true "breakpoints": true
},
{
"idx": 87,
"version": "7",
"when": 1745723563822,
"tag": "0087_lively_risque",
"breakpoints": true
} }
] ]
} }

View File

@@ -89,6 +89,115 @@ export default async function handler(
return; return;
} }
// Handle tag creation event
if (
req.headers["x-github-event"] === "push" &&
githubBody?.ref?.startsWith("refs/tags/")
) {
try {
const tagName = githubBody?.ref.replace("refs/tags/", "");
const repository = githubBody?.repository?.name;
const owner = githubBody?.repository?.owner?.name;
const deploymentTitle = `Tag created: ${tagName}`;
const deploymentHash = extractHash(req.headers, githubBody);
// Find applications configured to deploy on tag
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true),
eq(applications.triggerType, "tag"),
eq(applications.repository, repository),
eq(applications.owner, owner),
eq(applications.githubId, githubResult.githubId),
),
});
for (const app of apps) {
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
// Find compose apps configured to deploy on tag
const composeApps = await db.query.compose.findMany({
where: and(
eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true),
eq(compose.triggerType, "tag"),
eq(compose.repository, repository),
eq(compose.owner, owner),
eq(compose.githubId, githubResult.githubId),
),
});
for (const composeApp of composeApps) {
const jobData: DeploymentJob = {
composeId: composeApp.composeId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeApp.serverId,
};
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
continue;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
const totalApps = apps.length + composeApps.length;
if (totalApps === 0) {
res
.status(200)
.json({ message: "No apps configured to deploy on tag" });
return;
}
res.status(200).json({
message: `Deployed ${totalApps} apps based on tag ${tagName}`,
});
return;
} catch (error) {
console.error("Error deploying applications on tag:", error);
res
.status(400)
.json({ message: "Error deploying applications on tag", error });
return;
}
}
if (req.headers["x-github-event"] === "push") { if (req.headers["x-github-event"] === "push") {
try { try {
const branchName = githubBody?.ref?.replace("refs/heads/", ""); const branchName = githubBody?.ref?.replace("refs/heads/", "");
@@ -105,6 +214,7 @@ export default async function handler(
where: and( where: and(
eq(applications.sourceType, "github"), eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true), eq(applications.autoDeploy, true),
eq(applications.triggerType, "push"),
eq(applications.branch, branchName), eq(applications.branch, branchName),
eq(applications.repository, repository), eq(applications.repository, repository),
eq(applications.owner, owner), eq(applications.owner, owner),
@@ -150,6 +260,7 @@ export default async function handler(
where: and( where: and(
eq(compose.sourceType, "github"), eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true), eq(compose.autoDeploy, true),
eq(compose.triggerType, "push"),
eq(compose.branch, branchName), eq(compose.branch, branchName),
eq(compose.repository, repository), eq(compose.repository, repository),
eq(compose.owner, owner), eq(compose.owner, owner),

View File

@@ -355,6 +355,7 @@ export const applicationRouter = createTRPCRouter({
applicationStatus: "idle", applicationStatus: "idle",
githubId: input.githubId, githubId: input.githubId,
watchPaths: input.watchPaths, watchPaths: input.watchPaths,
triggerType: input.triggerType,
enableSubmodules: input.enableSubmodules, enableSubmodules: input.enableSubmodules,
}); });

View File

@@ -24,7 +24,7 @@ import { redirects } from "./redirects";
import { registry } from "./registry"; import { registry } from "./registry";
import { security } from "./security"; import { security } from "./security";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus, certificateType } from "./shared"; import { applicationStatus, certificateType, triggerType } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
@@ -149,6 +149,7 @@ export const applications = pgTable("application", {
owner: text("owner"), owner: text("owner"),
branch: text("branch"), branch: text("branch"),
buildPath: text("buildPath").default("/"), buildPath: text("buildPath").default("/"),
triggerType: triggerType("triggerType").default("push"),
autoDeploy: boolean("autoDeploy").$defaultFn(() => true), autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
// Gitlab // Gitlab
gitlabProjectId: integer("gitlabProjectId"), gitlabProjectId: integer("gitlabProjectId"),
@@ -473,7 +474,10 @@ export const apiSaveGithubProvider = createSchema
watchPaths: true, watchPaths: true,
enableSubmodules: true, enableSubmodules: true,
}) })
.required(); .required()
.extend({
triggerType: z.enum(["push", "tag"]).default("push"),
});
export const apiSaveGitlabProvider = createSchema export const apiSaveGitlabProvider = createSchema
.pick({ .pick({

View File

@@ -12,7 +12,7 @@ import { gitlab } from "./gitlab";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { projects } from "./project"; import { projects } from "./project";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus } from "./shared"; import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
@@ -77,6 +77,7 @@ export const compose = pgTable("compose", {
suffix: text("suffix").notNull().default(""), suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false), randomize: boolean("randomize").notNull().default(false),
isolatedDeployment: boolean("isolatedDeployment").notNull().default(false), isolatedDeployment: boolean("isolatedDeployment").notNull().default(false),
triggerType: triggerType("triggerType").default("push"),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"), composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId") projectId: text("projectId")
.notNull() .notNull()

View File

@@ -12,3 +12,5 @@ export const certificateType = pgEnum("certificateType", [
"none", "none",
"custom", "custom",
]); ]);
export const triggerType = pgEnum("triggerType", ["push", "tag"]);