From 4e83d9f1de5cb9885a96584f0bcecfd1d8c1a13f Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 6 May 2026 13:31:14 -0700 Subject: [PATCH 1/2] chore: drop min success default --- apps/api/openapi/openapi.json | 1 - apps/api/openapi/schemas/policies.jsonnet | 2 +- apps/api/src/types/openapi.ts | 7 ++----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 28555773e..56f9d7345 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -1037,7 +1037,6 @@ "type": "integer" }, "minimumSuccessPercentage": { - "default": 100, "format": "float", "maximum": 100, "minimum": 0, diff --git a/apps/api/openapi/schemas/policies.jsonnet b/apps/api/openapi/schemas/policies.jsonnet index 576467699..5c42a208c 100644 --- a/apps/api/openapi/schemas/policies.jsonnet +++ b/apps/api/openapi/schemas/policies.jsonnet @@ -184,7 +184,7 @@ local openapi = import '../lib/openapi.libsonnet'; description: 'CEL expression to match the environment(s) that must have a successful release before this environment can proceed.', }, - minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, + minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, minimumSoakTimeMinutes: { diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 16638a299..da9141c84 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -1493,11 +1493,8 @@ export interface components { * @description Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed. Defaults to 0 if not provided. */ minimumSoakTimeMinutes?: number; - /** - * Format: float - * @default 100 - */ - minimumSuccessPercentage: number; + /** Format: float */ + minimumSuccessPercentage?: number; /** * @description If true, jobs must also have passed verification to count toward the success percentage * @default false From dcc3056cc73d265212c8fa7b18b9c1897d363029 Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Wed, 6 May 2026 14:03:16 -0700 Subject: [PATCH 2/2] clenaup --- apps/api/src/routes/v1/workspaces/policies.ts | 4 +- apps/web/app/api/openapi.ts | 401 +++++++++++++++++- apps/workspace-engine/oapi/openapi.json | 1 - .../oapi/spec/schemas/policy.jsonnet | 2 +- e2e/api/schema.ts | 7 +- e2e/tests/api/policies/policies.spec.ts | 41 ++ packages/workspace-engine-sdk/src/schema.ts | 7 +- 7 files changed, 443 insertions(+), 20 deletions(-) diff --git a/apps/api/src/routes/v1/workspaces/policies.ts b/apps/api/src/routes/v1/workspaces/policies.ts index b77068c1b..7f495b37f 100644 --- a/apps/api/src/routes/v1/workspaces/policies.ts +++ b/apps/api/src/routes/v1/workspaces/policies.ts @@ -207,7 +207,9 @@ const formatPolicy = (p: PolicyRow) => { maximumAgeHours: r.maximumAgeHours, }), minimumSoakTimeMinutes: r.minimumSoakTimeMinutes, - minimumSuccessPercentage: r.minimumSuccessPercentage, + ...(r.minimumSuccessPercentage != null && { + minimumSuccessPercentage: r.minimumSuccessPercentage, + }), ...(r.successStatuses != null && { successStatuses: r.successStatuses, }), diff --git a/apps/web/app/api/openapi.ts b/apps/web/app/api/openapi.ts index dabd7e9ac..da9141c84 100644 --- a/apps/web/app/api/openapi.ts +++ b/apps/web/app/api/openapi.ts @@ -138,6 +138,47 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/workspaces/{workspaceId}/deployment-versions/{deploymentVersionId}/dependencies": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List deployment-version dependencies + * @description Returns the dependency edges declared by this deployment version. + */ + get: operations["listDeploymentVersionDependencies"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/workspaces/{workspaceId}/deployment-versions/{deploymentVersionId}/dependencies/{dependencyDeploymentId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Upsert deployment-version dependency + * @description Declare or update a version-selector dependency from this deployment version to another deployment. Identified by the (deploymentVersionId, dependencyDeploymentId) pair. + */ + put: operations["requestDeploymentVersionDependencyUpsert"]; + post?: never; + /** Delete deployment-version dependency */ + delete: operations["requestDeploymentVersionDependencyDeletion"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/workspaces/{workspaceId}/deployment-versions/{deploymentVersionId}/user-approval-records": { parameters: { query?: never; @@ -173,6 +214,23 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/workspaces/{workspaceId}/deployments/name/{name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get deployment by name */ + get: operations["getDeploymentByName"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/workspaces/{workspaceId}/deployments/{deploymentId}": { parameters: { query?: never; @@ -305,6 +363,23 @@ export interface paths { patch?: never; trace?: never; }; + "/v1/workspaces/{workspaceId}/environments/name/{name}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get environment by name */ + get: operations["getEnvironmentByName"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/v1/workspaces/{workspaceId}/environments/{environmentId}": { parameters: { query?: never; @@ -1079,6 +1154,13 @@ export interface components { }; /** Format: date-time */ createdAt?: string; + /** @description Map of dependency deployment ID to a CEL version selector evaluated against that deployment's current release on the same resource. Inserted atomically with the version so reconciliation cannot fire before edges are attached. */ + dependencies?: { + [key: string]: { + /** @description CEL expression evaluated against the dependency deployment's current release version on the same resource. */ + versionSelector: string; + }; + }; jobAgentConfig?: { [key: string]: unknown; }; @@ -1117,6 +1199,7 @@ export interface components { deploymentWindow?: components["schemas"]["DeploymentWindowRule"]; environmentProgression?: components["schemas"]["EnvironmentProgressionRule"]; gradualRollout?: components["schemas"]["GradualRolloutRule"]; + planValidationOpa?: components["schemas"]["PlanValidationOpaRule"]; retry?: components["schemas"]["RetryRule"]; verification?: components["schemas"]["VerificationRule"]; versionCooldown?: components["schemas"]["VersionCooldownRule"]; @@ -1341,6 +1424,12 @@ export interface components { status: components["schemas"]["DeploymentVersionStatus"]; tag: string; }; + DeploymentVersionDependency: { + dependencyDeploymentId: string; + deploymentVersionId: string; + /** @description CEL expression evaluated against the dependency deployment's current release version on the same resource. */ + versionSelector: string; + }; /** @enum {string} */ DeploymentVersionStatus: "unspecified" | "building" | "ready" | "failed" | "rejected"; DeploymentWindowRule: { @@ -1404,11 +1493,8 @@ export interface components { * @description Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed. Defaults to 0 if not provided. */ minimumSoakTimeMinutes?: number; - /** - * Format: float - * @default 100 - */ - minimumSuccessPercentage: number; + /** Format: float */ + minimumSuccessPercentage?: number; /** * @description If true, jobs must also have passed verification to count toward the success percentage * @default false @@ -1583,6 +1669,13 @@ export interface components { [key: string]: unknown; }; }; + PlanValidationOpaRule: { + description?: string; + /** @description Human-readable rule name; used in check output to identify which rule produced a violation. */ + name: string; + /** @description Rego v1 source code. Must define a `deny` rule set following the Conftest convention (deny contains msg if { ... }). */ + rego: string; + }; Policy: { createdAt: string; description?: string; @@ -1607,6 +1700,7 @@ export interface components { environmentProgression?: components["schemas"]["EnvironmentProgressionRule"]; gradualRollout?: components["schemas"]["GradualRolloutRule"]; id: string; + planValidationOpa?: components["schemas"]["PlanValidationOpaRule"]; policyId: string; retry?: components["schemas"]["RetryRule"]; verification?: components["schemas"]["VerificationRule"]; @@ -1985,6 +2079,10 @@ export interface components { resourceSelector?: string; value: components["schemas"]["Value"]; }; + UpsertDeploymentVersionDependencyRequest: { + /** @description CEL expression evaluated against the dependency deployment's current release version on the same resource. */ + versionSelector: string; + }; UpsertDeploymentVersionRequest: { config?: { [key: string]: unknown; @@ -2042,6 +2140,7 @@ export interface components { environmentProgression?: components["schemas"]["EnvironmentProgressionRule"]; gradualRollout?: components["schemas"]["GradualRolloutRule"]; id?: string; + planValidationOpa?: components["schemas"]["PlanValidationOpaRule"]; policyId?: string; retry?: components["schemas"]["RetryRule"]; verification?: components["schemas"]["VerificationRule"]; @@ -2908,6 +3007,134 @@ export interface operations { }; }; }; + listDeploymentVersionDependencies: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the deployment version */ + deploymentVersionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeploymentVersionDependency"][]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + requestDeploymentVersionDependencyUpsert: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the deployment version */ + deploymentVersionId: string; + /** @description ID of the dependency deployment */ + dependencyDeploymentId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpsertDeploymentVersionDependencyRequest"]; + }; + }; + responses: { + /** @description Accepted response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeploymentRequestAccepted"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + requestDeploymentVersionDependencyDeletion: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description ID of the deployment version */ + deploymentVersionId: string; + /** @description ID of the dependency deployment */ + dependencyDeploymentId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Accepted response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeploymentRequestAccepted"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; requestUserApprovalRecordUpsert: { parameters: { query?: never; @@ -3018,6 +3245,67 @@ export interface operations { "application/json": components["schemas"]["DeploymentRequestAccepted"]; }; }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Deployment name already exists in this workspace */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getDeploymentByName: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description Name of the deployment */ + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeploymentWithVariablesAndSystems"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; }; getDeployment: { @@ -3090,6 +3378,24 @@ export interface operations { "application/json": components["schemas"]["DeploymentRequestAccepted"]; }; }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Deployment name already exists in this workspace */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; }; requestDeploymentDeletion: { @@ -3283,6 +3589,8 @@ export interface operations { offset?: number; /** @description Sort order for results */ order?: "asc" | "desc"; + /** @description CEL expression to filter the results */ + cel?: string; }; header?: never; path: { @@ -3350,8 +3658,8 @@ export interface operations { }; }; responses: { - /** @description Accepted response */ - 202: { + /** @description Deployment version created */ + 200: { headers: { [name: string]: unknown; }; @@ -3368,6 +3676,15 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; }; requestDeploymentVersionUpdate: { @@ -3469,6 +3786,67 @@ export interface operations { "application/json": components["schemas"]["EnvironmentRequestAccepted"]; }; }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Environment name already exists in this workspace */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getEnvironmentByName: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description Name of the environment */ + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EnvironmentWithSystems"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; }; getEnvironment: { @@ -3559,6 +3937,15 @@ export interface operations { "application/json": components["schemas"]["ErrorResponse"]; }; }; + /** @description Environment name already exists in this workspace */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; }; }; requestEnvironmentDeletion: { diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 2edb0fc1b..da8046c08 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -612,7 +612,6 @@ "type": "integer" }, "minimumSuccessPercentage": { - "default": 100, "format": "float", "maximum": 100, "minimum": 0, diff --git a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet index 79360b04a..45a8a4c84 100644 --- a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet @@ -147,7 +147,7 @@ local openapi = import '../lib/openapi.libsonnet'; properties: { dependsOnEnvironmentSelector: { type: 'string', description: 'CEL expression to determine if the environment progression rule should be used' }, - minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, + minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, minimumSoakTimeMinutes: { diff --git a/e2e/api/schema.ts b/e2e/api/schema.ts index 16638a299..da9141c84 100644 --- a/e2e/api/schema.ts +++ b/e2e/api/schema.ts @@ -1493,11 +1493,8 @@ export interface components { * @description Minimum time to wait after the depends on environment is in a success state before the current environment can be deployed. Defaults to 0 if not provided. */ minimumSoakTimeMinutes?: number; - /** - * Format: float - * @default 100 - */ - minimumSuccessPercentage: number; + /** Format: float */ + minimumSuccessPercentage?: number; /** * @description If true, jobs must also have passed verification to count toward the success percentage * @default false diff --git a/e2e/tests/api/policies/policies.spec.ts b/e2e/tests/api/policies/policies.spec.ts index bb8f8e88f..1e21b0f3b 100644 --- a/e2e/tests/api/policies/policies.spec.ts +++ b/e2e/tests/api/policies/policies.spec.ts @@ -936,6 +936,47 @@ deny contains msg if { }); }); + test("should create a policy with environmentProgression rule omitting minimumSuccessPercentage", async ({ + api, + workspace, + }) => { + const name = `policy-envprog-nodefault-${faker.string.alphanumeric(8)}`; + const createRes = await api.POST("/v1/workspaces/{workspaceId}/policies", { + params: { path: { workspaceId: workspace.id } }, + body: { + name, + rules: [ + { + environmentProgression: { + dependsOnEnvironmentSelector: 'environment.name == "staging"', + }, + }, + ], + }, + }); + + expect(createRes.response.status).toBe(202); + const policyId = createRes.data!.id; + const created = createRes.data!.rules[0]!.environmentProgression!; + expect(created.dependsOnEnvironmentSelector).toBe( + 'environment.name == "staging"', + ); + expect(created.minimumSuccessPercentage).toBeUndefined(); + + const getRes = await api.GET( + "/v1/workspaces/{workspaceId}/policies/{policyId}", + { params: { path: { workspaceId: workspace.id, policyId } } }, + ); + expect(getRes.response.status).toBe(200); + expect( + getRes.data!.rules[0]!.environmentProgression!.minimumSuccessPercentage, + ).toBeUndefined(); + + await api.DELETE("/v1/workspaces/{workspaceId}/policies/{policyId}", { + params: { path: { workspaceId: workspace.id, policyId } }, + }); + }); + test("should create a policy with multiple rules", async ({ api, workspace, diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index 80bda15f4..c16b81219 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -414,11 +414,8 @@ export interface components { * @default 0 */ minimumSoakTimeMinutes: number; - /** - * Format: float - * @default 100 - */ - minimumSuccessPercentage: number; + /** Format: float */ + minimumSuccessPercentage?: number; /** * @description If true, jobs must also have passed verification to count toward the success percentage * @default false