Skip to content

Commit 83f7a55

Browse files
feat(raycicmd): add matrix expansion and filtering support
Integrate matrix_expand into conversion steps. This allows pipeline steps to selectively depend on a subset of matrix-defined steps, reducing unnecessary steps when tag filtering can filter those out. - Introduced a new YAML configuration for matrix selection tests in Buildkite. - Implemented matrix expansion logic in the converter, allowing for dynamic step generation based on matrix configurations. Topic: matrix-converter Relative: matrix-expand Labels: draft Signed-off-by: andrew <andrew@anyscale.com>
1 parent bc2e07e commit 83f7a55

File tree

3 files changed

+481
-5
lines changed

3 files changed

+481
-5
lines changed

.buildkite/matrix-test.rayci.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
group: matrix selection test
2+
sort_key: "99"
3+
tags:
4+
- matrix-test
5+
steps:
6+
# Matrix step with 5 values - only "selected-1" and "selected-2" should be run
7+
# when filtered (others are skipped)
8+
- label: "Matrix {{matrix.variant}}"
9+
key: matrix-step
10+
command: |
11+
echo "Running variant: {{matrix.variant}}"
12+
if [ "{{matrix.variant}}" != "selected-1" ] && [ "{{matrix.variant}}" != "selected-2" ]; then
13+
echo "ERROR: This variant should not have been spawned!"
14+
echo "Only 'selected-1' and 'selected-2' variants should run when matrix-test tag is used."
15+
exit 1
16+
fi
17+
echo "Correct variant spawned."
18+
depends_on: forge
19+
matrix:
20+
setup:
21+
variant:
22+
- "selected-1"
23+
- "selected-2"
24+
- "skipped-1"
25+
- "skipped-2"
26+
- "skipped-3"
27+
28+
# This step depends on only the "selected" variants
29+
- label: "Depends on selected only"
30+
key: depends-on-selected
31+
command: echo "This step depends on only the selected variants"
32+
depends_on:
33+
- key: matrix-step
34+
matrix:
35+
variant: ["selected-1", "selected-2"]
36+
- forge
37+
38+
- label: "Final validation"
39+
tags: always
40+
key: final-validation
41+
command: echo "Final validation"
42+
depends_on: depends-on-selected

raycicmd/converter.go

Lines changed: 152 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,150 @@ func stepTags(step map[string]any) []string {
115115
return nil
116116
}
117117

118+
// matrixExpansionContext holds state for matrix expansion.
119+
type matrixExpansionContext struct {
120+
// registry maps base step keys to their matrix configs
121+
registry map[string]*matrixConfig
122+
// expandedKeys maps base step keys to their expanded keys
123+
expandedKeys map[string][]string
124+
}
125+
126+
func newMatrixExpansionContext() *matrixExpansionContext {
127+
return &matrixExpansionContext{
128+
registry: make(map[string]*matrixConfig),
129+
expandedKeys: make(map[string][]string),
130+
}
131+
}
132+
133+
// expandMatrixInGroups preprocesses pipeline groups to expand matrix steps.
134+
// Returns the modified groups and a context for selector expansion.
135+
func expandMatrixInGroups(gs []*pipelineGroup) ([]*pipelineGroup, *matrixExpansionContext, error) {
136+
ctx := newMatrixExpansionContext()
137+
var result []*pipelineGroup
138+
139+
for _, g := range gs {
140+
newGroup := &pipelineGroup{
141+
filename: g.filename,
142+
sortKey: g.sortKey,
143+
Group: g.Group,
144+
Key: g.Key,
145+
Tags: g.Tags,
146+
SortKey: g.SortKey,
147+
DependsOn: g.DependsOn,
148+
DefaultJobEnv: g.DefaultJobEnv,
149+
}
150+
151+
for _, step := range g.Steps {
152+
matrixDef, hasMatrix := step["matrix"]
153+
if !hasMatrix {
154+
// No matrix, keep step as-is
155+
newGroup.Steps = append(newGroup.Steps, step)
156+
continue
157+
}
158+
159+
baseKey := stepKey(step)
160+
if baseKey == "" {
161+
// No key - pass through to Buildkite for native matrix handling
162+
newGroup.Steps = append(newGroup.Steps, step)
163+
continue
164+
}
165+
166+
// Parse matrix configuration
167+
cfg, err := parseMatrixConfig(matrixDef)
168+
if err != nil {
169+
return nil, nil, fmt.Errorf("parse matrix in step %q: %w", stepKey(step), err)
170+
}
171+
172+
// Validate label has placeholder
173+
if label, ok := step["label"].(string); ok {
174+
if !hasMatrixPlaceholder(label) {
175+
return nil, nil, fmt.Errorf("matrix step %q: label must contain {{matrix...}} placeholder", baseKey)
176+
}
177+
}
178+
179+
// Register for selector expansion
180+
ctx.registry[baseKey] = cfg
181+
182+
instances := cfg.expand()
183+
if len(instances) == 0 {
184+
return nil, nil, fmt.Errorf("matrix step %q: no instances after expansion", baseKey)
185+
}
186+
187+
var expandedKeysList []string
188+
for _, inst := range instances {
189+
expandedStep := inst.substituteValues(step).(map[string]any)
190+
191+
expandedKey := inst.generateKey(baseKey, cfg)
192+
if _, hasName := expandedStep["name"]; hasName {
193+
expandedStep["name"] = expandedKey
194+
} else {
195+
expandedStep["key"] = expandedKey
196+
}
197+
delete(expandedStep, "matrix")
198+
199+
originalTags := stepTags(step)
200+
matrixTags := inst.generateTags()
201+
allTags := append([]string{}, originalTags...)
202+
allTags = append(allTags, matrixTags...)
203+
if len(allTags) > 0 {
204+
expandedStep["tags"] = allTags
205+
}
206+
207+
expandedKeysList = append(expandedKeysList, expandedKey)
208+
newGroup.Steps = append(newGroup.Steps, expandedStep)
209+
}
210+
211+
ctx.expandedKeys[baseKey] = expandedKeysList
212+
}
213+
214+
result = append(result, newGroup)
215+
}
216+
217+
return result, ctx, nil
218+
}
219+
220+
// expandDependsOnSelectors processes depends_on to expand matrix selectors.
221+
func expandDependsOnSelectors(dependsOn any, ctx *matrixExpansionContext) ([]string, error) {
222+
selectors, err := parseMatrixDependsOn(dependsOn)
223+
if err != nil {
224+
return nil, err
225+
}
226+
227+
var result []string
228+
for _, sel := range selectors {
229+
if sel.Matrix == nil {
230+
// Simple key reference - check if it's a matrix step
231+
if expanded, ok := ctx.expandedKeys[sel.Key]; ok {
232+
// Matrix step: expand to all expanded keys
233+
result = append(result, expanded...)
234+
} else {
235+
// Non-matrix step: use key as-is
236+
result = append(result, sel.Key)
237+
}
238+
} else {
239+
matches, err := sel.expand(ctx.registry, ctx.expandedKeys)
240+
if err != nil {
241+
return nil, err
242+
}
243+
result = append(result, matches...)
244+
}
245+
}
246+
247+
return result, nil
248+
}
249+
118250
func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
119251
[]*bkPipelineGroup, error,
120252
) {
253+
expandedGroups, matrixCtx, err := expandMatrixInGroups(gs)
254+
if err != nil {
255+
return nil, fmt.Errorf("expand matrix: %w", err)
256+
}
257+
121258
set := newStepNodeSet()
122259
var groupNodes []*stepNode
123260

124-
for i, g := range gs {
261+
for i, g := range expandedGroups {
125262
groupNode := &stepNode{
126263
id: fmt.Sprintf("g%d", i),
127264
key: g.Key,
@@ -169,8 +306,20 @@ func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
169306
for _, step := range groupNode.subSteps {
170307
// Track step dependencies.
171308
if dependsOn, ok := step.src["depends_on"]; ok {
172-
deps := toStringList(dependsOn)
173-
for _, dep := range deps {
309+
// Expand matrix selectors in depends_on
310+
expandedDeps, err := expandDependsOnSelectors(dependsOn, matrixCtx)
311+
if err != nil {
312+
return nil, fmt.Errorf("expand depends_on for step %q: %w", step.key, err)
313+
}
314+
315+
// Update the step source with expanded deps for Buildkite output
316+
if len(expandedDeps) == 1 {
317+
step.src["depends_on"] = expandedDeps[0]
318+
} else {
319+
step.src["depends_on"] = expandedDeps
320+
}
321+
322+
for _, dep := range expandedDeps {
174323
if depNode, ok := set.byKey(dep); ok {
175324
set.addDep(step.id, depNode.id)
176325
}

0 commit comments

Comments
 (0)