package workflow import ( "fmt" "sort" "strings" ) // combo is one set of matrix values, which becomes one job. Chapter 15A. type combo map[string]string // expand turns a matrix into the combinations it names, in a stable order. func expand(m map[string]any) ([]combo, []string) { var notes []string keys := make([]string, 0, len(m)) for k := range m { // include and exclude are not axes, they change the product the axes make. if k == "include" || k == "exclude" { continue } keys = append(keys, k) } sort.Strings(keys) // One empty combination, widened by each axis in turn, which is the product. out := []combo{{}} for _, k := range keys { values := stringList(m[k]) if len(values) == 0 { notes = append(notes, "matrix axis "+k+" has no values barerepo can read") continue } var next []combo for _, c := range out { for _, v := range values { widened := combo{} for ck, cv := range c { widened[ck] = cv } widened[k] = v next = append(next, widened) } } out = next } out = applyExclude(out, m["exclude"]) if _, has := m["include"]; has { // include can add keys and whole combinations, and guessing at it would run the wrong builds. notes = append(notes, "matrix include is not applied, so only the listed combinations run") } if len(out) == 1 && len(out[0]) == 0 { return nil, notes } if len(out) == 0 { // The axes made combinations and exclude removed every one, so there is nothing to build. notes = append(notes, "excludes every combination its matrix makes") } return out, notes } // applyExclude drops the combinations a matrix said not to build. func applyExclude(in []combo, raw any) []combo { rules, ok := raw.([]any) if !ok { return in } out := in[:0] for _, c := range in { if !excluded(c, rules) { out = append(out, c) } } return out } // excluded reports whether one combination matches any exclude rule, which needs every key to match. func excluded(c combo, rules []any) bool { for _, raw := range rules { rule, ok := raw.(map[string]any) if !ok || len(rule) == 0 { continue } all := true for k, v := range rule { if c[k] != scalar(v) { all = false break } } if all { return true } } return false } // suffix names one combination, so two jobs from one matrix are told apart in the runs list. func (c combo) suffix() string { if len(c) == 0 { return "" } keys := make([]string, 0, len(c)) for k := range c { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { // The axis is named, because a reader of the runs list cannot guess which 1.26 this is. parts = append(parts, k+" "+c[k]) } return " (" + strings.Join(parts, ", ") + ")" } // fill replaces the matrix expressions in one string, which is how runs-on and a run step get values. func (c combo) fill(s string) string { if len(c) == 0 || !strings.Contains(s, "${{") { return s } var b strings.Builder rest := s for { before, after, found := strings.Cut(rest, "${{") if !found { b.WriteString(rest) return b.String() } b.WriteString(before) inner, tail, closed := strings.Cut(after, "}}") if !closed { b.WriteString("${{") b.WriteString(after) return b.String() } name := strings.TrimSpace(inner) if key, ok := strings.CutPrefix(name, "matrix."); ok { if v, known := c[key]; known { b.WriteString(v) rest = tail continue } } b.WriteString("${{" + inner + "}}") rest = tail } } // vars exports the combination, so a step reading $MATRIX_OS works the way a step reading env does. func (c combo) vars() string { if len(c) == 0 { return "" } keys := make([]string, 0, len(c)) for k := range c { keys = append(keys, k) } sort.Strings(keys) var b strings.Builder for _, k := range keys { fmt.Fprintf(&b, "export MATRIX_%s=%s\n", envName(k), shellQuote(c[k])) } return b.String() } // envName turns a matrix key into a shell name, since go-version is not a usable variable. func envName(k string) string { var b strings.Builder for _, r := range strings.ToUpper(k) { switch { case r >= 'A' && r <= 'Z', r >= '0' && r <= '9': b.WriteRune(r) default: b.WriteByte('_') } } return b.String() }