diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 8945dd482..760ba4e89 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -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" }, diff --git a/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet b/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet index dc9795d48..c50ae4d0d 100644 --- a/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet @@ -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' }, }, }, diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index d78f745a4..0c5c808b9 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -1228,8 +1228,11 @@ type WorkflowJobTemplate struct { // Config Configuration for the job agent Config map[string]interface{} `json:"config"` Id string `json:"id"` - Matrix *WorkflowJobMatrix `json:"matrix,omitempty"` - Name string `json:"name"` + + // If CEL expression to determine if the job should run + If *string `json:"if,omitempty"` + Matrix *WorkflowJobMatrix `json:"matrix,omitempty"` + Name string `json:"name"` // Ref Reference to the job agent Ref string `json:"ref"` diff --git a/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go b/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go index 1a8bcd68b..4c6569a37 100644 --- a/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go +++ b/apps/workspace-engine/pkg/workspace/workflowmanager/manager.go @@ -5,6 +5,7 @@ import ( "fmt" "maps" "time" + "workspace-engine/pkg/celutil" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/jobagents" "workspace-engine/pkg/workspace/store" @@ -12,6 +13,10 @@ import ( "github.com/google/uuid" ) +var workflowCelEnv, _ = celutil.NewEnvBuilder(). + WithMapVariable("inputs"). + BuildCached(24 * time.Hour) + type Manager struct { store *store.Store jobAgentRegistry *jobagents.Registry @@ -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 { @@ -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, diff --git a/apps/workspace-engine/pkg/workspace/workflowmanager/manager_test.go b/apps/workspace-engine/pkg/workspace/workflowmanager/manager_test.go index d262409f3..1ede602ae 100644 --- a/apps/workspace-engine/pkg/workspace/workflowmanager/manager_test.go +++ b/apps/workspace-engine/pkg/workspace/workflowmanager/manager_test.go @@ -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)) diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index 41ddf6c31..15935349c 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -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 */