Skip to content

Commit 700f6d9

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 700f6d9

File tree

3 files changed

+499
-5
lines changed

3 files changed

+499
-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: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,164 @@ 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+
// Create meta wait-step with original key.
219+
// Note: The meta-step intentionally has no tags. It is included in
220+
// filtered output only via dependency propagation - when a downstream
221+
// step depends on the base key, the meta-step (and all its expanded
222+
// step dependencies) will be pulled in. If no downstream step depends
223+
// on the base key, the meta-step is excluded, allowing tag filters to
224+
// select only specific matrix variants.
225+
metaStep := map[string]any{
226+
"wait": nil,
227+
"key": baseKey,
228+
"depends_on": expandedKeysList,
229+
}
230+
newGroup.Steps = append(newGroup.Steps, metaStep)
231+
}
232+
233+
result = append(result, newGroup)
234+
}
235+
236+
return result, ctx, nil
237+
}
238+
239+
// expandDependsOnSelectors processes depends_on to expand matrix selectors.
240+
func expandDependsOnSelectors(dependsOn any, ctx *matrixExpansionContext) ([]string, error) {
241+
selectors, err := parseMatrixDependsOn(dependsOn)
242+
if err != nil {
243+
return nil, err
244+
}
245+
246+
var result []string
247+
for _, sel := range selectors {
248+
if sel.Matrix == nil {
249+
// Simple key reference
250+
result = append(result, sel.Key)
251+
} else {
252+
// Selector with matrix filter - expand to matching keys
253+
matches, err := expandMatrixSelector(sel, ctx.registry, ctx.expandedKeys)
254+
if err != nil {
255+
return nil, err
256+
}
257+
result = append(result, matches...)
258+
}
259+
}
260+
261+
return result, nil
262+
}
263+
118264
func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
119265
[]*bkPipelineGroup, error,
120266
) {
267+
expandedGroups, matrixCtx, err := expandMatrixInGroups(gs)
268+
if err != nil {
269+
return nil, fmt.Errorf("expand matrix: %w", err)
270+
}
271+
121272
set := newStepNodeSet()
122273
var groupNodes []*stepNode
123274

124-
for i, g := range gs {
275+
for i, g := range expandedGroups {
125276
groupNode := &stepNode{
126277
id: fmt.Sprintf("g%d", i),
127278
key: g.Key,
@@ -169,8 +320,20 @@ func (c *converter) convertGroups(gs []*pipelineGroup, filter *stepFilter) (
169320
for _, step := range groupNode.subSteps {
170321
// Track step dependencies.
171322
if dependsOn, ok := step.src["depends_on"]; ok {
172-
deps := toStringList(dependsOn)
173-
for _, dep := range deps {
323+
// Expand matrix selectors in depends_on
324+
expandedDeps, err := expandDependsOnSelectors(dependsOn, matrixCtx)
325+
if err != nil {
326+
return nil, fmt.Errorf("expand depends_on for step %q: %w", step.key, err)
327+
}
328+
329+
// Update the step source with expanded deps for Buildkite output
330+
if len(expandedDeps) == 1 {
331+
step.src["depends_on"] = expandedDeps[0]
332+
} else {
333+
step.src["depends_on"] = expandedDeps
334+
}
335+
336+
for _, dep := range expandedDeps {
174337
if depNode, ok := set.byKey(dep); ok {
175338
set.addDep(step.id, depNode.id)
176339
}

0 commit comments

Comments
 (0)