package ciconfig import ( "fmt" "sort" "strings" "github.com/bmatcuk/doublestar/v4" "bugabinga.net/luci/internal/storage" ) const MaxMatrixChildren = 64 func (cfg Config) Job(name string) (Job, bool) { for _, job := range cfg.Jobs { if job.Name == name { return job, true } } return Job{}, false } func (job Job) NeedsChangedPaths() bool { return job.Trigger.Push != nil && (len(job.Trigger.Push.Include) > 0 || len(job.Trigger.Push.Exclude) > 0) } func (job Job) MatchesPush(ref string, changedPaths []string) bool { push := job.Trigger.Push if push == nil { return false } if branch, ok := strings.CutPrefix(ref, "refs/heads/"); ok { if len(push.Branches) == 0 && len(push.Tags) > 0 { return false } if !matchesAny(push.Branches, branch) { return false } } else if tag, ok := strings.CutPrefix(ref, "refs/tags/"); ok { if len(push.Tags) == 0 && len(push.Branches) > 0 { return false } if !matchesAny(push.Tags, tag) { return false } } else { return false } if len(push.Include) == 0 && len(push.Exclude) == 0 { return true } for _, changed := range changedPaths { if matchesAny(push.Include, changed) && !matchesAnyNonempty(push.Exclude, changed) { return true } } return false } func matchesAny(patterns []string, value string) bool { if len(patterns) == 0 { return true } return matchesAnyNonempty(patterns, value) } func matchesAnyNonempty(patterns []string, value string) bool { for _, pattern := range patterns { matched, _ := doublestar.Match(pattern, value) if matched { return true } } return false } func validateGlobs(patterns []string) error { for _, pattern := range patterns { if strings.TrimSpace(pattern) == "" || !doublestar.ValidatePattern(pattern) { return fmt.Errorf("invalid glob %q", pattern) } } return nil } func (job Job) MatrixSize() int { size := 1 for _, axis := range job.Matrix.Axes { if len(axis.Values) == 0 || size > MaxMatrixChildren/len(axis.Values) { return MaxMatrixChildren + 1 } size *= len(axis.Values) } return size } func (job Job) ExpandMatrix() []ExpandedJob { if len(job.Matrix.Axes) == 0 { return []ExpandedJob{{Job: job, Name: job.Name, Values: map[string]string{}}} } var out []ExpandedJob var walk func(int, map[string]string) walk = func(axisIndex int, values map[string]string) { if axisIndex == len(job.Matrix.Axes) { copyValues := make(map[string]string, len(values)) for key, value := range values { copyValues[key] = value } out = append(out, ExpandedJob{Job: job, Name: matrixName(job.Name, job.Matrix.Axes, copyValues), Values: copyValues}) return } axis := job.Matrix.Axes[axisIndex] for _, value := range axis.Values { values[axis.Name] = value walk(axisIndex+1, values) } delete(values, axis.Name) } walk(0, map[string]string{}) return out } func matrixName(name string, axes []Axis, values map[string]string) string { parts := make([]string, 0, len(axes)) for _, axis := range axes { parts = append(parts, axis.Name+"="+values[axis.Name]) } return name + "[" + strings.Join(parts, ",") + "]" } // Interpolate resolves matrix placeholders exactly as execution does. func Interpolate(text string, values map[string]string) string { keys := make([]string, 0, len(values)) for key := range values { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { text = strings.ReplaceAll(text, "${"+key+"}", values[key]) } return text } func (job Job) SecretPaths(dataDir string, repo string) (map[string]string, error) { layout := storage.New(dataDir) paths := make(map[string]string, len(job.Secrets)) for _, secret := range job.Secrets { path, err := layout.SecretPath(repo, job.Name, secret.Name) if err != nil { return nil, err } paths[secret.Name] = path } return paths, nil }