@@ -287,6 +301,14 @@ export const PolicyEvaluationsCell: React.FC<{
)}
+ {blockingVersionDependencies.length > 0 && (
+
+ )}
+
{blockingReleaseTargetJob != null && (
)}
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/ReleaseTargetRow.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/ReleaseTargetRow.tsx
index 468cc86e0..8c6bb4601 100644
--- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/ReleaseTargetRow.tsx
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/ReleaseTargetRow.tsx
@@ -173,7 +173,7 @@ export const ReleaseTargetRow: React.FC<{
resource: { id: string; name: string };
environment: { id: string; name: string };
deployment: { id: string; name: string };
- version: { id: string };
+ version: { id: string; tag: string };
jobs: Array<{
id: string;
status: SCHEMA.JobStatus;
@@ -222,7 +222,8 @@ export const ReleaseTargetRow: React.FC<{
diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/policy-evaluations/VersionDependencyBadge.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/policy-evaluations/VersionDependencyBadge.tsx
new file mode 100644
index 000000000..51ec74178
--- /dev/null
+++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(raw)/releases/[releaseId]/(sidebar)/jobs/_components/policy-evaluations/VersionDependencyBadge.tsx
@@ -0,0 +1,171 @@
+import type * as schema from "@ctrlplane/db/schema";
+import React from "react";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+import { IconSitemapFilled } from "@tabler/icons-react";
+
+import { Badge } from "@ctrlplane/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@ctrlplane/ui/dialog";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@ctrlplane/ui/table";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@ctrlplane/ui/tooltip";
+
+import { DeploymentVersionConditionBadge } from "~/app/[workspaceSlug]/(app)/_components/deployments/version/condition/DeploymentVersionConditionBadge";
+import { ResourceIcon } from "~/app/[workspaceSlug]/(app)/_components/resources/ResourceIcon";
+import { urls } from "~/app/urls";
+
+type Dependency = schema.VersionDependency & {
+ resourcesForDependency: schema.Resource[];
+ deployment: schema.Deployment;
+};
+
+const SingleResourceCell: React.FC<{
+ resource: schema.Resource;
+}> = ({ resource }) => {
+ const { workspaceSlug } = useParams<{ workspaceSlug: string }>();
+ const resourceUrl = urls
+ .workspace(workspaceSlug)
+ .resource(resource.id)
+ .deployments();
+
+ return (
+
+
+
+ {resource.name}
+
+
+ );
+};
+
+const ResourceCell: React.FC<{
+ resources: schema.Resource[];
+}> = ({ resources }) => {
+ const { workspaceSlug } = useParams<{ workspaceSlug: string }>();
+
+ if (resources.length === 0)
+ return (
+
+ No resources
+
+ );
+
+ if (resources.length === 1)
+ return
;
+
+ return (
+
+
+
+
+
+ {resources.length} resources
+
+
+
+ {resources.map((resource) => (
+
+
+ {resource.name}
+
+ ))}
+
+
+
+
+ );
+};
+
+const DependencyRow: React.FC<{
+ dependency: Dependency;
+}> = ({ dependency }) => (
+
+ {dependency.deployment.name}
+
+ {dependency.versionSelector != null && (
+
+ )}
+ {dependency.versionSelector == null && (
+ No version selector
+ )}
+
+
+
+);
+
+export const VersionDependencyBadge: React.FC<{
+ resource: { id: string; name: string };
+ version: { tag: string };
+ dependencyResults: Dependency[];
+}> = ({ resource, version, dependencyResults }) => (
+
+);
diff --git a/packages/api/src/router/policy/evaluate.ts b/packages/api/src/router/policy/evaluate.ts
index 69accc8a2..4714bf9f6 100644
--- a/packages/api/src/router/policy/evaluate.ts
+++ b/packages/api/src/router/policy/evaluate.ts
@@ -4,11 +4,13 @@ import { TRPCError } from "@trpc/server";
import { isPresent } from "ts-is-present";
import { z } from "zod";
-import { and, eq, selector } from "@ctrlplane/db";
+import { and, eq, inArray, selector, takeFirst } from "@ctrlplane/db";
+import { getResourceParents } from "@ctrlplane/db/queries";
import * as schema from "@ctrlplane/db/schema";
import {
getConcurrencyRule,
getRolloutInfoForReleaseTarget,
+ getVersionDependencyRule,
mergePolicies,
ReleaseTargetConcurrencyRule,
versionAnyApprovalRule,
@@ -63,12 +65,14 @@ const getFilterReasons = async (
policies: Policy[],
version: Version[],
versionId: string,
- ruleGetter: (policy: Policy) => Array
>,
+ ruleGetter: (
+ policy: Policy,
+ ) => Array> | Promise>>,
) => {
return Object.fromEntries(
await Promise.all(
policies.map(async (policy) => {
- const rules = ruleGetter(policy);
+ const rules = await ruleGetter(policy);
const rejectionReasons = await Promise.all(
rules.map(async (rule) => {
const result = await rule.filter(version);
@@ -106,6 +110,71 @@ const getConcurrencyBlocked = async (
),
);
+const getResourceFromReleaseTarget = async (db: Tx, releaseTargetId: string) =>
+ db
+ .select()
+ .from(schema.releaseTarget)
+ .innerJoin(
+ schema.resource,
+ eq(schema.releaseTarget.resourceId, schema.resource.id),
+ )
+ .where(eq(schema.releaseTarget.id, releaseTargetId))
+ .then(takeFirst)
+ .then((r) => r.resource);
+
+const getVersionDependencyInfo = async (
+ db: Tx,
+ releaseTargetId: string,
+ dependency: schema.VersionDependency,
+) => {
+ const deployment = await db
+ .select()
+ .from(schema.deployment)
+ .where(eq(schema.deployment.id, dependency.deploymentId))
+ .then(takeFirst);
+
+ const resource = await getResourceFromReleaseTarget(db, releaseTargetId);
+ const { relationships } = await getResourceParents(db, resource.id);
+ const parentResourceIds = Object.values(relationships).map(
+ ({ source }) => source.id,
+ );
+ const parentResources =
+ parentResourceIds.length > 0
+ ? await db
+ .select()
+ .from(schema.resource)
+ .where(inArray(schema.resource.id, parentResourceIds))
+ : [];
+
+ const resourcesForDependency: schema.Resource[] = [
+ resource,
+ ...parentResources,
+ ];
+ return { resourcesForDependency, deployment };
+};
+
+const getVersionDependency = async (
+ db: Tx,
+ releaseTargetId: string,
+ version: schema.DeploymentVersion,
+) => {
+ const rule = await getVersionDependencyRule(releaseTargetId);
+ const result = await rule.filter([version]);
+ const dependencyResult = result.dependencyResults[version.id];
+ if (dependencyResult == null) return [];
+
+ const allDependenciesPromise = dependencyResult.map(
+ async (dependencyResult) => {
+ const { isSatisfied, dependency } = dependencyResult;
+ const { resourcesForDependency, deployment } =
+ await getVersionDependencyInfo(db, releaseTargetId, dependency);
+ return { ...dependency, isSatisfied, resourcesForDependency, deployment };
+ },
+ );
+
+ return Promise.all(allDependenciesPromise);
+};
+
export const evaluateEnvironment = protectedProcedure
.input(
z.object({
@@ -287,6 +356,12 @@ export const evaluateReleaseTarget = protectedProcedure
version,
);
+ const versionDependency = await getVersionDependency(
+ ctx.db,
+ releaseTargetId,
+ version,
+ );
+
return {
policies,
rules: {
@@ -311,6 +386,7 @@ export const evaluateReleaseTarget = protectedProcedure
),
),
),
+ versionDependency,
},
};
});
diff --git a/packages/rule-engine/src/manager/version-manager-rules/index.ts b/packages/rule-engine/src/manager/version-manager-rules/index.ts
index b12d5be35..15aab7d11 100644
--- a/packages/rule-engine/src/manager/version-manager-rules/index.ts
+++ b/packages/rule-engine/src/manager/version-manager-rules/index.ts
@@ -2,3 +2,4 @@ export * from "./environment-version-rollout.js";
export * from "./version-approval.js";
export * from "./concurrency.js";
export * from "./release-target-lock-rule.js";
+export * from "./version-dependency.js";