Skip to content

chore: manager resolves matrix#775

Open
adityachoudhari26 wants to merge 1 commit intomainfrom
manager-resolves-matrix
Open

chore: manager resolves matrix#775
adityachoudhari26 wants to merge 1 commit intomainfrom
manager-resolves-matrix

Conversation

@adityachoudhari26
Copy link
Member

@adityachoudhari26 adityachoudhari26 commented Feb 4, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Added matrix expansion support for workflows, enabling a single job template to generate multiple jobs with different parameter combinations
    • Implemented selector-based input resolution for workflows, allowing dynamic resource selection based on criteria during workflow creation
    • Enhanced job dispatch logic to support per-job configuration and parameterization

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

The pull request introduces matrix resolution and job expansion logic to the workflow engine. It adds a resolvedMatrix field to WorkflowJob, creates a MatrixResolver to compute parameter combinations via Cartesian product, implements selector resolution for workflow inputs, and updates the workflow manager to resolve matrices upfront and dispatch multiple jobs when matrix configurations are present.

Changes

Cohort / File(s) Summary
Schema and Type Definitions
apps/workspace-engine/oapi/openapi.json, apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet, apps/workspace-engine/pkg/oapi/oapi.gen.go, apps/workspace-engine/pkg/workspace/jobagents/types/types.go
Added resolvedMatrix field to WorkflowJob schema and generated Go types; added Matrix field to DispatchContext for passing matrix data during job dispatch.
Matrix Resolution
apps/workspace-engine/pkg/workspace/workflowmanager/matrix_resolver.go
New file implementing MatrixResolver to convert WorkflowJobMatrix into concrete parameter combinations via Cartesian product, with error handling for missing/invalid inputs.
Selector Resolution
apps/workspace-engine/pkg/workspace/workflowmanager/resolve_selectors.go
New file providing selector resolution logic for workflow template inputs, supporting Resource, Environment, and Deployment selectors with helper methods for entity lookup and map conversion.
Workflow Manager
apps/workspace-engine/pkg/workspace/workflowmanager/manager.go
Updated CreateWorkflow to resolve selectors and matrices upfront; refactored dispatch logic to create and dispatch multiple jobs when ResolvedMatrix is present, with simplified mergeJobAgentConfig signature.
Store Accessors
apps/workspace-engine/pkg/workspace/store/deployments.go, apps/workspace-engine/pkg/workspace/store/environments.go
Added ForSelector methods to Deployments and Environments for selector-based lookup returning maps of matched entities.
Job Agent Config
apps/workspace-engine/pkg/workspace/jobagents/registry.go
Added matrix propagation from job agent config into render context when workflow is present.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WorkflowManager
    participant SelectorResolver
    participant MatrixResolver
    participant JobDispatcher
    participant Store

    Client->>WorkflowManager: CreateWorkflow(template, inputs)
    
    WorkflowManager->>SelectorResolver: getInputWithResolvedSelectors()
    SelectorResolver->>Store: getResources/getEnvironments/getDeployments(selector)
    Store-->>SelectorResolver: matched entities
    SelectorResolver-->>WorkflowManager: resolved inputs
    
    loop For each jobTemplate
        alt Matrix present
            WorkflowManager->>MatrixResolver: NewMatrixResolver(matrix, inputs)
            MatrixResolver-->>MatrixResolver: getMatrixRows()
            MatrixResolver-->>MatrixResolver: computeCartesianProduct()
            MatrixResolver-->>WorkflowManager: Resolve()
            WorkflowManager->>WorkflowManager: store ResolvedMatrix on WorkflowJob
        else No matrix
            WorkflowManager->>WorkflowManager: ResolvedMatrix = nil
        end
    end
    
    WorkflowManager->>JobDispatcher: dispatch workflow
    loop For each workflowJob
        alt ResolvedMatrix exists
            loop For each matrix item
                JobDispatcher->>JobDispatcher: mergeJobAgentConfig + matrix item
                JobDispatcher->>Store: create Job with augmented config
            end
        else No matrix
            JobDispatcher->>JobDispatcher: mergeJobAgentConfig
            JobDispatcher->>Store: create single Job
        end
    end
    
    JobDispatcher-->>Client: workflow with jobs created
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hop through matrices with glee,
Cartesian products wild and free!
Each selector finds its kin so true,
One job blooms into jobs anew!
The workflow engine dances light,
Expansion logic, pure delight! 🎆

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: manager resolves matrix' accurately describes the primary change: the workflow manager now resolves matrix configurations during workflow creation and uses resolved matrices for job dispatching.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch manager-resolves-matrix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/workspace-engine/pkg/workspace/jobagents/registry.go (1)

82-90: ⚠️ Potential issue | 🟠 Major

Guard the matrix type assertion to avoid panics.

Line 88 will panic if matrix isn’t a map[string]interface{} (e.g., array or string). Use a safe cast and fail fast with a clear error.

✅ Suggested fix
-		if job.JobAgentConfig["matrix"] != nil {
-			renderContext.Matrix = job.JobAgentConfig["matrix"].(map[string]interface{})
-		}
+		if raw, ok := job.JobAgentConfig["matrix"]; ok && raw != nil {
+			matrix, ok := raw.(map[string]interface{})
+			if !ok {
+				return fmt.Errorf("job agent config matrix must be an object")
+			}
+			renderContext.Matrix = matrix
+		}
🤖 Fix all issues with AI agents
In `@apps/workspace-engine/pkg/workspace/workflowmanager/manager.go`:
- Around line 54-60: The current loop upserts WorkflowJobs early and then calls
NewMatrixResolver(...).Resolve(), which can fail and leave orphaned jobs; change
the logic in the manager that iterates job templates (the code that constructs
wfJob and calls WorkflowJobs.Upsert) to first resolve all job templates
including calling NewMatrixResolver(jobTemplate.Matrix,
inputsWithResolvedSelectors).Resolve() and setting WorkflowJob.ResolvedMatrix
(or collect resolved wfJob objects in-memory), and only after all templates
resolve successfully perform WorkflowJobs.Upsert for each wfJob; alternatively,
implement a rollback path that calls WorkflowJobs.Delete (or the repository's
delete/cleanup method) for any jobs already Upserted if Resolve() returns an
error—apply this change around the loop that creates wfJob and where
WorkflowJobs.Upsert is invoked.

In `@apps/workspace-engine/pkg/workspace/workflowmanager/matrix_resolver.go`:
- Around line 25-49: In getMatrixRows, the branch handling AsWorkflowJobMatrix1
assumes r.inputs[asString] is []map[string]interface{} which fails for
JSON-decoded arrays (they are []interface{}); update the logic in getMatrixRows
(and the matrixRow creation) to first attempt the existing
[]map[string]interface{} assertion, and if that fails check for []interface{}
and iterate over its elements converting each element to map[string]interface{}
(returning an error if any element is not a map); keep the same error message
("input %s is not an array") or refine to indicate element type issues and
ensure the converted slice is used as Values when appending the matrixRow.

In `@apps/workspace-engine/pkg/workspace/workflowmanager/resolve_selectors.go`:
- Around line 62-105: The code in getInputWithResolvedSelectors currently
unsafely asserts selectorInputEntry.(string) which panics for non-string types;
change the handling of selectorInputEntry (from asSelectorArray.Name) to perform
a safe type check (e.g., type switch or ok, str := selectorInputEntry.(string))
and if the value is not a string return a clear validation error rather than
panicking, then pass the validated string into sel.FromCelSelector (or handle
the absence/invalid type by falling back to the default selector) so sel is only
constructed when a valid string is provided.

Comment on lines +54 to +60
if jobTemplate.Matrix != nil {
resolvedMatrix, err := NewMatrixResolver(jobTemplate.Matrix, inputsWithResolvedSelectors).Resolve()
if err != nil {
return workflow, fmt.Errorf("failed to resolve matrix: %w", err)
}
wfJob.ResolvedMatrix = &resolvedMatrix
}
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

Avoid partial WorkflowJob persistence on matrix resolution failure.
If matrix resolution fails (Line 55–57), earlier jobs have already been Upserted (Line 61), but the workflow isn’t stored yet—this leaves orphaned jobs. Consider deferring WorkflowJobs.Upsert until all templates resolve, or rolling back on error.

💡 Suggested change to defer job persistence
	for idx, jobTemplate := range workflowTemplate.Jobs {
		wfJob := &oapi.WorkflowJob{
			Id:         uuid.New().String(),
			WorkflowId: workflow.Id,
			Index:      idx,
			Ref:        jobTemplate.Ref,
			Config:     maps.Clone(jobTemplate.Config),
		}

		if jobTemplate.Matrix != nil {
			resolvedMatrix, err := NewMatrixResolver(jobTemplate.Matrix, inputsWithResolvedSelectors).Resolve()
			if err != nil {
				return workflow, fmt.Errorf("failed to resolve matrix: %w", err)
			}
			wfJob.ResolvedMatrix = &resolvedMatrix
		}
-		m.store.WorkflowJobs.Upsert(ctx, wfJob)
		workflowJobs = append(workflowJobs, wfJob)
	}

+	for _, wfJob := range workflowJobs {
+		m.store.WorkflowJobs.Upsert(ctx, wfJob)
+	}
🤖 Prompt for AI Agents
In `@apps/workspace-engine/pkg/workspace/workflowmanager/manager.go` around lines
54 - 60, The current loop upserts WorkflowJobs early and then calls
NewMatrixResolver(...).Resolve(), which can fail and leave orphaned jobs; change
the logic in the manager that iterates job templates (the code that constructs
wfJob and calls WorkflowJobs.Upsert) to first resolve all job templates
including calling NewMatrixResolver(jobTemplate.Matrix,
inputsWithResolvedSelectors).Resolve() and setting WorkflowJob.ResolvedMatrix
(or collect resolved wfJob objects in-memory), and only after all templates
resolve successfully perform WorkflowJobs.Upsert for each wfJob; alternatively,
implement a rollback path that calls WorkflowJobs.Delete (or the repository's
delete/cleanup method) for any jobs already Upserted if Resolve() returns an
error—apply this change around the loop that creates wfJob and where
WorkflowJobs.Upsert is invoked.

Comment on lines +25 to +49
func (r *MatrixResolver) getMatrixRows() ([]matrixRow, error) {
matrixRows := make([]matrixRow, 0, len(*r.matrix))
for key, value := range *r.matrix {
asArray, err := value.AsWorkflowJobMatrix0()
if err == nil {
matrixRows = append(matrixRows, matrixRow{
Key: key,
Values: asArray,
})
continue
}
asString, err := value.AsWorkflowJobMatrix1()
if err == nil {
arrayFromInput, ok := r.inputs[asString].([]map[string]interface{})
if !ok {
return nil, fmt.Errorf("input %s is not an array", asString)
}
matrixRows = append(matrixRows, matrixRow{
Key: key,
Values: arrayFromInput,
})
continue
}
}
return matrixRows, nil
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

Handle JSON-decoded input arrays when matrix references inputs.
Line 38 assumes []map[string]interface{}. Inputs coming from JSON are typically []interface{} (with map[string]interface{} elements), so this path will error even for valid arrays. Consider accepting []interface{} and converting items.

🔧 Suggested conversion to support JSON-decoded arrays
		asString, err := value.AsWorkflowJobMatrix1()
		if err == nil {
-			arrayFromInput, ok := r.inputs[asString].([]map[string]interface{})
-			if !ok {
-				return nil, fmt.Errorf("input %s is not an array", asString)
-			}
+			raw, ok := r.inputs[asString]
+			if !ok {
+				return nil, fmt.Errorf("input %s not found", asString)
+			}
+			var arrayFromInput []map[string]interface{}
+			switch v := raw.(type) {
+			case []map[string]interface{}:
+				arrayFromInput = v
+			case []interface{}:
+				arrayFromInput = make([]map[string]interface{}, 0, len(v))
+				for _, item := range v {
+					m, ok := item.(map[string]interface{})
+					if !ok {
+						return nil, fmt.Errorf("input %s contains non-object item", asString)
+					}
+					arrayFromInput = append(arrayFromInput, m)
+				}
+			default:
+				return nil, fmt.Errorf("input %s is not an array", asString)
+			}
			matrixRows = append(matrixRows, matrixRow{
				Key:    key,
				Values: arrayFromInput,
			})
			continue
		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (r *MatrixResolver) getMatrixRows() ([]matrixRow, error) {
matrixRows := make([]matrixRow, 0, len(*r.matrix))
for key, value := range *r.matrix {
asArray, err := value.AsWorkflowJobMatrix0()
if err == nil {
matrixRows = append(matrixRows, matrixRow{
Key: key,
Values: asArray,
})
continue
}
asString, err := value.AsWorkflowJobMatrix1()
if err == nil {
arrayFromInput, ok := r.inputs[asString].([]map[string]interface{})
if !ok {
return nil, fmt.Errorf("input %s is not an array", asString)
}
matrixRows = append(matrixRows, matrixRow{
Key: key,
Values: arrayFromInput,
})
continue
}
}
return matrixRows, nil
func (r *MatrixResolver) getMatrixRows() ([]matrixRow, error) {
matrixRows := make([]matrixRow, 0, len(*r.matrix))
for key, value := range *r.matrix {
asArray, err := value.AsWorkflowJobMatrix0()
if err == nil {
matrixRows = append(matrixRows, matrixRow{
Key: key,
Values: asArray,
})
continue
}
asString, err := value.AsWorkflowJobMatrix1()
if err == nil {
raw, ok := r.inputs[asString]
if !ok {
return nil, fmt.Errorf("input %s not found", asString)
}
var arrayFromInput []map[string]interface{}
switch v := raw.(type) {
case []map[string]interface{}:
arrayFromInput = v
case []interface{}:
arrayFromInput = make([]map[string]interface{}, 0, len(v))
for _, item := range v {
m, ok := item.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("input %s contains non-object item", asString)
}
arrayFromInput = append(arrayFromInput, m)
}
default:
return nil, fmt.Errorf("input %s is not an array", asString)
}
matrixRows = append(matrixRows, matrixRow{
Key: key,
Values: arrayFromInput,
})
continue
}
}
return matrixRows, nil
}
🤖 Prompt for AI Agents
In `@apps/workspace-engine/pkg/workspace/workflowmanager/matrix_resolver.go`
around lines 25 - 49, In getMatrixRows, the branch handling AsWorkflowJobMatrix1
assumes r.inputs[asString] is []map[string]interface{} which fails for
JSON-decoded arrays (they are []interface{}); update the logic in getMatrixRows
(and the matrixRow creation) to first attempt the existing
[]map[string]interface{} assertion, and if that fails check for []interface{}
and iterate over its elements converting each element to map[string]interface{}
(returning an error if any element is not a map); keep the same error message
("input %s is not an array") or refine to indicate element type issues and
ensure the converted slice is used as Values when appending the matrixRow.

Comment on lines +62 to +105
func (m *Manager) getInputWithResolvedSelectors(ctx context.Context, workflowTemplate *oapi.WorkflowTemplate, inputs map[string]any) (map[string]any, error) {
inputsClone := maps.Clone(inputs)

for _, input := range workflowTemplate.Inputs {
asArray, err := input.AsWorkflowArrayInput()
if err != nil {
continue
}

asSelectorArray, err := asArray.AsWorkflowSelectorArrayInput()
if err != nil {
continue
}

sel := asSelectorArray.Selector.Default

selectorInputEntry, ok := inputsClone[asSelectorArray.Name]
if ok {
selectorInputEntryString := selectorInputEntry.(string)
sel = &oapi.Selector{}
if err := sel.FromCelSelector(oapi.CelSelector{Cel: selectorInputEntryString}); err != nil {
return nil, fmt.Errorf("failed to parse selector: %w", err)
}
}

if sel == nil {
return nil, fmt.Errorf("selector is nil")
}

var matchedEntities []map[string]interface{}

switch asSelectorArray.Selector.EntityType {
case oapi.WorkflowSelectorArrayInputSelectorEntityTypeResource:
matchedEntities = m.getResources(ctx, sel)
case oapi.WorkflowSelectorArrayInputSelectorEntityTypeEnvironment:
matchedEntities = m.getEnvironments(ctx, sel)
case oapi.WorkflowSelectorArrayInputSelectorEntityTypeDeployment:
matchedEntities = m.getDeployments(ctx, sel)
}

inputsClone[asSelectorArray.Name] = matchedEntities
}

return inputsClone, nil
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 | 🔴 Critical

Guard selector input type to avoid runtime panic.
Pipeline failure shows a panic at Line 80: interface {} is float64, not string. The direct selectorInputEntry.(string) assertion will panic for non-string inputs (e.g., JSON numbers). Use a safe type assertion and return a validation error instead.

🛠️ Suggested fix for safe selector parsing
	selectorInputEntry, ok := inputsClone[asSelectorArray.Name]
	if ok {
-		selectorInputEntryString := selectorInputEntry.(string)
+		selectorInputEntryString, ok := selectorInputEntry.(string)
+		if !ok {
+			return nil, fmt.Errorf("selector input %s must be a string", asSelectorArray.Name)
+		}
		sel = &oapi.Selector{}
		if err := sel.FromCelSelector(oapi.CelSelector{Cel: selectorInputEntryString}); err != nil {
			return nil, fmt.Errorf("failed to parse selector: %w", err)
		}
	}
🧰 Tools
🪛 GitHub Actions: Apps / Workspace Engine

[error] 80-80: Runtime panic: interface conversion: interface {} is float64, not string at resolve_selectors.go:80

🤖 Prompt for AI Agents
In `@apps/workspace-engine/pkg/workspace/workflowmanager/resolve_selectors.go`
around lines 62 - 105, The code in getInputWithResolvedSelectors currently
unsafely asserts selectorInputEntry.(string) which panics for non-string types;
change the handling of selectorInputEntry (from asSelectorArray.Name) to perform
a safe type check (e.g., type switch or ok, str := selectorInputEntry.(string))
and if the value is not a string return a clear validation error rather than
panicking, then pass the validated string into sel.FromCelSelector (or handle
the absence/invalid type by falling back to the default selector) so sel is only
constructed when a valid string is provided.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants