@@ -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 , ctx .stepKeyToExpanded )
234+ if err != nil {
235+ return nil , err
236+ }
237+ result = append (result , matches ... )
238+ }
239+ }
240+
241+ return result , nil
242+ }
243+
118244func (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