Skip to content

Commit 18a25db

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 b488745 commit 18a25db

File tree

3 files changed

+483
-5
lines changed

3 files changed

+483
-5
lines changed

.buildkite/matrix-test.rayci.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
tags:
33+
- always
34+
depends_on:
35+
- key: matrix-step
36+
matrix:
37+
variant: ["selected-1", "selected-2"]
38+
- forge

raycicmd/converter.go

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,156 @@ 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+
// Expand matrix into individual steps
183+
instances := expandMatrix(cfg)
184+
if len(instances) == 0 {
185+
return nil, nil, fmt.Errorf("matrix step %q: no instances after expansion", baseKey)
186+
}
187+
188+
var expandedKeysList []string
189+
for _, inst := range instances {
190+
// Deep copy and substitute
191+
expandedStep := deepCopyStepMap(step)
192+
expandedStep = substituteMatrixValues(expandedStep, inst.Values).(map[string]any)
193+
194+
// Generate expanded key and update name/key fields
195+
expandedKey := generateMatrixInstanceKey(baseKey, cfg, inst.Values)
196+
if _, hasName := expandedStep["name"]; hasName {
197+
expandedStep["name"] = expandedKey
198+
} else {
199+
expandedStep["key"] = expandedKey
200+
}
201+
delete(expandedStep, "matrix")
202+
203+
// Add auto-generated tags (inherit original + add matrix tags)
204+
originalTags := stepTags(step)
205+
matrixTags := generateMatrixTags(inst.Values)
206+
allTags := append([]string{}, originalTags...)
207+
allTags = append(allTags, matrixTags...)
208+
if len(allTags) > 0 {
209+
expandedStep["tags"] = allTags
210+
}
211+
212+
expandedKeysList = append(expandedKeysList, expandedKey)
213+
newGroup.Steps = append(newGroup.Steps, expandedStep)
214+
}
215+
216+
ctx.expandedKeys[baseKey] = expandedKeysList
217+
}
218+
219+
result = append(result, newGroup)
220+
}
221+
222+
return result, ctx, nil
223+
}
224+
225+
// expandDependsOnSelectors processes depends_on to expand matrix selectors.
226+
func expandDependsOnSelectors(dependsOn any, ctx *matrixExpansionContext) ([]string, error) {
227+
selectors, err := parseMatrixDependsOn(dependsOn)
228+
if err != nil {
229+
return nil, err
230+
}
231+
232+
var result []string
233+
for _, sel := range selectors {
234+
if sel.Matrix == nil {
235+
// Simple key reference - check if it's a matrix step
236+
if expanded, ok := ctx.expandedKeys[sel.Key]; ok {
237+
// Matrix step: expand to all expanded keys
238+
result = append(result, expanded...)
239+
} else {
240+
// Non-matrix step: use key as-is
241+
result = append(result, sel.Key)
242+
}
243+
} else {
244+
// Selector with matrix filter - expand to matching keys
245+
matches, err := expandMatrixSelector(sel, ctx.registry, ctx.expandedKeys)
246+
if err != nil {
247+
return nil, err
248+
}
249+
result = append(result, matches...)
250+
}
251+
}
252+
253+
return result, nil
254+
}
255+
118256
func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
119257
[]*bkPipelineGroup, error,
120258
) {
259+
expandedGroups, matrixCtx, err := expandMatrixInGroups(gs)
260+
if err != nil {
261+
return nil, fmt.Errorf("expand matrix: %w", err)
262+
}
263+
121264
set := newStepNodeSet()
122265
var groupNodes []*stepNode
123266

124-
for i, g := range gs {
267+
for i, g := range expandedGroups {
125268
groupNode := &stepNode{
126269
id: fmt.Sprintf("g%d", i),
127270
key: g.Key,
@@ -169,8 +312,20 @@ func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
169312
for _, step := range groupNode.subSteps {
170313
// Track step dependencies.
171314
if dependsOn, ok := step.src["depends_on"]; ok {
172-
deps := toStringList(dependsOn)
173-
for _, dep := range deps {
315+
// Expand matrix selectors in depends_on
316+
expandedDeps, err := expandDependsOnSelectors(dependsOn, matrixCtx)
317+
if err != nil {
318+
return nil, fmt.Errorf("expand depends_on for step %q: %w", step.key, err)
319+
}
320+
321+
// Update the step source with expanded deps for Buildkite output
322+
if len(expandedDeps) == 1 {
323+
step.src["depends_on"] = expandedDeps[0]
324+
} else {
325+
step.src["depends_on"] = expandedDeps
326+
}
327+
328+
for _, dep := range expandedDeps {
174329
if depNode, ok := set.byKey(dep); ok {
175330
set.addDep(step.id, depNode.id)
176331
}

0 commit comments

Comments
 (0)