From 5d6741d3df85d2c82136e31727891d83a32beed3 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 18 Jun 2025 14:05:08 -0700 Subject: [PATCH 1/3] fix: approvals should be scoped to an environment --- .../compute-systems-release-targets.ts | 4 +- .../release-cell/ApprovalRequiredCell.tsx | 3 +- .../DeploymentVersionEnvironmentCell.tsx | 2 +- .../deployment-version/ApprovalDialog.tsx | 245 +- .../DeploymentEnvironmentCell.tsx | 4 +- .../environment/[environmentId]/openapi.ts | 86 + .../environment/[environmentId]/route.ts | 115 + .../[deploymentVersionId]/approve/openapi.ts | 2 + .../[deploymentVersionId]/approve/route.ts | 26 +- .../environment/[environmentId]/openapi.ts | 86 + .../environment/[environmentId]/route.ts | 98 + .../[deploymentVersionId]/reject/route.ts | 23 +- e2e/api/schema.ts | 164 + .../api/policies/approval-policy.spec.ts | 230 + .../api/policies/approval-policy.spec.yaml | 10 +- openapi.v1.json | 213 + packages/api/src/router/deployment-version.ts | 38 +- packages/api/src/router/policy/evaluate.ts | 9 +- .../db/drizzle/0117_little_the_santerians.sql | 14 + packages/db/drizzle/meta/0117_snapshot.json | 6771 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/rules/approval-any.ts | 7 +- packages/db/src/schema/rules/approval-base.ts | 10 +- .../src/manager/version-manager-rules.ts | 11 +- .../environment-version-rollout.ts | 26 +- .../version-manager-rules/version-approval.ts | 40 +- .../src/rules/version-approval-rule.ts | 59 +- 27 files changed, 8189 insertions(+), 114 deletions(-) create mode 100644 apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/environment/[environmentId]/openapi.ts create mode 100644 apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/approve/environment/[environmentId]/route.ts create mode 100644 apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/reject/environment/[environmentId]/openapi.ts create mode 100644 apps/webservice/src/app/api/v1/deployment-versions/[deploymentVersionId]/reject/environment/[environmentId]/route.ts create mode 100644 packages/db/drizzle/0117_little_the_santerians.sql create mode 100644 packages/db/drizzle/meta/0117_snapshot.json diff --git a/apps/event-worker/src/workers/compute-systems-release-targets.ts b/apps/event-worker/src/workers/compute-systems-release-targets.ts index 7981a446b..8a8974c70 100644 --- a/apps/event-worker/src/workers/compute-systems-release-targets.ts +++ b/apps/event-worker/src/workers/compute-systems-release-targets.ts @@ -11,9 +11,7 @@ import { } from "@ctrlplane/events"; import { logger } from "@ctrlplane/logger"; -const log = logger.child({ - component: "computeSystemsReleaseTargetsWorker", -}); +const log = logger.child({ component: "computeSystemsReleaseTargetsWorker" }); const findMatchingEnvironmentDeploymentPairs = ( tx: Tx, diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx index 3af379883..7789890cd 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(deploy)/(raw)/systems/[systemSlug]/(raw)/deployments/[deploymentSlug]/(sidebar)/_components/release-cell/ApprovalRequiredCell.tsx @@ -74,7 +74,7 @@ export const ApprovalRequiredCell: React.FC<{ deploymentVersion: { id: string; tag: string }; deployment: { id: string; name: string; slug: string }; environment: { id: string; name: string }; - system: { slug: string }; + system: { id: string; slug: string }; }> = ({ policies, deploymentVersion, deployment, environment, system }) => { const { workspaceSlug } = useParams<{ workspaceSlug: string }>(); @@ -143,6 +143,7 @@ export const ApprovalRequiredCell: React.FC<{ void; + onRemove: (environmentId: string) => void; +}> = ({ allEnvironments, selectedEnvironmentIds, onSelect, onRemove }) => { + const unselectedEnvironments = allEnvironments.filter( + (environment) => !selectedEnvironmentIds.includes(environment.id), + ); + + const selectedEnvironments = allEnvironments.filter((environment) => + selectedEnvironmentIds.includes(environment.id), + ); + + return ( +
+
+ +

+ Select the environments to approve the release for. +

+
+ +
+ {selectedEnvironments.map((environment) => ( + + {environment.name} + + + ))} +
+ + + + + + + + + + onSelect(allEnvironments.map((e) => e.id))} + > + All environments + + {unselectedEnvironments.map((environment) => ( + onSelect([environment.id])} + > + {environment.name} + + ))} + + + + +
+ ); +}; + +const ApprovalDialogControl: React.FC<{ versionId: string; - versionTag: string; + environments: schema.Environment[]; environmentId: string; - children: React.ReactNode; - onSubmit?: () => void; -}> = ({ versionId, versionTag, environmentId, children, onSubmit }) => { - const [open, setOpen] = useState(false); - const addRecord = api.deployment.version.addApprovalRecord.useMutation(); + onSubmit: () => void; + onCancel: () => void; +}> = ({ versionId, environments, environmentId, onSubmit, onCancel }) => { + const [environmentIds, setEnvironmentIds] = useState([ + environmentId, + ]); const router = useRouter(); - const [reason, setReason] = useState(""); + const addRecord = api.deployment.version.addApprovalRecord.useMutation(); const handleSubmit = (status: SCHEMA.ApprovalStatus) => addRecord .mutateAsync({ deploymentVersionId: versionId, - environmentId, + environmentIds, status, reason, }) - .then(() => setOpen(false)) - .then(() => onSubmit?.()) + .then(() => onSubmit()) .then(() => router.refresh()); + const setEnvironmentSelected = (environmentIds: string[]) => + setEnvironmentIds((prev) => [...prev, ...environmentIds]); + + const setEnvironmentUnselected = (environmentId: string) => + setEnvironmentIds((prev) => prev.filter((id) => id !== environmentId)); + + return ( +
+ + +
+
+ +

+ Provide a reason for the approval or rejection (optional). +

+
+