Skip to content

Commit

Permalink
force release
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 committed Sep 11, 2024
1 parent 7527ada commit 5fe0aa7
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
"use client";

import { useRouter } from "next/navigation";
import { TbDotsVertical, TbReload } from "react-icons/tb";
import { TbAlertTriangle, TbDotsVertical, TbReload } from "react-icons/tb";

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@ctrlplane/ui/alert-dialog";
import { Badge } from "@ctrlplane/ui/badge";
import { Button } from "@ctrlplane/ui/button";
import { Button, buttonVariants } from "@ctrlplane/ui/button";
import {
Dialog,
DialogContent,
Expand All @@ -23,7 +34,7 @@ import {

import { api } from "~/trpc/react";

export const ReleaseDropdownMenu: React.FC<{
const RedeployReleaseDialog: React.FC<{
release: {
id: string;
name: string;
Expand All @@ -32,62 +43,133 @@ export const ReleaseDropdownMenu: React.FC<{
id: string;
name: string;
};
isReleaseCompleted: boolean;
}> = ({ release, environment, isReleaseCompleted }) => {
children: React.ReactNode;
}> = ({ release, environment, children }) => {
const router = useRouter();
const redeploy = api.release.deploy.toEnvironment.useMutation();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<TbDotsVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
disabled={!isReleaseCompleted}
onSelect={(e) => e.preventDefault()}
className="space-x-2"
>
<TbReload />
<span>Redeploy</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Redeploy{" "}
<Badge variant="secondary" className="h-7 text-lg">
{release.name}
</Badge>{" "}
to {environment.name}?
</DialogTitle>
<DialogDescription>
This will redeploy the release to all targets in the
environment.
</DialogDescription>
</DialogHeader>
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Redeploy{" "}
<Badge variant="secondary" className="h-7 text-lg">
{release.name}
</Badge>{" "}
to {environment.name}?
</DialogTitle>
<DialogDescription>
This will redeploy the release to all targets in the environment.
</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button
onClick={() =>
redeploy
.mutateAsync({
environmentId: environment.id,
releaseId: release.id,
})
.then(() => router.refresh())
}
>
Redeploy
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DropdownMenuContent>
</DropdownMenu>
<DialogFooter>
<Button
onClick={() =>
redeploy
.mutateAsync({
environmentId: environment.id,
releaseId: release.id,
})
.then(() => router.refresh())
}
>
Redeploy
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

const ForceReleaseDialog: React.FC<{
release: {
id: string;
name: string;
};
environment: {
id: string;
name: string;
};
children: React.ReactNode;
}> = ({ release, environment, children }) => {
const forceDeploy = api.release.deploy.toEnvironment.useMutation();
const router = useRouter();
return (
<AlertDialog>
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Force release {release.name} to {environment.name}?
</AlertDialogTitle>
<AlertDialogDescription>
This will force the release to be deployed to all targets in the
environment regardless of any policies set on the environment.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<div className="flex-grow" />
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
onClick={() =>
forceDeploy
.mutateAsync({
environmentId: environment.id,
releaseId: release.id,
isForcedRelease: true,
})
.then(() => router.refresh())
}
>
Force deploy
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

export const ReleaseDropdownMenu: React.FC<{
release: {
id: string;
name: string;
};
environment: {
id: string;
name: string;
};
isReleaseCompleted: boolean;
}> = ({ release, environment, isReleaseCompleted }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<TbDotsVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<RedeployReleaseDialog release={release} environment={environment}>
<DropdownMenuItem
disabled={!isReleaseCompleted}
onSelect={(e) => e.preventDefault()}
className="space-x-2"
>
<TbReload />
<span>Redeploy</span>
</DropdownMenuItem>
</RedeployReleaseDialog>
<ForceReleaseDialog release={release} environment={environment}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="space-x-2"
>
<TbAlertTriangle />
<span>Force deploy</span>
</DropdownMenuItem>
</ForceReleaseDialog>
</DropdownMenuContent>
</DropdownMenu>
);
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const DeploymentTable: React.FC<{
<td
key={env.id}
className={cn(
"h-[55px] w-[200px] border-x border-b border-neutral-800 border-x-neutral-800/30 p-2 px-3",
"h-[55px] w-[220px] border-x border-b border-neutral-800 border-x-neutral-800/30 p-2 px-3",
envIdx === environments.length - 1 &&
"border-r-neutral-800",
idx === 0 && "border-t",
Expand Down
54 changes: 51 additions & 3 deletions packages/api/src/router/release.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import type { Tx } from "@ctrlplane/db";
import type { JobConfig } from "@ctrlplane/db/schema";
import _ from "lodash";
import { satisfies } from "semver";
import { isPresent } from "ts-is-present";
import { z } from "zod";

import { and, desc, eq, inArray, ne, takeFirst } from "@ctrlplane/db";
import {
and,
desc,
eq,
inArray,
ne,
notInArray,
takeFirst,
} from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import {
createRelease,
deployment,
environment,
environmentPolicy,
jobConfig,
release,
releaseDependency,
} from "@ctrlplane/db/schema";
import {
cancelOldJobConfigsOnJobDispatch,
createJobConfigs,
createJobExecutionApprovals,
createJobExecutions,
dispatchJobConfigs,
isPassingAllPolicies,
isPassingEnvironmentPolicy,
Expand Down Expand Up @@ -109,18 +121,54 @@ export const releaseRouter = createTRPCRouter({
{ type: "environment", id: input.environmentId },
),
})
.input(z.object({ environmentId: z.string(), releaseId: z.string() }))
.input(
z.object({
environmentId: z.string(),
releaseId: z.string(),
isForcedRelease: z.boolean().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
const cancelPreviousJobExecutions = async (
tx: Tx,
jobConfigs: JobConfig[],
) =>
tx
.select()
.from(jobConfig)
.where(
and(
eq(jobConfig.releaseId, input.releaseId),
eq(jobConfig.environmentId, input.environmentId),
notInArray(
jobConfig.id,
jobConfigs.map((j) => j.id),
),
),
)
.then((existingJobConfigs) =>
createJobExecutions(tx, existingJobConfigs, "cancelled").then(
() => {},
),
);

const jobConfigs = await createJobConfigs(ctx.db, "redeploy")
.causedById(ctx.session.user.id)
.environments([input.environmentId])
.releases([input.releaseId])
.filter(isPassingReleaseSequencingCancelPolicy)
.then(
input.isForcedRelease
? cancelPreviousJobExecutions
: createJobExecutionApprovals,
)
.insert();

await dispatchJobConfigs(ctx.db)
.jobConfigs(jobConfigs)
.filter(isPassingAllPolicies)
.filter(
input.isForcedRelease ? () => jobConfigs : isPassingAllPolicies,
)
.then(cancelOldJobConfigsOnJobDispatch)
.dispatch();

Expand Down
2 changes: 1 addition & 1 deletion packages/job-dispatch/src/job-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type FilterFunc = (
insertJobConfigs: JobConfigInsert[],
) => Promise<JobConfigInsert[]>;

type ThenFunc = (tx: Tx, jobConfigs: JobConfig[]) => Promise<void>;
type ThenFunc = (tx: Tx, jobConfigs: JobConfig[]) => Promise<void> | void;

export const createJobConfigs = (tx: Tx, type: JobConfigType) =>
new JobConfigBuilder(tx, type);
Expand Down
2 changes: 1 addition & 1 deletion packages/job-dispatch/src/job-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { dispatchJobExecutionsQueue } from "./queue.js";
export type DispatchFilterFunc = (
db: Tx,
jobConfigs: JobConfig[],
) => Promise<JobConfig[]>;
) => Promise<JobConfig[]> | JobConfig[];

type ThenFunc = (tx: Tx, jobConfigs: JobConfig[]) => Promise<void>;

Expand Down

0 comments on commit 5fe0aa7

Please sign in to comment.