@@ -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+
118250func (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