Skip to content

Commit

Permalink
fix: Deploy release if it is after latest non-pending (#200)
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 authored Nov 4, 2024
1 parent f437ae9 commit 69408b2
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 9 deletions.
10 changes: 7 additions & 3 deletions packages/api/src/router/release-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
createJobApprovals,
createReleaseJobTriggers,
dispatchReleaseJobTriggers,
isPassingAllPolicies,
isPassingAllPoliciesExceptNewerThanLastActive,
isPassingLockingPolicy,
isPassingReleaseStringCheckPolicy,
} from "@ctrlplane/job-dispatch";
Expand Down Expand Up @@ -91,7 +91,9 @@ export const releaseDeployRouter = createTRPCRouter({
await dispatchReleaseJobTriggers(ctx.db)
.releaseTriggers(releaseJobTriggers)
.filter(
input.isForcedRelease ? isPassingLockingPolicy : isPassingAllPolicies,
input.isForcedRelease
? isPassingLockingPolicy
: isPassingAllPoliciesExceptNewerThanLastActive,
)
.then(cancelOldReleaseJobTriggersOnJobDispatch)
.dispatch();
Expand Down Expand Up @@ -160,7 +162,9 @@ export const releaseDeployRouter = createTRPCRouter({
await dispatchReleaseJobTriggers(ctx.db)
.releaseTriggers(releaseJobTriggers)
.filter(
input.isForcedRelease ? isPassingLockingPolicy : isPassingAllPolicies,
input.isForcedRelease
? isPassingLockingPolicy
: isPassingAllPoliciesExceptNewerThanLastActive,
)
.then(cancelOldReleaseJobTriggersOnJobDispatch)
.dispatch();
Expand Down
96 changes: 94 additions & 2 deletions packages/job-dispatch/src/policies/release-sequencing.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Tx } from "@ctrlplane/db";
import { isAfter } from "date-fns";
import _ from "lodash";
import { isPresent } from "ts-is-present";

import { and, eq, inArray, notExists, sql } from "@ctrlplane/db";
import { and, eq, inArray, ne, notExists, sql } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { activeStatus } from "@ctrlplane/validators/jobs";
import { activeStatus, JobStatus } from "@ctrlplane/validators/jobs";

import type { ReleaseIdPolicyChecker } from "./utils.js";

Expand Down Expand Up @@ -65,3 +68,92 @@ export const isPassingNoActiveJobsPolicy: ReleaseIdPolicyChecker = async (
.map((rjt) => rjt.release_job_trigger)
.value();
};

const latestActiveReleaseSubQuery = (db: Tx) =>
db
.select({
id: schema.release.id,
deploymentId: schema.release.deploymentId,
version: schema.release.version,
createdAt: schema.release.createdAt,
name: schema.release.name,
config: schema.release.config,
environmentId: schema.releaseJobTrigger.environmentId,
rank: sql<number>`ROW_NUMBER() OVER (PARTITION BY ${schema.release.deploymentId}, ${schema.releaseJobTrigger.environmentId} ORDER BY ${schema.release.createdAt} DESC)`.as(
"rank",
),
})
.from(schema.release)
.innerJoin(
schema.releaseJobTrigger,
eq(schema.releaseJobTrigger.releaseId, schema.release.id),
)
.innerJoin(schema.job, eq(schema.releaseJobTrigger.jobId, schema.job.id))
.where(ne(schema.job.status, JobStatus.Pending))
.as("active_releases");

/**
* This policy checks if the release is newer than the last release that was deployed for a deployment/environment.
* i.e. you can only dispatch a release if every release later than it is pending.
* @param db
* @param releaseJobTriggers
*/
export const isPassingNewerThanLastActiveReleasePolicy: ReleaseIdPolicyChecker =
async (db, releaseJobTriggers) => {
if (releaseJobTriggers.length === 0) return [];

const activeRelease = latestActiveReleaseSubQuery(db);

const releaseIds = releaseJobTriggers.map((rjt) => rjt.releaseId);
const releases = await db
.select()
.from(schema.release)
.where(inArray(schema.release.id, releaseIds));

const deploymentIds = _.uniq(releases.map((r) => r.deploymentId));
const deployments = await db
.select()
.from(schema.deployment)
.leftJoin(
activeRelease,
and(
eq(activeRelease.deploymentId, schema.deployment.id),
eq(activeRelease.rank, 1),
),
)
.where(inArray(schema.deployment.id, deploymentIds))
.then((rows) =>
_.chain(rows)
.groupBy((r) => r.deployment.id)
.map((r) => ({
...r[0]!.deployment,
activeReleases: r.map((r) => r.active_releases).filter(isPresent),
}))
.value(),
);

return _.chain(releaseJobTriggers)
.groupBy((rjt) => {
const release = releases.find((r) => r.id === rjt.releaseId);
if (!release) return null;
return [release.deploymentId, rjt.environmentId];
})
.filter(isPresent)
.map((triggers) => _.maxBy(triggers, (t) => t.createdAt)!)
.map((t) => {
const release = releases.find((r) => r.id === t.releaseId);
if (!release) return null;
const deployment = deployments.find(
(d) => d.id === release.deploymentId,
);
if (!deployment) return null;
const activeRelease = deployment.activeReleases.find(
(r) => r.environmentId === t.environmentId,
);
if (!activeRelease) return t;
if (release.id === activeRelease.id) return t;
return isAfter(release.createdAt, activeRelease.createdAt) ? t : null;
})
.filter(isPresent)
.value();
};
28 changes: 27 additions & 1 deletion packages/job-dispatch/src/policy-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,39 @@ import { isPassingConcurrencyPolicy } from "./policies/concurrency-policy.js";
import { isPassingJobRolloutPolicy } from "./policies/gradual-rollout.js";
import { isPassingApprovalPolicy } from "./policies/manual-approval.js";
import { isPassingReleaseDependencyPolicy } from "./policies/release-dependency.js";
import { isPassingNoActiveJobsPolicy } from "./policies/release-sequencing.js";
import {
isPassingNewerThanLastActiveReleasePolicy,
isPassingNoActiveJobsPolicy,
} from "./policies/release-sequencing.js";
import { isPassingReleaseWindowPolicy } from "./policies/release-window.js";
import { isPassingCriteriaPolicy } from "./policies/success-rate-criteria-passing.js";

export const isPassingAllPolicies = async (
db: Tx,
releaseJobTriggers: schema.ReleaseJobTrigger[],
) => {
if (releaseJobTriggers.length === 0) return [];
const checks: ReleaseIdPolicyChecker[] = [
isPassingLockingPolicy,
isPassingApprovalPolicy,
isPassingCriteriaPolicy,
isPassingConcurrencyPolicy,
isPassingReleaseDependencyPolicy,
isPassingJobRolloutPolicy,
isPassingNoActiveJobsPolicy,
isPassingNewerThanLastActiveReleasePolicy,
isPassingReleaseWindowPolicy,
];

let passingJobs = releaseJobTriggers;
for (const check of checks) passingJobs = await check(db, passingJobs);

return passingJobs;
};

export const isPassingAllPoliciesExceptNewerThanLastActive = async (
db: Tx,
releaseJobTriggers: schema.ReleaseJobTrigger[],
) => {
if (releaseJobTriggers.length === 0) return [];
const checks: ReleaseIdPolicyChecker[] = [
Expand Down
9 changes: 6 additions & 3 deletions packages/job-dispatch/src/release-sequencing.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Tx } from "@ctrlplane/db";

import { inArray, sql } from "@ctrlplane/db";
import { inArray, isNull, sql } from "@ctrlplane/db";
import * as schema from "@ctrlplane/db/schema";
import { JobStatus } from "@ctrlplane/validators/jobs";

Expand Down Expand Up @@ -40,7 +40,7 @@ export const cancelOldReleaseJobTriggersOnJobDispatch = async (
inner join ${schema.release} on ${schema.releaseJobTrigger.releaseId} = ${schema.release.id}
inner join ${schema.deployment} on ${schema.release.deploymentId} = ${schema.deployment.id}
inner join ${schema.environment} on ${schema.releaseJobTrigger.environmentId} = ${schema.environment.id}
inner join ${schema.environmentPolicy} on ${schema.environment.policyId} = ${schema.environmentPolicy.id}
left join ${schema.environmentPolicy} on ${schema.environment.policyId} = ${schema.environmentPolicy.id}
inner join (${triggersSubquery}) as triggers on
${schema.deployment.id} = triggers.cancelDeploymentId
and ${schema.releaseJobTrigger.environmentId} = triggers.cancelEnvironmentId
Expand All @@ -49,7 +49,10 @@ export const cancelOldReleaseJobTriggersOnJobDispatch = async (
schema.releaseJobTrigger.id,
releaseJobTriggers.map((t) => t.id),
)}
and ${schema.environmentPolicy.releaseSequencing} = ${schema.releaseSequencingType.enumValues.at(1)}
and (
${schema.environmentPolicy.releaseSequencing} = ${schema.releaseSequencingType.enumValues.at(1)}
or ${isNull(schema.environmentPolicy.releaseSequencing)}
)
`;

const jobsToCancel = await db
Expand Down

0 comments on commit 69408b2

Please sign in to comment.