Skip to content

Commit

Permalink
fix: Clean up release sequencing (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Oct 14, 2024
1 parent a28593d commit 9cc03ba
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 237 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const ReleaseManagement: React.FC<{
<RadioGroupItem value="wait" />
</FormControl>
<FormLabel className="flex items-center gap-2 font-normal">
Pause deployment until active releases are completed
Keep pending releases
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ export const createEdgesWhereEnvironmentHasNoPolicy = (
};
});

export const createEdgesFromPolicyToReleaseSequencing = (
envs: Array<{ id: string; policyId?: string | null }>,
) =>
envs.map((e) => ({
id: `${e.policyId ?? "trigger"}-release-sequencing-${e.id}`,
source: e.policyId ?? "trigger",
target: `${e.id}-release-sequencing`,
markerEnd,
}));

export const createEdgesFromReleaseSequencingToEnvironment = (
envs: Array<{ id: string; policyId?: string | null }>,
) =>
envs.map((e) => ({
id: `${e.id}-release-sequencing-${e.id}`,
source: `${e.id}-release-sequencing`,
target: e.id,
markerEnd,
}));

export const createEdgesFromPolicyDeployment = (
policyDeployments: Array<EnvironmentPolicyDeployment>,
) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ import ReactFlow, {
import { ArrowEdge } from "~/app/[workspaceSlug]/_components/reactflow/ArrowEdge";
import {
createEdgesFromPolicyDeployment,
createEdgesWhereEnvironmentHasNoPolicy,
createEdgesFromPolicyToReleaseSequencing,
createEdgesFromReleaseSequencingToEnvironment,
createEdgesWherePolicyHasNoEnvironment,
} from "~/app/[workspaceSlug]/_components/reactflow/edges";
import { getLayoutedElementsDagre } from "~/app/[workspaceSlug]/_components/reactflow/layout";
import { EnvironmentNode } from "./FlowNode";
import { PolicyNode } from "./FlowPolicyNode";
import { ReleaseSequencingNode } from "./ReleaseSequencingNode";

const nodeTypes: NodeTypes = {
environment: EnvironmentNode,
policy: PolicyNode,
"release-sequencing": ReleaseSequencingNode,
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export const FlowDiagram: React.FC<{
Expand Down Expand Up @@ -59,10 +62,26 @@ export const FlowDiagram: React.FC<{
release,
},
})),
...envs.map((env) => {
const policy = policies.find((p) => p.id === env.policyId);
return {
id: env.id + "-release-sequencing",
type: "release-sequencing",
position: { x: 0, y: 0 },
data: {
releaseId: release.id,
deploymentId: release.deploymentId,
environmentId: env.id,
policyType: policy?.releaseSequencing,
label: `${env.name} - release sequencing`,
},
};
}),
]);

const [edges, setEdges, onEdgesChange] = useEdgesState([
...createEdgesWhereEnvironmentHasNoPolicy(envs),
...createEdgesFromPolicyToReleaseSequencing(envs),
...createEdgesFromReleaseSequencingToEnvironment(envs),
...createEdgesWherePolicyHasNoEnvironment(policies, policyDeployments),
...createEdgesFromPolicyDeployment(policyDeployments),
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { NodeProps } from "reactflow";
import { IconCheck, IconLoader2 } from "@tabler/icons-react";
import { Handle, Position } from "reactflow";
import colors from "tailwindcss/colors";

import { cn } from "@ctrlplane/ui";
import { JobStatus } from "@ctrlplane/validators/jobs";

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

type ReleaseSequencingNodeProps = NodeProps<{
policyType?: "cancel" | "wait";
releaseId: string;
deploymentId: string;
environmentId: string;
}>;

const Passing: React.FC = () => (
<div className="rounded-full bg-green-400 p-0.5 dark:text-black">
<IconCheck strokeWidth={3} className="h-3 w-3" />
</div>
);

const Waiting: React.FC = () => (
<div className="animate-spin rounded-full bg-blue-400 p-0.5 dark:text-black">
<IconLoader2 strokeWidth={3} className="h-3 w-3" />
</div>
);

const Loading: React.FC = () => (
<div className="animate-spin rounded-full bg-muted-foreground p-0.5 dark:text-black">
<IconLoader2 strokeWidth={3} className="h-3 w-3" />
</div>
);

const WaitingOnActiveCheck: React.FC<ReleaseSequencingNodeProps["data"]> = ({
releaseId,
deploymentId,
environmentId,
}) => {
const allJobs = api.job.config.byDeploymentId.useQuery(deploymentId, {
refetchInterval: 10_000,
});
const isReleasePending = allJobs.data?.some(
(j) =>
j.job.status === JobStatus.Pending &&
j.release.id === releaseId &&
j.environmentId === environmentId,
);
const isWaitingOnActive =
isReleasePending &&
allJobs.data?.some(
(j) =>
j.job.status === JobStatus.InProgress &&
j.release.id !== releaseId &&
j.environmentId === environmentId,
);

return (
<div className="flex items-center gap-2">
{allJobs.isLoading && <Loading />}
{isWaitingOnActive && (
<>
<Waiting /> Another release is in progress
</>
)}
{!isWaitingOnActive && !allJobs.isLoading && (
<>
<Passing /> All other releases finished
</>
)}
</div>
);
};

export const ReleaseSequencingNode: React.FC<ReleaseSequencingNodeProps> = ({
data,
}) => {
return (
<>
<div
className={cn(
"relative w-[250px] space-y-1 rounded-md border px-2 py-1.5 text-sm",
)}
>
<WaitingOnActiveCheck {...data} />
</div>
<Handle
type="target"
className="h-2 w-2 rounded-full border border-neutral-500"
style={{ background: colors.neutral[800] }}
position={Position.Left}
/>
<Handle
type="source"
className="h-2 w-2 rounded-full border border-neutral-500"
style={{ background: colors.neutral[800] }}
position={Position.Right}
/>
</>
);
};
2 changes: 0 additions & 2 deletions apps/webservice/src/app/api/v1/releases/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
createReleaseJobTriggers,
dispatchReleaseJobTriggers,
isPassingAllPolicies,
isPassingReleaseSequencingCancelPolicy,
isPassingReleaseStringCheckPolicy,
} from "@ctrlplane/job-dispatch";
import { logger } from "@ctrlplane/logger";
Expand Down Expand Up @@ -61,7 +60,6 @@ export const POST = async (req: NextRequest) => {
createReleaseJobTriggers(db, "new_release")
.causedById(user.id)
.filter(isPassingReleaseStringCheckPolicy)
.filter(isPassingReleaseSequencingCancelPolicy)
.releases([release.id])
.then(createJobApprovals)
.insert()
Expand Down
12 changes: 0 additions & 12 deletions packages/api/src/router/release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
dispatchReleaseJobTriggers,
isPassingAllPolicies,
isPassingLockingPolicy,
isPassingReleaseSequencingCancelPolicy,
isPassingReleaseStringCheckPolicy,
} from "@ctrlplane/job-dispatch";
import { Permission } from "@ctrlplane/validators/auth";
Expand Down Expand Up @@ -225,11 +224,6 @@ export const releaseRouter = createTRPCRouter({
.causedById(ctx.session.user.id)
.environments([input.environmentId])
.releases([input.releaseId])
.filter(
input.isForcedRelease
? (_, releaseJobTriggers) => releaseJobTriggers
: isPassingReleaseSequencingCancelPolicy,
)
.filter(
input.isForcedRelease
? (_, releaseJobTriggers) => releaseJobTriggers
Expand Down Expand Up @@ -313,11 +307,6 @@ export const releaseRouter = createTRPCRouter({
.environments([env.id])
.releases([rel.id])
.targets([t.id])
.filter(
input.isForcedRelease
? (_tx, releaseJobTriggers) => releaseJobTriggers
: isPassingReleaseSequencingCancelPolicy,
)
.filter(
input.isForcedRelease
? (_, releaseJobTriggers) => releaseJobTriggers
Expand Down Expand Up @@ -369,7 +358,6 @@ export const releaseRouter = createTRPCRouter({
.causedById(ctx.session.user.id)
.filter(isPassingReleaseStringCheckPolicy)
.releases([rel.id])
.filter(isPassingReleaseSequencingCancelPolicy)
.then(createJobApprovals)
.insert();

Expand Down
47 changes: 19 additions & 28 deletions packages/job-dispatch/src/job-creation.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
import type { Tx } from "@ctrlplane/db";
import _ from "lodash";

import { and, eq, isNotNull, or, takeFirst } from "@ctrlplane/db";
import { and, eq, isNotNull, ne, or, takeFirst } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import * as schema from "@ctrlplane/db/schema";
// import {
// deployment,
// environment,
// environmentPolicy,
// environmentPolicyDeployment,
// job,
// jobAgent,
// release,
// releaseDependency,
// releaseJobTrigger,
// } from "@ctrlplane/db/schema";
/**
* Converts a job config into a job which means they can now be
* picked up by job agents
*/
import { logger } from "@ctrlplane/logger";
import { JobStatus } from "@ctrlplane/validators/jobs";

Expand Down Expand Up @@ -96,10 +81,6 @@ export const onJobCompletion = async (je: schema.Job) => {
schema.release,
eq(schema.releaseJobTrigger.releaseId, schema.release.id),
)
.innerJoin(
schema.environment,
eq(schema.releaseJobTrigger.environmentId, schema.environment.id),
)
.innerJoin(
schema.deployment,
eq(schema.release.deploymentId, schema.deployment.id),
Expand All @@ -111,28 +92,38 @@ export const onJobCompletion = async (je: schema.Job) => {
eq(schema.releaseJobTrigger.releaseId, triggers.release.id),
eq(
schema.environmentPolicyDeployment.environmentId,
triggers.environment.id,
triggers.release_job_trigger.environmentId,
),
);

const isWaitingOnPreviousReleasesInSameEnvironment = and(
eq(schema.environmentPolicy.releaseSequencing, "wait"),
eq(schema.environment.id, triggers.environment.id),
);

const isWaitingOnConcurrencyRequirementInSameRelease = and(
eq(schema.environmentPolicy.concurrencyType, "some"),
eq(schema.environment.id, triggers.environment.id),
eq(schema.environment.id, triggers.release_job_trigger.environmentId),
eq(schema.releaseJobTrigger.releaseId, triggers.release.id),
eq(schema.job.status, JobStatus.Pending),
);

const isDependentOnVersionOfTriggerDeployment = isNotNull(
schema.releaseDependency.id,
);

const isWaitingOnJobToFinish = and(
eq(schema.environment.id, triggers.release_job_trigger.environmentId),
eq(schema.deployment.id, triggers.deployment.id),
ne(schema.release.id, triggers.release.id),
);

const affectedReleaseJobTriggers = await db
.select()
.from(schema.releaseJobTrigger)
.innerJoin(
schema.release,
eq(schema.releaseJobTrigger.releaseId, schema.release.id),
)
.innerJoin(
schema.deployment,
eq(schema.release.deploymentId, schema.deployment.id),
)
.innerJoin(schema.job, eq(schema.releaseJobTrigger.jobId, schema.job.id))
.innerJoin(
schema.environment,
Expand Down Expand Up @@ -164,7 +155,7 @@ export const onJobCompletion = async (je: schema.Job) => {
eq(schema.job.status, JobStatus.Pending),
or(
isDependentOnTriggerForCriteria,
isWaitingOnPreviousReleasesInSameEnvironment,
isWaitingOnJobToFinish,
isWaitingOnConcurrencyRequirementInSameRelease,
isDependentOnVersionOfTriggerDeployment,
),
Expand Down
Loading

0 comments on commit 9cc03ba

Please sign in to comment.