From d2420ed6e83907370e80cb616869b1294c022d27 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:43:08 -0600 Subject: [PATCH] feat(github-webhooks): #186 implement github autodeployment with zero configuration --- .../general/deploy-application.tsx | 27 ++---- .../application/rebuild-application.tsx | 19 +--- .../deployments/show-deployments-compose.tsx | 8 +- .../compose/general/deploy-compose.tsx | 26 ++--- .../compose/general/rebuild-compose.tsx | 18 +--- .../compose/general/stop-compose.tsx | 18 +--- .../settings/github/github-setup.tsx | 3 +- pages/api/deploy/[refreshToken].ts | 6 -- pages/api/deploy/compose/[refreshToken].ts | 5 - pages/api/deploy/github.ts | 97 +++++++++++++++++++ server/queues/deployments-queue.ts | 11 ++- 11 files changed, 143 insertions(+), 95 deletions(-) create mode 100644 pages/api/deploy/github.ts diff --git a/components/dashboard/application/general/deploy-application.tsx b/components/dashboard/application/general/deploy-application.tsx index 47fbb5c0e..d8a333841 100644 --- a/components/dashboard/application/general/deploy-application.tsx +++ b/components/dashboard/application/general/deploy-application.tsx @@ -11,7 +11,6 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; import { toast } from "sonner"; interface Props { @@ -26,8 +25,6 @@ export const DeployApplication = ({ applicationId }: Props) => { { enabled: !!applicationId }, ); - const { mutateAsync: markRunning } = - api.application.markRunning.useMutation(); const { mutateAsync: deploy } = api.application.deploy.useMutation(); return ( @@ -48,24 +45,16 @@ export const DeployApplication = ({ applicationId }: Props) => { Cancel { - await markRunning({ + toast.success("Deploying Application...."); + + await refetch(); + await deploy({ applicationId, - }) - .then(async () => { - toast.success("Deploying Application...."); + }).catch(() => { + toast.error("Error to deploy Application"); + }); - await refetch(); - await deploy({ - applicationId, - }).catch(() => { - toast.error("Error to deploy Application"); - }); - - await refetch(); - }) - .catch((e) => { - toast.error(e.message || "Error to deploy Application"); - }); + await refetch(); }} > Confirm diff --git a/components/dashboard/application/rebuild-application.tsx b/components/dashboard/application/rebuild-application.tsx index 92e8bb4ef..0284ab8f6 100644 --- a/components/dashboard/application/rebuild-application.tsx +++ b/components/dashboard/application/rebuild-application.tsx @@ -25,8 +25,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => { }, { enabled: !!applicationId }, ); - const { mutateAsync: markRunning } = - api.application.markRunning.useMutation(); + const { mutateAsync } = api.application.redeploy.useMutation(); const utils = api.useUtils(); return ( @@ -54,22 +53,14 @@ export const RedbuildApplication = ({ applicationId }: Props) => { Cancel { - await markRunning({ + toast.success("Redeploying Application...."); + await mutateAsync({ applicationId, }) .then(async () => { - await mutateAsync({ + await utils.application.one.invalidate({ applicationId, - }) - .then(async () => { - await utils.application.one.invalidate({ - applicationId, - }); - toast.success("Application rebuild succesfully"); - }) - .catch(() => { - toast.error("Error to rebuild the application"); - }); + }); }) .catch(() => { toast.error("Error to rebuild the application"); diff --git a/components/dashboard/compose/deployments/show-deployments-compose.tsx b/components/dashboard/compose/deployments/show-deployments-compose.tsx index a36887993..b4de30b67 100644 --- a/components/dashboard/compose/deployments/show-deployments-compose.tsx +++ b/components/dashboard/compose/deployments/show-deployments-compose.tsx @@ -9,14 +9,11 @@ import { import { api } from "@/utils/api"; import { RocketIcon } from "lucide-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 { DateTooltip } from "@/components/shared/date-tooltip"; import { ShowDeploymentCompose } from "./show-deployment-compose"; import { RefreshTokenCompose } from "./refresh-token-compose"; import { CancelQueuesCompose } from "./cancel-queues-compose"; -// import { RefreshToken } from "./refresh-token";// interface Props { composeId: string; @@ -90,6 +87,11 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => { {deployment.title} + {deployment.description && ( + + {deployment.description} + + )}
diff --git a/components/dashboard/compose/general/deploy-compose.tsx b/components/dashboard/compose/general/deploy-compose.tsx index 1617ee7bb..e9d5dfc19 100644 --- a/components/dashboard/compose/general/deploy-compose.tsx +++ b/components/dashboard/compose/general/deploy-compose.tsx @@ -25,7 +25,6 @@ export const DeployCompose = ({ composeId }: Props) => { { enabled: !!composeId }, ); - const { mutateAsync: markRunning } = api.compose.update.useMutation(); const { mutateAsync: deploy } = api.compose.deploy.useMutation(); return ( @@ -44,25 +43,16 @@ export const DeployCompose = ({ composeId }: Props) => { Cancel { - await markRunning({ + toast.success("Deploying Compose...."); + + await refetch(); + await deploy({ composeId, - composeStatus: "running", - }) - .then(async () => { - toast.success("Deploying Compose...."); + }).catch(() => { + toast.error("Error to deploy Compose"); + }); - await refetch(); - await deploy({ - composeId, - }).catch(() => { - toast.error("Error to deploy Compose"); - }); - - await refetch(); - }) - .catch((e) => { - toast.error(e.message || "Error to deploy Compose"); - }); + await refetch(); }} > Confirm diff --git a/components/dashboard/compose/general/rebuild-compose.tsx b/components/dashboard/compose/general/rebuild-compose.tsx index 25ad6d079..199d4f93d 100644 --- a/components/dashboard/compose/general/rebuild-compose.tsx +++ b/components/dashboard/compose/general/rebuild-compose.tsx @@ -25,7 +25,6 @@ export const RedbuildCompose = ({ composeId }: Props) => { }, { enabled: !!composeId }, ); - const { mutateAsync: markRunning } = api.compose.update.useMutation(); const { mutateAsync } = api.compose.redeploy.useMutation(); const utils = api.useUtils(); return ( @@ -53,23 +52,14 @@ export const RedbuildCompose = ({ composeId }: Props) => { Cancel { - await markRunning({ + toast.success("Redeploying Compose...."); + await mutateAsync({ composeId, - composeStatus: "running", }) .then(async () => { - await mutateAsync({ + await utils.compose.one.invalidate({ composeId, - }) - .then(async () => { - await utils.compose.one.invalidate({ - composeId, - }); - toast.success("Compose rebuild succesfully"); - }) - .catch(() => { - toast.error("Error to rebuild the compose"); - }); + }); }) .catch(() => { toast.error("Error to rebuild the compose"); diff --git a/components/dashboard/compose/general/stop-compose.tsx b/components/dashboard/compose/general/stop-compose.tsx index 029cce113..2bb3cdebe 100644 --- a/components/dashboard/compose/general/stop-compose.tsx +++ b/components/dashboard/compose/general/stop-compose.tsx @@ -25,7 +25,6 @@ export const StopCompose = ({ composeId }: Props) => { }, { enabled: !!composeId }, ); - const { mutateAsync: markRunning } = api.compose.update.useMutation(); const { mutateAsync, isLoading } = api.compose.stop.useMutation(); const utils = api.useUtils(); return ( @@ -47,23 +46,14 @@ export const StopCompose = ({ composeId }: Props) => { Cancel { - await markRunning({ + await mutateAsync({ composeId, - composeStatus: "running", }) .then(async () => { - await mutateAsync({ + await utils.compose.one.invalidate({ composeId, - }) - .then(async () => { - await utils.compose.one.invalidate({ - composeId, - }); - toast.success("Compose rebuild succesfully"); - }) - .catch(() => { - toast.error("Error to stop the compose"); - }); + }); + toast.success("Compose stopped succesfully"); }) .catch(() => { toast.error("Error to stop the compose"); diff --git a/components/dashboard/settings/github/github-setup.tsx b/components/dashboard/settings/github/github-setup.tsx index 1e78d001f..0119ebfe2 100644 --- a/components/dashboard/settings/github/github-setup.tsx +++ b/components/dashboard/settings/github/github-setup.tsx @@ -48,6 +48,7 @@ export const GithubSetup = () => { const [organizationName, setOrganization] = useState(""); const { data } = api.admin.one.useQuery(); useEffect(() => { + const url = document.location.origin; const manifest = JSON.stringify( { redirect_url: `${origin}/api/redirect?authId=${data?.authId}`, @@ -55,7 +56,7 @@ export const GithubSetup = () => { url: origin, hook_attributes: { // 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 }, callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación diff --git a/pages/api/deploy/[refreshToken].ts b/pages/api/deploy/[refreshToken].ts index c2aa3f16f..d05f7e1ef 100644 --- a/pages/api/deploy/[refreshToken].ts +++ b/pages/api/deploy/[refreshToken].ts @@ -1,4 +1,3 @@ -import { updateApplicationStatus } from "@/server/api/services/application"; import { db } from "@/server/db"; import { applications } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/deployments-queue"; @@ -68,11 +67,6 @@ export default async function handler( } try { - await updateApplicationStatus( - application.applicationId as string, - "running", - ); - const jobData: DeploymentJob = { applicationId: application.applicationId as string, titleLog: deploymentTitle, diff --git a/pages/api/deploy/compose/[refreshToken].ts b/pages/api/deploy/compose/[refreshToken].ts index 1c5739818..b750ffac6 100644 --- a/pages/api/deploy/compose/[refreshToken].ts +++ b/pages/api/deploy/compose/[refreshToken].ts @@ -9,7 +9,6 @@ import { extractCommitMessage, extractHash, } from "../[refreshToken]"; -import { updateCompose } from "@/server/api/services/compose"; export default async function handler( req: NextApiRequest, @@ -56,10 +55,6 @@ export default async function handler( } try { - await updateCompose(composeResult.composeId, { - composeStatus: "running", - }); - const jobData: DeploymentJob = { composeId: composeResult.composeId as string, titleLog: deploymentTitle, diff --git a/pages/api/deploy/github.ts b/pages/api/deploy/github.ts new file mode 100644 index 000000000..e088777b6 --- /dev/null +++ b/pages/api/deploy/github.ts @@ -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 }); + } +} diff --git a/server/queues/deployments-queue.ts b/server/queues/deployments-queue.ts index 54f23073b..94a92219f 100644 --- a/server/queues/deployments-queue.ts +++ b/server/queues/deployments-queue.ts @@ -2,9 +2,14 @@ import { type Job, Worker } from "bullmq"; import { deployApplication, rebuildApplication, + updateApplicationStatus, } from "../api/services/application"; import { myQueue, redisConfig } from "./queueSetup"; -import { deployCompose, rebuildCompose } from "../api/services/compose"; +import { + deployCompose, + rebuildCompose, + updateCompose, +} from "../api/services/compose"; type DeployJob = | { @@ -29,6 +34,7 @@ export const deploymentWorker = new Worker( async (job: Job) => { try { if (job.data.applicationType === "application") { + await updateApplicationStatus(job.data.applicationId, "running"); if (job.data.type === "redeploy") { await rebuildApplication({ applicationId: job.data.applicationId, @@ -43,6 +49,9 @@ export const deploymentWorker = new Worker( }); } } else if (job.data.applicationType === "compose") { + await updateCompose(job.data.composeId, { + composeStatus: "running", + }); if (job.data.type === "deploy") { await deployCompose({ composeId: job.data.composeId,