Skip to content

Commit a9a0117

Browse files
feat(raycicmd): integrate matrix expansion into pipeline conversion
Wire matrix expansion into the step conversion flow. Matrix steps are expanded inline during conversion, with a meta wait-step created to preserve the original key for backwards-compatible depends_on references. ## Auto-Generated Tags Each expanded step receives auto-generated tags based on matrix values: python-3.11, cuda-12.1.1 These tags integrate with rayci's tag filtering system, allowing selective runs like "only run python-3.11 tests" via tag rules. ## Meta-Step Pattern The meta wait-step preserves backwards compatibility: - Key: original step key (e.g., "ray-build") - Type: wait step - depends_on: all expanded step keys This allows existing `depends_on: ray-build` references to wait for ALL matrix instances without modification. ## Filtering Behavior When tag filtering is applied: - Only matching expanded steps are included - If a step depends on the meta-step, ALL instances are pulled in - If a step uses selector syntax, only matching instances are included - If all instances are filtered out, the meta-step is also excluded Topic: matrix-converter Relative: matrix-expand Signed-off-by: andrew <andrew@anyscale.com>
1 parent 4ce2802 commit a9a0117

File tree

3 files changed

+475
-5
lines changed

3 files changed

+475
-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: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,144 @@ func stepTags(step map[string]any) []string {
115115
return nil
116116
}
117117

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

124-
for i, g := range gs {
255+
for i, g := range expandedGroups {
125256
groupNode := &stepNode{
126257
id: fmt.Sprintf("g%d", i),
127258
key: g.Key,
@@ -169,8 +300,20 @@ func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
169300
for _, step := range groupNode.subSteps {
170301
// Track step dependencies.
171302
if dependsOn, ok := step.src["depends_on"]; ok {
172-
deps := toStringList(dependsOn)
173-
for _, dep := range deps {
303+
// Expand matrix selectors in depends_on
304+
expandedDeps, err := expandDependsOnSelectors(dependsOn, matrixCtx)
305+
if err != nil {
306+
return nil, fmt.Errorf("expand depends_on for step %q: %w", step.key, err)
307+
}
308+
309+
// Update the step source with expanded deps for Buildkite output
310+
if len(expandedDeps) == 1 {
311+
step.src["depends_on"] = expandedDeps[0]
312+
} else {
313+
step.src["depends_on"] = expandedDeps
314+
}
315+
316+
for _, dep := range expandedDeps {
174317
if depNode, ok := set.byKey(dep); ok {
175318
set.addDep(step.id, depNode.id)
176319
}

0 commit comments

Comments
 (0)