Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/workspace-engine/oapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@
],
"type": "object"
},
"DeploymentDependency": {
"properties": {
"versionSelector": {
"description": "CEL expression evaluated against the dependency deployment's current release version on the same resource.",
"type": "string"
}
},
"required": [
"versionSelector"
],
"type": "object"
},
"DeploymentDependencyRule": {
"properties": {
"dependsOn": {
Expand Down
11 changes: 11 additions & 0 deletions apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ local openapi = import '../lib/openapi.libsonnet';
},
},

DeploymentDependency: {
type: 'object',
required: ['versionSelector'],
properties: {
versionSelector: {
type: 'string',
description: "CEL expression evaluated against the dependency deployment's current release version on the same resource.",
},
},
},


DeploymentWithVariablesAndSystems: {
type: 'object',
Expand Down
39 changes: 39 additions & 0 deletions apps/workspace-engine/pkg/db/computed_resources.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions apps/workspace-engine/pkg/db/deployments.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions apps/workspace-engine/pkg/db/queries/computed_resources.sql
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ JOIN system_environment se
AND se.system_id = sd.system_id
WHERE cdr.resource_id = @resource_id;

-- name: GetReleaseTargetForDeploymentResource :one
-- Returns one release target for a (deployment, resource) pair across any
-- environment. Used by the deployment-version-dependency evaluator, which
-- only needs to identify some environment in which the dependency lives.
SELECT DISTINCT
cdr.deployment_id,
cer.environment_id,
cdr.resource_id
FROM computed_deployment_resource cdr
JOIN computed_environment_resource cer
ON cer.resource_id = cdr.resource_id
JOIN system_deployment sd
ON sd.deployment_id = cdr.deployment_id
JOIN system_environment se
ON se.environment_id = cer.environment_id
AND se.system_id = sd.system_id
WHERE cdr.deployment_id = @deployment_id
AND cdr.resource_id = @resource_id
LIMIT 1;
Comment on lines +75 to +93
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetReleaseTargetForDeploymentResource picks an arbitrary environment via LIMIT 1 over computed tables, then GetCurrentReleaseByReleaseTarget looks for the latest successful release in that specific environment. If the chosen environment has no successful releases (but another environment does), the deployment-version-dependency evaluator will incorrectly deny. Consider changing this query (or adding a dedicated query) to select the environment that has the latest successful release for (deployment_id, resource_id), e.g., by joining release/release_job/job with j.status='successful' and ordering by j.completed_at DESC.

Copilot uses AI. Check for mistakes.

-- name: GetReleaseTargetsForEnvironment :many
-- Returns all valid release targets for an environment by joining computed
-- resource tables through the system link tables.
Expand Down
6 changes: 6 additions & 0 deletions apps/workspace-engine/pkg/db/queries/deployments.sql
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@ SELECT deployment_id FROM system_deployment WHERE system_id = $1;
-- name: DeleteDeployment :exec
DELETE FROM deployment WHERE id = $1;

-- name: GetDeploymentDependenciesByDeploymentID :many
SELECT dependency_deployment_id, version_selector
FROM deployment_dependency
WHERE deployment_id = $1
ORDER BY dependency_deployment_id;

6 changes: 6 additions & 0 deletions apps/workspace-engine/pkg/oapi/oapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package deploymentversiondependency

import (
"context"
"fmt"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"workspace-engine/pkg/celutil"
"workspace-engine/pkg/oapi"
cel "workspace-engine/pkg/selector/langs/cel"
"workspace-engine/pkg/workspace/releasemanager/policy/evaluator"
"workspace-engine/pkg/workspace/releasemanager/policy/results"
)

var tracer = otel.Tracer("DeploymentVersionDependencyEvaluator")

const RuleType = "deploymentVersionDependency"

type Evaluator struct {
getters Getters
}

func NewEvaluator(getters Getters) evaluator.Evaluator {
if getters == nil {
return nil
}
return evaluator.WithMemoization(&Evaluator{getters: getters})
}

func (e *Evaluator) ScopeFields() evaluator.ScopeFields {
return evaluator.ScopeDeployment | evaluator.ScopeResource
}

func (e *Evaluator) RuleType() string { return RuleType }

func (e *Evaluator) RuleId() string { return "deployment-version-dependency" }

func (e *Evaluator) Complexity() int { return 3 }

func (e *Evaluator) Evaluate(
ctx context.Context,
scope evaluator.EvaluatorScope,
) *oapi.RuleEvaluation {
ctx, span := tracer.Start(ctx, "DeploymentVersionDependencyEvaluator.Evaluate")
defer span.End()

span.SetAttributes(
attribute.String("deployment.id", scope.Deployment.Id),
attribute.String("resource.id", scope.Resource.Id),
)

edges, err := e.getters.GetDependencies(ctx, scope.Deployment.Id)
if err != nil {
span.RecordError(err)
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: failed to load dependencies: %v", err),
).WithDetail("error", err.Error())
}

if len(edges) == 0 {
return results.NewAllowedResult("Deployment dependency: no dependencies declared")
}

for _, edge := range edges {
if denied := e.evaluateEdge(ctx, scope, edge); denied != nil {
return denied
}
}

return results.NewAllowedResult("Deployment dependency: all dependencies satisfied")
}
Comment on lines +56 to +72
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All user-facing result messages in this evaluator start with "Deployment dependency:", which is easily confused with the existing policy-rule evaluator deploymentdependency (and the new rule type is deploymentVersionDependency). Consider updating the message prefix to something unambiguous like "Deployment version dependency:" so UI/logs clearly identify which evaluator produced the result.

Copilot uses AI. Check for mistakes.

func (e *Evaluator) evaluateEdge(
ctx context.Context,
scope evaluator.EvaluatorScope,
edge DependencyEdge,
) *oapi.RuleEvaluation {
program, err := cel.CompileProgram(edge.VersionSelector)
if err != nil {
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: failed to compile selector for %s: %v",
edge.DependencyDeploymentID, err),
).
WithDetail("dependency_deployment_id", edge.DependencyDeploymentID).
WithDetail("version_selector", edge.VersionSelector)
}

rt, err := e.getters.GetReleaseTargetForDeploymentResource(
ctx, edge.DependencyDeploymentID, scope.Resource.Id,
)
if err != nil {
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: failed to look up release target for %s: %v",
edge.DependencyDeploymentID, err),
).WithDetail("dependency_deployment_id", edge.DependencyDeploymentID)
}
if rt == nil {
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: dependency %s is not deployed on this resource",
edge.DependencyDeploymentID),
).WithDetail("dependency_deployment_id", edge.DependencyDeploymentID)
}

version, err := e.getters.GetCurrentVersionForReleaseTarget(ctx, rt)
if err != nil {
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: failed to load current version for %s: %v",
edge.DependencyDeploymentID, err),
).WithDetail("dependency_deployment_id", edge.DependencyDeploymentID)
}
if version == nil {
return results.NewDeniedResult(
fmt.Sprintf(
"Deployment dependency: dependency %s has no successful release on this resource",
edge.DependencyDeploymentID,
),
).WithDetail("dependency_deployment_id", edge.DependencyDeploymentID)
}

celCtx := map[string]any{"version": cel.DeploymentVersionToMap(version)}
matched, err := celutil.EvalBool(program, celCtx)
if err != nil {
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: CEL evaluation error for %s: %v",
edge.DependencyDeploymentID, err),
).
WithDetail("dependency_deployment_id", edge.DependencyDeploymentID).
WithDetail("version_selector", edge.VersionSelector)
}
if !matched {
return results.NewDeniedResult(
fmt.Sprintf("Deployment dependency: dependency %s version %s does not satisfy selector",
edge.DependencyDeploymentID, version.Tag),
).
WithDetail("dependency_deployment_id", edge.DependencyDeploymentID).
WithDetail("dependency_version", version.Tag).
WithDetail("version_selector", edge.VersionSelector)
}

return nil
}
Loading
Loading