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
4 changes: 4 additions & 0 deletions apps/workspace-engine/oapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2468,6 +2468,10 @@
"id": {
"type": "string"
},
"if": {
"description": "CEL expression to determine if the job should run",
"type": "string"
},
"matrix": {
"$ref": "#/components/schemas/WorkflowJobMatrix"
},
Expand Down
1 change: 1 addition & 0 deletions apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ local openapi = import '../lib/openapi.libsonnet';
ref: { type: 'string', description: 'Reference to the job agent' },
config: { type: 'object', additionalProperties: true, description: 'Configuration for the job agent' },
matrix: openapi.schemaRef('WorkflowJobMatrix'),
'if': { type: 'string', description: 'CEL expression to determine if the job should run' },
},
},

Expand Down
7 changes: 5 additions & 2 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.

27 changes: 27 additions & 0 deletions apps/workspace-engine/pkg/workspace/workflowmanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ import (
"fmt"
"maps"
"time"
"workspace-engine/pkg/celutil"
"workspace-engine/pkg/oapi"
"workspace-engine/pkg/workspace/jobagents"
"workspace-engine/pkg/workspace/store"

"github.com/google/uuid"
)

var workflowCelEnv, _ = celutil.NewEnvBuilder().
WithMapVariable("inputs").
BuildCached(24 * time.Hour)
Comment on lines +16 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/workspace-engine/pkg/workspace/workflowmanager/manager.go | head -100

Repository: ctrlplanedev/ctrlplane

Length of output: 3801


🏁 Script executed:

find apps/workspace-engine -name "celutil*" -type f

Repository: ctrlplanedev/ctrlplane

Length of output: 48


🏁 Script executed:

rg -l "celutil" apps/workspace-engine --type go | head -20

Repository: ctrlplanedev/ctrlplane

Length of output: 859


🏁 Script executed:

cat -n apps/workspace-engine/pkg/celutil/cel.go

Repository: ctrlplanedev/ctrlplane

Length of output: 8195


🏁 Script executed:

rg "workflowCelEnv" apps/workspace-engine/pkg/workspace/workflowmanager/manager.go

Repository: ctrlplanedev/ctrlplane

Length of output: 167


Handle CEL env initialization errors instead of ignoring them.

If BuildCached fails, workflowCelEnv is nil and calling Compile at line 63 will panic. Capture the error and surface it (or fail fast) before evaluation.

🔧 Suggested fix
-var workflowCelEnv, _ = celutil.NewEnvBuilder().
+var workflowCelEnv, workflowCelEnvErr = celutil.NewEnvBuilder().
 	WithMapVariable("inputs").
 	BuildCached(24 * time.Hour)
 func (m *Manager) evaluateJobTemplateIf(jobTemplate oapi.WorkflowJobTemplate, inputs map[string]any) (bool, error) {
+	if workflowCelEnvErr != nil {
+		return false, fmt.Errorf("failed to initialize workflow CEL environment: %w", workflowCelEnvErr)
+	}
 	prg, err := workflowCelEnv.Compile(*jobTemplate.If)
 	if err != nil {
 		return false, fmt.Errorf("failed to compile CEL expression for job %q: %w", jobTemplate.Name, err)
 	}
🤖 Prompt for AI Agents
In `@apps/workspace-engine/pkg/workspace/workflowmanager/manager.go` around lines
16 - 18, The global initialization currently ignores errors from
celutil.NewEnvBuilder().WithMapVariable("inputs").BuildCached(...) so
workflowCelEnv can be nil and later cause a panic when calling Compile; change
the initialization to capture the returned (env, err), check err (and that env
!= nil) and fail fast or return an error from the surrounding init/constructor
(or log and os.Exit) so callers don't hit Compile on a nil env; update any code
that assumed the package-global (e.g., where Compile is invoked) to handle the
initialization error if you convert initialization to a function that returns
(env, error) instead of a silent global.


type Manager struct {
store *store.Store
jobAgentRegistry *jobagents.Registry
Expand Down Expand Up @@ -54,6 +59,18 @@ func (m *Manager) maybeSetDefaultInputValues(inputs map[string]any, workflowTemp
}
}

func (m *Manager) evaluateJobTemplateIf(jobTemplate oapi.WorkflowJobTemplate, inputs map[string]any) (bool, error) {
prg, err := workflowCelEnv.Compile(*jobTemplate.If)
if err != nil {
return false, fmt.Errorf("failed to compile CEL expression for job %q: %w", jobTemplate.Name, err)
}
result, err := celutil.EvalBool(prg, map[string]any{"inputs": inputs})
if err != nil {
return false, fmt.Errorf("failed to evaluate CEL expression for job %q: %w", jobTemplate.Name, err)
}
return result, nil
}

func (m *Manager) CreateWorkflow(ctx context.Context, workflowTemplateId string, inputs map[string]any) (*oapi.Workflow, error) {
workflowTemplate, ok := m.store.WorkflowTemplates.Get(workflowTemplateId)
if !ok {
Expand All @@ -70,6 +87,16 @@ func (m *Manager) CreateWorkflow(ctx context.Context, workflowTemplateId string,

workflowJobs := make([]*oapi.WorkflowJob, 0, len(workflowTemplate.Jobs))
for idx, jobTemplate := range workflowTemplate.Jobs {
if jobTemplate.If != nil {
shouldRun, err := m.evaluateJobTemplateIf(jobTemplate, inputs)
if err != nil {
return nil, fmt.Errorf("failed to evaluate CEL expression for job %q: %w", jobTemplate.Name, err)
}
if !shouldRun {
continue
}
}

wfJob := &oapi.WorkflowJob{
Id: uuid.New().String(),
WorkflowId: workflow.Id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,43 @@ func TestWorkflowView_IsComplete(t *testing.T) {
assert.True(t, wfv.IsComplete())
}

func TestCreateWorkflow_SkipsJobWhenIfEvaluatesToFalse(t *testing.T) {
ctx := context.Background()
store := store.New("test-workspace", statechange.NewChangeSet[any]())
jobAgentRegistry := jobagents.NewRegistry(store, verification.NewManager(store))
manager := NewWorkflowManager(store, jobAgentRegistry)

jobAgent := &oapi.JobAgent{
Id: "test-job-agent",
Name: "test-job-agent",
Type: "test-runner",
}
store.JobAgents.Upsert(ctx, jobAgent)

ifTrue := "inputs.run_job == true"
ifFalse := "inputs.run_job == false"

workflowTemplate := &oapi.WorkflowTemplate{
Id: "test-workflow-template",
Name: "test-workflow-template",
Jobs: []oapi.WorkflowJobTemplate{
{Id: "always-job", Name: "always-job", Ref: "test-job-agent", Config: map[string]any{}},
{Id: "true-job", Name: "true-job", Ref: "test-job-agent", Config: map[string]any{}, If: &ifTrue},
{Id: "false-job", Name: "false-job", Ref: "test-job-agent", Config: map[string]any{}, If: &ifFalse},
},
}
store.WorkflowTemplates.Upsert(ctx, workflowTemplate)

wf, err := manager.CreateWorkflow(ctx, "test-workflow-template", map[string]any{
"run_job": true,
})
assert.NoError(t, err)
assert.NotNil(t, wf)

wfJobs := store.WorkflowJobs.GetByWorkflowId(wf.Id)
assert.Len(t, wfJobs, 2, "should have 2 jobs: always-job and true-job, but not false-job")
}

func TestMaybeSetDefaultInputValues_SetsStringDefault(t *testing.T) {
store := store.New("test-workspace", statechange.NewChangeSet[any]())
jobAgentRegistry := jobagents.NewRegistry(store, verification.NewManager(store))
Expand Down
2 changes: 2 additions & 0 deletions packages/workspace-engine-sdk/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,8 @@ export interface components {
[key: string]: unknown;
};
id: string;
/** @description CEL expression to determine if the job should run */
if?: string;
matrix?: components["schemas"]["WorkflowJobMatrix"];
name: string;
/** @description Reference to the job agent */
Expand Down
Loading