mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(github-webhooks): #186 implement github autodeployment with zero configuration
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -26,8 +25,6 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
|||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: markRunning } =
|
|
||||||
api.application.markRunning.useMutation();
|
|
||||||
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,24 +45,16 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await markRunning({
|
toast.success("Deploying Application....");
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
await deploy({
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
}).catch(() => {
|
||||||
.then(async () => {
|
toast.error("Error to deploy Application");
|
||||||
toast.success("Deploying Application....");
|
});
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
|
||||||
applicationId,
|
|
||||||
}).catch(() => {
|
|
||||||
toast.error("Error to deploy Application");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error(e.message || "Error to deploy Application");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
|||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: markRunning } =
|
|
||||||
api.application.markRunning.useMutation();
|
|
||||||
const { mutateAsync } = api.application.redeploy.useMutation();
|
const { mutateAsync } = api.application.redeploy.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
@@ -54,22 +53,14 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await markRunning({
|
toast.success("Redeploying Application....");
|
||||||
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await mutateAsync({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
});
|
||||||
.then(async () => {
|
|
||||||
await utils.application.one.invalidate({
|
|
||||||
applicationId,
|
|
||||||
});
|
|
||||||
toast.success("Application rebuild succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to rebuild the application");
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to rebuild the application");
|
toast.error("Error to rebuild the application");
|
||||||
|
|||||||
@@ -9,14 +9,11 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RocketIcon } from "lucide-react";
|
import { RocketIcon } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// import { CancelQueues } from "./cancel-queues";
|
|
||||||
// import { ShowDeployment } from "./show-deployment-compose";
|
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { ShowDeploymentCompose } from "./show-deployment-compose";
|
import { ShowDeploymentCompose } from "./show-deployment-compose";
|
||||||
import { RefreshTokenCompose } from "./refresh-token-compose";
|
import { RefreshTokenCompose } from "./refresh-token-compose";
|
||||||
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
||||||
// import { RefreshToken } from "./refresh-token";//
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
composeId: string;
|
composeId: string;
|
||||||
@@ -90,6 +87,11 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{deployment.title}
|
{deployment.title}
|
||||||
</span>
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const DeployCompose = ({ composeId }: Props) => {
|
|||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: markRunning } = api.compose.update.useMutation();
|
|
||||||
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
const { mutateAsync: deploy } = api.compose.deploy.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,25 +43,16 @@ export const DeployCompose = ({ composeId }: Props) => {
|
|||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await markRunning({
|
toast.success("Deploying Compose....");
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
await deploy({
|
||||||
composeId,
|
composeId,
|
||||||
composeStatus: "running",
|
}).catch(() => {
|
||||||
})
|
toast.error("Error to deploy Compose");
|
||||||
.then(async () => {
|
});
|
||||||
toast.success("Deploying Compose....");
|
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
|
||||||
composeId,
|
|
||||||
}).catch(() => {
|
|
||||||
toast.error("Error to deploy Compose");
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error(e.message || "Error to deploy Compose");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const RedbuildCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: markRunning } = api.compose.update.useMutation();
|
|
||||||
const { mutateAsync } = api.compose.redeploy.useMutation();
|
const { mutateAsync } = api.compose.redeploy.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
@@ -53,23 +52,14 @@ export const RedbuildCompose = ({ composeId }: Props) => {
|
|||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await markRunning({
|
toast.success("Redeploying Compose....");
|
||||||
|
await mutateAsync({
|
||||||
composeId,
|
composeId,
|
||||||
composeStatus: "running",
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await mutateAsync({
|
await utils.compose.one.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
})
|
});
|
||||||
.then(async () => {
|
|
||||||
await utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success("Compose rebuild succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to rebuild the compose");
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to rebuild the compose");
|
toast.error("Error to rebuild the compose");
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export const StopCompose = ({ composeId }: Props) => {
|
|||||||
},
|
},
|
||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
const { mutateAsync: markRunning } = api.compose.update.useMutation();
|
|
||||||
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
|
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
@@ -47,23 +46,14 @@ export const StopCompose = ({ composeId }: Props) => {
|
|||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await markRunning({
|
await mutateAsync({
|
||||||
composeId,
|
composeId,
|
||||||
composeStatus: "running",
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await mutateAsync({
|
await utils.compose.one.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
})
|
});
|
||||||
.then(async () => {
|
toast.success("Compose stopped succesfully");
|
||||||
await utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success("Compose rebuild succesfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to stop the compose");
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to stop the compose");
|
toast.error("Error to stop the compose");
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const GithubSetup = () => {
|
|||||||
const [organizationName, setOrganization] = useState<string>("");
|
const [organizationName, setOrganization] = useState<string>("");
|
||||||
const { data } = api.admin.one.useQuery();
|
const { data } = api.admin.one.useQuery();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const url = document.location.origin;
|
||||||
const manifest = JSON.stringify(
|
const manifest = JSON.stringify(
|
||||||
{
|
{
|
||||||
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
||||||
@@ -55,7 +56,7 @@ export const GithubSetup = () => {
|
|||||||
url: origin,
|
url: origin,
|
||||||
hook_attributes: {
|
hook_attributes: {
|
||||||
// JUST FOR TESTING
|
// JUST FOR TESTING
|
||||||
url: "https://webhook.site/b6a167c0-ceb5-4f0c-a257-97c0fd163977",
|
url: `${url}/api/deploy/github`,
|
||||||
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
||||||
},
|
},
|
||||||
callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación
|
callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { updateApplicationStatus } from "@/server/api/services/application";
|
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { applications } from "@/server/db/schema";
|
import { applications } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||||
@@ -68,11 +67,6 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateApplicationStatus(
|
|
||||||
application.applicationId as string,
|
|
||||||
"running",
|
|
||||||
);
|
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: application.applicationId as string,
|
applicationId: application.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
extractCommitMessage,
|
extractCommitMessage,
|
||||||
extractHash,
|
extractHash,
|
||||||
} from "../[refreshToken]";
|
} from "../[refreshToken]";
|
||||||
import { updateCompose } from "@/server/api/services/compose";
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
@@ -56,10 +55,6 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateCompose(composeResult.composeId, {
|
|
||||||
composeStatus: "running",
|
|
||||||
});
|
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: composeResult.composeId as string,
|
composeId: composeResult.composeId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
|
|||||||
97
pages/api/deploy/github.ts
Normal file
97
pages/api/deploy/github.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { applications, compose } from "@/server/db/schema";
|
||||||
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
|
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
||||||
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
const github = req.body;
|
||||||
|
|
||||||
|
if (req.headers["x-github-event"] === "ping") {
|
||||||
|
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.headers["x-github-event"] !== "push") {
|
||||||
|
res.status(400).json({ message: "We only accept push events" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const branchName = github?.ref?.replace("refs/heads/", "");
|
||||||
|
const repository = github?.repository?.name;
|
||||||
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
|
|
||||||
|
const apps = await db.query.applications.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(applications.sourceType, "github"),
|
||||||
|
eq(applications.autoDeploy, true),
|
||||||
|
eq(applications.branch, branchName),
|
||||||
|
eq(applications.repository, repository),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const app of apps) {
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
titleLog: deploymentTitle,
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "application",
|
||||||
|
};
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeApps = await db.query.compose.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(compose.sourceType, "github"),
|
||||||
|
eq(compose.autoDeploy, true),
|
||||||
|
eq(compose.branch, branchName),
|
||||||
|
eq(compose.repository, repository),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const composeApp of composeApps) {
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
composeId: composeApp.composeId as string,
|
||||||
|
titleLog: deploymentTitle,
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "compose",
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalApps = apps.length + composeApps.length;
|
||||||
|
const emptyApps = totalApps === 0;
|
||||||
|
|
||||||
|
if (emptyApps) {
|
||||||
|
res.status(200).json({ message: "No apps to deploy" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ message: "Error To Deploy Application", error });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,14 @@ import { type Job, Worker } from "bullmq";
|
|||||||
import {
|
import {
|
||||||
deployApplication,
|
deployApplication,
|
||||||
rebuildApplication,
|
rebuildApplication,
|
||||||
|
updateApplicationStatus,
|
||||||
} from "../api/services/application";
|
} from "../api/services/application";
|
||||||
import { myQueue, redisConfig } from "./queueSetup";
|
import { myQueue, redisConfig } from "./queueSetup";
|
||||||
import { deployCompose, rebuildCompose } from "../api/services/compose";
|
import {
|
||||||
|
deployCompose,
|
||||||
|
rebuildCompose,
|
||||||
|
updateCompose,
|
||||||
|
} from "../api/services/compose";
|
||||||
|
|
||||||
type DeployJob =
|
type DeployJob =
|
||||||
| {
|
| {
|
||||||
@@ -29,6 +34,7 @@ export const deploymentWorker = new Worker(
|
|||||||
async (job: Job<DeploymentJob>) => {
|
async (job: Job<DeploymentJob>) => {
|
||||||
try {
|
try {
|
||||||
if (job.data.applicationType === "application") {
|
if (job.data.applicationType === "application") {
|
||||||
|
await updateApplicationStatus(job.data.applicationId, "running");
|
||||||
if (job.data.type === "redeploy") {
|
if (job.data.type === "redeploy") {
|
||||||
await rebuildApplication({
|
await rebuildApplication({
|
||||||
applicationId: job.data.applicationId,
|
applicationId: job.data.applicationId,
|
||||||
@@ -43,6 +49,9 @@ export const deploymentWorker = new Worker(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (job.data.applicationType === "compose") {
|
} else if (job.data.applicationType === "compose") {
|
||||||
|
await updateCompose(job.data.composeId, {
|
||||||
|
composeStatus: "running",
|
||||||
|
});
|
||||||
if (job.data.type === "deploy") {
|
if (job.data.type === "deploy") {
|
||||||
await deployCompose({
|
await deployCompose({
|
||||||
composeId: job.data.composeId,
|
composeId: job.data.composeId,
|
||||||
|
|||||||
Reference in New Issue
Block a user