feat(github-webhooks): #186 implement github autodeployment with zero configuration

This commit is contained in:
Mauricio Siu 2024-07-01 23:43:08 -06:00
parent faf24dfa25
commit d2420ed6e8
11 changed files with 143 additions and 95 deletions

View File

@ -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) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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

View File

@ -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) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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");

View File

@ -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) => {
<span className="text-sm text-muted-foreground">
{deployment.title}
</span>
{deployment.description && (
<span className="text-sm text-muted-foreground">
{deployment.description}
</span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground">

View File

@ -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) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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

View File

@ -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) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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");

View File

@ -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) => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
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");

View File

@ -48,6 +48,7 @@ export const GithubSetup = () => {
const [organizationName, setOrganization] = useState<string>("");
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

View File

@ -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,

View File

@ -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,

View 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 });
}
}

View File

@ -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<DeploymentJob>) => {
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,