tools/issue-labeler/labeler/labels.go (133 lines of code) (raw):

package labeler import ( "context" "fmt" "regexp" "sort" "strings" _ "embed" "github.com/golang/glog" "github.com/google/go-github/v61/github" "gopkg.in/yaml.v2" ) var sectionRegexp = regexp.MustCompile(`### (New or )?Affected Resource\(s\)[^#]+`) var commentRegexp = regexp.MustCompile(`<!--.*?-->`) var resourceRegexp = regexp.MustCompile(`google_[\w*.]+`) var ( //go:embed enrolled_teams.yml EnrolledTeamsYaml []byte ) type LabelData struct { Team string `yaml:"team,omitempty"` Resources []string `yaml:"resources"` } type RegexpLabel struct { Regexp *regexp.Regexp Label string } type LabelChange struct { Name string Color string IsNew bool NeedsUpdate bool } func BuildRegexLabels(teamsYaml []byte) ([]RegexpLabel, error) { enrolledTeams := make(map[string]LabelData) regexpLabels := []RegexpLabel{} if err := yaml.Unmarshal(teamsYaml, &enrolledTeams); err != nil { return regexpLabels, fmt.Errorf("unmarshalling enrolled teams yaml: %w", err) } for label, data := range enrolledTeams { for _, resource := range data.Resources { exactResource := fmt.Sprintf("^%s$", resource) regexpLabels = append(regexpLabels, RegexpLabel{ Regexp: regexp.MustCompile(exactResource), Label: label, }) } } sort.Slice(regexpLabels, func(i, j int) bool { return regexpLabels[i].Label < regexpLabels[j].Label }) return regexpLabels, nil } func ExtractAffectedResources(body string) []string { section := sectionRegexp.FindString(body) section = commentRegexp.ReplaceAllString(section, "") if section != "" { return resourceRegexp.FindAllString(section, -1) } return []string{} } func ComputeLabels(resources []string, regexpLabels []RegexpLabel) []string { labelSet := make(map[string]struct{}) for _, resource := range resources { for _, rl := range regexpLabels { if rl.Regexp.MatchString(resource) { glog.Infof("found resource %q, applying label %q", resource, rl.Label) labelSet[rl.Label] = struct{}{} break } } } labels := []string{} for label := range labelSet { labels = append(labels, label) } sort.Strings(labels) return labels } // EnsureLabelsWithColor applies the computed changes using the GitHub API func EnsureLabelsWithColor(repository string, labelNames []string, color string) error { client := newGitHubClient() ctx := context.Background() owner, repo, err := splitRepository(repository) if err != nil { return fmt.Errorf("invalid repository format: %w", err) } // Get all existing labels first existingLabels, err := listLabels(repository) if err != nil { return fmt.Errorf("failed to list existing labels: %w", err) } changes := ComputeLabelChanges(existingLabels, labelNames, color) for _, change := range changes { if change.IsNew { _, _, err := client.Issues.CreateLabel(ctx, owner, repo, &github.Label{ Name: &change.Name, Color: &change.Color, }) if err != nil { return fmt.Errorf("failed to create label %s: %w", change.Name, err) } } else if change.NeedsUpdate { _, _, err := client.Issues.EditLabel(ctx, owner, repo, change.Name, &github.Label{ Color: &change.Color, }) if err != nil { return fmt.Errorf("failed to update label %s color: %w", change.Name, err) } } } return nil } // ComputeLabelChanges determines which labels need to be created or updated func ComputeLabelChanges(existingLabels []*github.Label, desiredLabels []string, desiredColor string) []LabelChange { labelMap := make(map[string]*github.Label) for _, label := range existingLabels { labelMap[label.GetName()] = label } changes := make([]LabelChange, 0, len(desiredLabels)) desiredColor = strings.ToUpper(desiredColor) for _, labelName := range desiredLabels { change := LabelChange{ Name: labelName, Color: desiredColor, } if existingLabel, exists := labelMap[labelName]; exists { change.IsNew = false change.NeedsUpdate = strings.ToUpper(existingLabel.GetColor()) != desiredColor } else { change.IsNew = true change.NeedsUpdate = false } changes = append(changes, change) } return changes }