Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/ciconfig/match.go

Raw
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
}