Skip to content
Closed
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
42 changes: 42 additions & 0 deletions .buildkite/matrix-test.rayci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
group: matrix selection test
sort_key: "99"
tags:
- matrix-test
steps:
# Matrix step with 5 values - only "selected-1" and "selected-2" should be run
# when filtered (others are skipped)
- label: "Matrix {{matrix.variant}}"
key: matrix-step
command: |
echo "Running variant: {{matrix.variant}}"
if [ "{{matrix.variant}}" != "selected-1" ] && [ "{{matrix.variant}}" != "selected-2" ]; then
echo "ERROR: This variant should not have been spawned!"
echo "Only 'selected-1' and 'selected-2' variants should run when matrix-test tag is used."
exit 1
fi
echo "Correct variant spawned."
depends_on: forge
matrix:
setup:
variant:
- "selected-1"
- "selected-2"
- "skipped-1"
- "skipped-2"
- "skipped-3"

# This step depends on only the "selected" variants
- label: "Depends on selected only"
key: depends-on-selected
command: echo "This step depends on only the selected variants"
depends_on:
- key: matrix-step
matrix:
variant: ["selected-1", "selected-2"]
- forge

- label: "Final validation"
tags: always
key: final-validation
command: echo "Final validation"
depends_on: depends-on-selected
149 changes: 146 additions & 3 deletions raycicmd/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,144 @@ func stepTags(step map[string]any) []string {
return nil
}

// matrixExpansionContext tracks matrix steps during expansion for later
// dependency resolution. When a step depends on a matrix step (e.g., "ray-build"),
// we need to know what keys it expanded into (e.g., ["ray-build-python310", ...]).
type matrixExpansionContext struct {
stepKeyToConfig map[string]*matrixConfig
stepKeyToExpanded map[string][]string
}

func expandMatrixInGroups(gs []*pipelineGroup) ([]*pipelineGroup, *matrixExpansionContext, error) {
ctx := &matrixExpansionContext{
stepKeyToConfig: make(map[string]*matrixConfig),
stepKeyToExpanded: make(map[string][]string),
}
var result []*pipelineGroup

for _, g := range gs {
newGroup := &pipelineGroup{
filename: g.filename,
sortKey: g.sortKey,
Group: g.Group,
Key: g.Key,
Tags: g.Tags,
SortKey: g.SortKey,
DependsOn: g.DependsOn,
DefaultJobEnv: g.DefaultJobEnv,
}

for _, step := range g.Steps {
matrixDef, hasMatrix := step["matrix"]
if !hasMatrix {
// No matrix, keep step as-is
newGroup.Steps = append(newGroup.Steps, step)
continue
}

baseKey := stepKey(step)
if baseKey == "" {
// No key - pass through to Buildkite for native matrix handling
newGroup.Steps = append(newGroup.Steps, step)
continue
}

// Parse matrix configuration
cfg, err := parseMatrixConfig(matrixDef)
if err != nil {
return nil, nil, fmt.Errorf("parse matrix in step %q: %w", stepKey(step), err)
}

// Validate label has placeholder
if label, ok := step["label"].(string); ok {
if !hasMatrixPlaceholder(label) {
return nil, nil, fmt.Errorf("matrix step %q: label must contain {{matrix...}} placeholder", baseKey)
}
}

// Register for selector expansion
ctx.stepKeyToConfig[baseKey] = cfg

instances := cfg.expand()
if len(instances) == 0 {
return nil, nil, fmt.Errorf("matrix step %q: no instances after expansion", baseKey)
}

var expandedKeysList []string
for _, inst := range instances {
expandedStep := inst.substituteValues(step).(map[string]any)

expandedKey := inst.generateKey(baseKey, cfg)
if _, hasName := expandedStep["name"]; hasName {
expandedStep["name"] = expandedKey
} else {
expandedStep["key"] = expandedKey
}
delete(expandedStep, "matrix")

originalTags := stepTags(step)
matrixTags := inst.generateTags()
allTags := append([]string{}, originalTags...)
allTags = append(allTags, matrixTags...)
if len(allTags) > 0 {
expandedStep["tags"] = allTags
}

expandedKeysList = append(expandedKeysList, expandedKey)
newGroup.Steps = append(newGroup.Steps, expandedStep)
}

ctx.stepKeyToExpanded[baseKey] = expandedKeysList
}

result = append(result, newGroup)
}

return result, ctx, nil
}

// expandDependsOnSelectors processes depends_on to expand matrix selectors.
func expandDependsOnSelectors(dependsOn any, ctx *matrixExpansionContext) ([]string, error) {
selectors, err := parseMatrixDependsOn(dependsOn)
if err != nil {
return nil, err
}

var result []string
for _, sel := range selectors {
if sel.Matrix == nil {
// Simple key reference - check if it's a matrix step
if expanded, ok := ctx.stepKeyToExpanded[sel.Key]; ok {
// Matrix step: expand to all expanded keys
result = append(result, expanded...)
} else {
// Non-matrix step: use key as-is
result = append(result, sel.Key)
}
} else {
matches, err := sel.expand(ctx.stepKeyToConfig, ctx.stepKeyToExpanded)
if err != nil {
return nil, err
}
result = append(result, matches...)
}
}

return result, nil
}

func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
[]*bkPipelineGroup, error,
) {
expandedGroups, matrixCtx, err := expandMatrixInGroups(gs)
if err != nil {
return nil, fmt.Errorf("expand matrix: %w", err)
}

set := newStepNodeSet()
var groupNodes []*stepNode

for i, g := range gs {
for i, g := range expandedGroups {
groupNode := &stepNode{
id: fmt.Sprintf("g%d", i),
key: g.Key,
Expand Down Expand Up @@ -169,8 +300,20 @@ func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
for _, step := range groupNode.subSteps {
// Track step dependencies.
if dependsOn, ok := step.src["depends_on"]; ok {
deps := toStringList(dependsOn)
for _, dep := range deps {
// Expand matrix selectors in depends_on
expandedDeps, err := expandDependsOnSelectors(dependsOn, matrixCtx)
if err != nil {
return nil, fmt.Errorf("expand depends_on for step %q: %w", step.key, err)
}

// Update the step source with expanded deps for Buildkite output
if len(expandedDeps) == 1 {
step.src["depends_on"] = expandedDeps[0]
} else {
step.src["depends_on"] = expandedDeps
}

for _, dep := range expandedDeps {
if depNode, ok := set.byKey(dep); ok {
set.addDep(step.id, depNode.id)
}
Expand Down
Loading