From 4ea95dcb293019e8d53f4850b48f7577830efc97 Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Tue, 21 Nov 2023 02:53:40 -0800 Subject: [PATCH] [chore] generate issue templates with githubgen (#28655) Remove a bash script, use go code instead. --------- Co-authored-by: Andrzej Stencel --- .../scripts/add-component-options.sh | 51 --- Makefile | 11 +- cmd/githubgen/codeowners.go | 195 ++++++++++ cmd/githubgen/dependabot.go | 115 ++++++ cmd/githubgen/issuetemplates.go | 66 ++++ cmd/githubgen/main.go | 336 +++--------------- 6 files changed, 430 insertions(+), 344 deletions(-) delete mode 100755 .github/workflows/scripts/add-component-options.sh create mode 100644 cmd/githubgen/codeowners.go create mode 100644 cmd/githubgen/dependabot.go create mode 100644 cmd/githubgen/issuetemplates.go diff --git a/.github/workflows/scripts/add-component-options.sh b/.github/workflows/scripts/add-component-options.sh deleted file mode 100755 index 372d72024c5f..000000000000 --- a/.github/workflows/scripts/add-component-options.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env sh -# -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 -# -# Takes the list of components from the CODEOWNERS file and inserts them -# as a YAML list in a GitHub issue template, then prints out the resulting -# contents. -# -# Note that this is script is intended to be POSIX-compliant since it is -# intended to also be called from the Makefile on developer machines, -# which aren't guaranteed to have Bash or a GNU userland installed. - -if [ -z "${FILE}" ]; then - echo 'FILE is empty, please ensure it is set.' - exit 1 -fi - -CUR_DIRECTORY=$(dirname "$0") - -# Get the line number for text within a file -get_line_number() { - text=$1 - file=$2 - - grep -n "${text}" "${file}" | awk '{ print $1 }' | grep -oE '[0-9]+' -} - -LABELS="" - -START_LINE=$(get_line_number '# Start Collector components list' "${FILE}") -END_LINE=$(get_line_number '# End Collector components list' "${FILE}") -TOTAL_LINES=$(wc -l "${FILE}" | awk '{ print $1 }') - -head -n "${START_LINE}" "${FILE}" -for COMPONENT in $(sh "${CUR_DIRECTORY}/get-components.sh"); do - TYPE=$(echo "${COMPONENT}" | cut -f1 -d'/') - REST=$(echo "${COMPONENT}" | cut -f2- -d'/' | sed "s%${TYPE}/%/%" | sed "s%${TYPE}\$%%") - LABEL="" - - if [ -z "${TYPE}" ] | [ -z "${REST}" ]; then - LABEL="${COMPONENT}" - else - LABEL="${TYPE}/${REST}" - fi - - LABELS="${LABELS}${LABEL}\n" -done -printf "${LABELS}" | sort | awk '{ printf " - %s\n",$1 }' -tail -n $((TOTAL_LINES-END_LINE+1)) "${FILE}" - diff --git a/Makefile b/Makefile index e9737e3a5171..e6c40f8d8f0b 100644 --- a/Makefile +++ b/Makefile @@ -146,7 +146,7 @@ DEPENDABOT_PATH=".github/dependabot.yml" .PHONY: gendependabot gendependabot: cd cmd/githubgen && $(GOCMD) install . - githubgen -dependabot + githubgen dependabot # Define a delegation target for each module @@ -398,10 +398,5 @@ genconfigdocs: .PHONY: generate-gh-issue-templates generate-gh-issue-templates: - for FILE in bug_report feature_request other; do \ - YAML_FILE=".github/ISSUE_TEMPLATE/$${FILE}.yaml"; \ - TMP_FILE=".github/ISSUE_TEMPLATE/$${FILE}.yaml.tmp"; \ - cat "$${YAML_FILE}" > "$${TMP_FILE}"; \ - FILE="$${TMP_FILE}" ./.github/workflows/scripts/add-component-options.sh > "$${YAML_FILE}"; \ - rm "$${TMP_FILE}"; \ - done + cd cmd/githubgen && $(GOCMD) install . + githubgen issue-templates diff --git a/cmd/githubgen/codeowners.go b/cmd/githubgen/codeowners.go new file mode 100644 index 000000000000..f53c64b91ca0 --- /dev/null +++ b/cmd/githubgen/codeowners.go @@ -0,0 +1,195 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/google/go-github/v53/github" +) + +const allowlistHeader = `# Code generated by githubgen. DO NOT EDIT. +##################################################### +# +# List of components in OpenTelemetry Collector Contrib +# waiting on owners to be assigned +# +##################################################### +# +# Learn about membership in OpenTelemetry community: +# https://github.com/open-telemetry/community/blob/main/community-membership.md +# +# +# Learn about CODEOWNERS file format: +# https://help.github.com/en/articles/about-code-owners +# + +## +# NOTE: New components MUST have a codeowner. Add new components to the CODEOWNERS file instead of here. +## + +## COMMON & SHARED components +internal/common + +` + +const unmaintainedHeader = ` + +## UNMAINTAINED components +## The Github issue template generation code needs this to generate the corresponding labels. + +` + +const codeownersHeader = `# Code generated by githubgen. DO NOT EDIT. +##################################################### +# +# List of approvers for OpenTelemetry Collector Contrib +# +##################################################### +# +# Learn about membership in OpenTelemetry community: +# https://github.com/open-telemetry/community/blob/main/community-membership.md +# +# +# Learn about CODEOWNERS file format: +# https://help.github.com/en/articles/about-code-owners +# +# NOTE: Lines should be entered in the following format: +# /.. +# extension/oauth2clientauthextension/ @open-telemetry/collector-contrib-approvers @pavankrish123 @jpkrohling +# Path separator and minimum of 1 space between component path and owners is +# important for validation steps +# + +* @open-telemetry/collector-contrib-approvers +` + +type codeownersGenerator struct { +} + +func (cg codeownersGenerator) generate(data *githubData) error { + allowlistData, err := os.ReadFile(data.allowlistFilePath) + if err != nil { + return err + } + allowlistLines := strings.Split(string(allowlistData), "\n") + + allowlist := make(map[string]struct{}, len(allowlistLines)) + for _, line := range allowlistLines { + allowlist[line] = struct{}{} + } + var missingCodeowners []string + var duplicateCodeowners []string + members, err := getGithubMembers() + if err != nil { + return err + } + for _, codeowner := range data.codeowners { + _, present := members[codeowner] + + if !present { + _, allowed := allowlist[codeowner] + allowed = allowed || strings.HasPrefix(codeowner, "open-telemetry/") + if !allowed { + missingCodeowners = append(missingCodeowners, codeowner) + } + } else if _, ok := allowlist[codeowner]; ok { + duplicateCodeowners = append(duplicateCodeowners, codeowner) + } + } + if len(missingCodeowners) > 0 { + sort.Strings(missingCodeowners) + return fmt.Errorf("codeowners are not members: %s", strings.Join(missingCodeowners, ", ")) + } + if len(duplicateCodeowners) > 0 { + sort.Strings(duplicateCodeowners) + return fmt.Errorf("codeowners members duplicate in allowlist: %s", strings.Join(duplicateCodeowners, ", ")) + } + + codeowners := codeownersHeader + deprecatedList := "## DEPRECATED components\n" + unmaintainedList := "\n## UNMAINTAINED components\n" + + unmaintainedCodeowners := unmaintainedHeader + currentFirstSegment := "" +LOOP: + for _, key := range data.folders { + m := data.components[key] + for stability := range m.Status.Stability { + if stability == unmaintainedStatus { + unmaintainedList += key + "/\n" + unmaintainedCodeowners += fmt.Sprintf("%s/%s @open-telemetry/collector-contrib-approvers \n", key, strings.Repeat(" ", data.maxLength-len(key))) + continue LOOP + } + if stability == "deprecated" && (m.Status.Codeowners == nil || len(m.Status.Codeowners.Active) == 0) { + deprecatedList += key + "/\n" + } + } + + if m.Status.Codeowners != nil { + parts := strings.Split(key, string(os.PathSeparator)) + firstSegment := parts[0] + if firstSegment != currentFirstSegment { + currentFirstSegment = firstSegment + codeowners += "\n" + } + owners := "" + for _, owner := range m.Status.Codeowners.Active { + owners += " " + owners += "@" + owner + } + codeowners += fmt.Sprintf("%s/%s @open-telemetry/collector-contrib-approvers%s\n", key, strings.Repeat(" ", data.maxLength-len(key)), owners) + } + } + + err = os.WriteFile(filepath.Join(".github", "CODEOWNERS"), []byte(codeowners+unmaintainedCodeowners), 0600) + if err != nil { + return err + } + err = os.WriteFile(filepath.Join(".github", "ALLOWLIST"), []byte(allowlistHeader+deprecatedList+unmaintainedList), 0600) + if err != nil { + return err + } + return nil +} + +func getGithubMembers() (map[string]struct{}, error) { + githubToken := os.Getenv("GITHUB_TOKEN") + if githubToken == "" { + return nil, fmt.Errorf("Set the environment variable `GITHUB_TOKEN` to a PAT token to authenticate") + } + client := github.NewTokenClient(context.Background(), githubToken) + var allUsers []*github.User + pageIndex := 0 + for { + users, resp, err := client.Organizations.ListMembers(context.Background(), "open-telemetry", + &github.ListMembersOptions{ + PublicOnly: false, + ListOptions: github.ListOptions{ + PerPage: 50, + Page: pageIndex, + }, + }, + ) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if len(users) == 0 { + break + } + allUsers = append(allUsers, users...) + pageIndex++ + } + + usernames := make(map[string]struct{}, len(allUsers)) + for _, u := range allUsers { + usernames[*u.Login] = struct{}{} + } + return usernames, nil +} diff --git a/cmd/githubgen/dependabot.go b/cmd/githubgen/dependabot.go new file mode 100644 index 000000000000..b5ad2eb8d111 --- /dev/null +++ b/cmd/githubgen/dependabot.go @@ -0,0 +1,115 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "gopkg.in/yaml.v3" +) + +const ( + maxDependabotUpdates = 220 + dependabotHeader = `# File generated by "make gendependabot"; DO NOT EDIT. + +` +) + +type dependabotSchedule struct { + Interval yaml.Node `yaml:"interval"` + Day yaml.Node `yaml:"day"` +} + +type dependabotUpdate struct { + PackageEcosystem yaml.Node `yaml:"package-ecosystem"` + Directory yaml.Node `yaml:"directory"` + Schedule dependabotSchedule `yaml:"schedule"` + Priority int `yaml:"-"` +} + +type dependabotData struct { + Version int `yaml:"version"` + Updates []dependabotUpdate `yaml:"updates"` +} + +func makePriority(status *Status) int { + // not an internal component such as pkg/**, and no distributions: + if (status.Class == "receiver" || status.Class == "processor" || status.Class == "exporter" || status.Class == "connector" || status.Class == "extension" || status.Class == "cmd") && + len(status.Distributions) == 0 && status.Class != "" { + return 1 + } + // start with a score of 2 + maxScore := 2 + for stability := range status.Stability { + switch stability { + case "deprecated": // stay with 2 + case "unmaintained": + return 1 + case "alpha": + if maxScore < 3 { + maxScore = 3 + } + case "beta": + if maxScore < 4 { + maxScore = 4 + } + case "stable": + if maxScore < 5 { + maxScore = 5 + } + } + } + return maxScore +} + +func newDependabotUpdate(directory string, priority int) dependabotUpdate { + return dependabotUpdate{ + PackageEcosystem: yaml.Node{Value: "gomod", Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, + Directory: yaml.Node{Value: "/" + directory, Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, + Schedule: dependabotSchedule{ + Interval: yaml.Node{Value: "weekly", Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, + Day: yaml.Node{Value: "wednesday", Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, + }, + Priority: priority, + } +} + +type dependabotGenerator struct { +} + +func (dg dependabotGenerator) generate(data *githubData) error { + dependabotData := data.dependabotData + sort.Slice(dependabotData.Updates, func(i, j int) bool { + return dependabotData.Updates[i].Priority > dependabotData.Updates[j].Priority + }) + removed := dependabotData.Updates[maxDependabotUpdates:] + dependabotData.Updates = dependabotData.Updates[:maxDependabotUpdates] + if len(removed) > 0 { + sort.Slice(removed, func(i, j int) bool { + return strings.Compare(removed[i].Directory.Value, removed[j].Directory.Value) < 0 + }) + fmt.Printf("The following modules were not added to Dependabot to keep within limits of %d\n", maxDependabotUpdates) + for _, update := range removed { + fmt.Printf(" - %q\n", update.Directory.Value) + } + } + + sort.Slice(dependabotData.Updates, func(i, j int) bool { + return strings.Compare(dependabotData.Updates[i].Directory.Value, dependabotData.Updates[j].Directory.Value) < 0 + }) + + var yamlContents bytes.Buffer + encoder := yaml.NewEncoder(&yamlContents) + encoder.SetIndent(2) + err := encoder.Encode(dependabotData) + if err != nil { + return err + } + err = os.WriteFile(filepath.Join(".github", "dependabot.yml"), append([]byte(dependabotHeader), yamlContents.Bytes()...), 0600) + return err +} diff --git a/cmd/githubgen/issuetemplates.go b/cmd/githubgen/issuetemplates.go new file mode 100644 index 000000000000..91f6ede0534b --- /dev/null +++ b/cmd/githubgen/issuetemplates.go @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "bytes" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +const ( + startComponentList = `# Start Collector components list` + endComponentList = `# End Collector components list` +) + +func folderToShortName(folder string) string { + if folder == "internal/coreinternal" { + return "internal/core" + } + path := strings.Split(folder, "/") + switch path[0] { + case "receiver", "exporter", "extension", "processor", "connector": + path[1] = strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(path[1], "internal"), "extension"), "exporter"), "connector"), "processor"), "receiver") + path[len(path)-1] = strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(path[len(path)-1], "internal"), "extension"), "exporter"), "connector"), "processor"), "receiver") + default: + } + + return strings.Join(path, "/") +} + +type issueTemplatesGenerator struct { +} + +func (itg issueTemplatesGenerator) generate(data *githubData) error { + shortNames := make([]string, len(data.folders)) + for i, f := range data.folders { + shortNames[i] = folderToShortName(f) + } + sort.Strings(shortNames) + replacement := []byte(startComponentList + "\n - " + strings.Join(shortNames, "\n - ") + "\n " + endComponentList) + issuesFolder := filepath.Join(".github", "ISSUE_TEMPLATE") + entries, err := os.ReadDir(issuesFolder) + if err != nil { + return err + } + for _, e := range entries { + templateContents, err := os.ReadFile(filepath.Join(issuesFolder, e.Name())) + if err != nil { + return err + } + matchOldContent := regexp.MustCompile("(?s)" + startComponentList + ".*" + endComponentList) + oldContent := matchOldContent.FindSubmatch(templateContents) + if len(oldContent) > 0 { + templateContents = bytes.ReplaceAll(templateContents, oldContent[0], replacement) + err = os.WriteFile(filepath.Join(issuesFolder, e.Name()), templateContents, 0600) + if err != nil { + return err + } + } + + } + return nil +} diff --git a/cmd/githubgen/main.go b/cmd/githubgen/main.go index 7de47722cd4a..56696d21245a 100644 --- a/cmd/githubgen/main.go +++ b/cmd/githubgen/main.go @@ -4,7 +4,6 @@ package main import ( - "bytes" "context" "flag" "fmt" @@ -13,163 +12,59 @@ import ( "os" "path/filepath" "sort" - "strings" - "gopkg.in/yaml.v3" - - "github.com/google/go-github/v53/github" "go.opentelemetry.io/collector/confmap/provider/fileprovider" ) -const maxDependabotUpdates = 220 - -const codeownersHeader = `# Code generated by githubgen. DO NOT EDIT. -##################################################### -# -# List of approvers for OpenTelemetry Collector Contrib -# -##################################################### -# -# Learn about membership in OpenTelemetry community: -# https://github.com/open-telemetry/community/blob/main/community-membership.md -# -# -# Learn about CODEOWNERS file format: -# https://help.github.com/en/articles/about-code-owners -# -# NOTE: Lines should be entered in the following format: -# /.. -# extension/oauth2clientauthextension/ @open-telemetry/collector-contrib-approvers @pavankrish123 @jpkrohling -# Path separator and minimum of 1 space between component path and owners is -# important for validation steps -# - -* @open-telemetry/collector-contrib-approvers -` - -const unmaintainedHeader = ` - -## UNMAINTAINED components -## The Github issue template generation code needs this to generate the corresponding labels. - -` - -const allowlistHeader = `# Code generated by githubgen. DO NOT EDIT. -##################################################### -# -# List of components in OpenTelemetry Collector Contrib -# waiting on owners to be assigned -# -##################################################### -# -# Learn about membership in OpenTelemetry community: -# https://github.com/open-telemetry/community/blob/main/community-membership.md -# -# -# Learn about CODEOWNERS file format: -# https://help.github.com/en/articles/about-code-owners -# - -## -# NOTE: New components MUST have a codeowner. Add new components to the CODEOWNERS file instead of here. -## - -## COMMON & SHARED components -internal/common - -` - -const dependabotHeader = `# File generated by "make gendependabot"; DO NOT EDIT. - -` - const unmaintainedStatus = "unmaintained" +type generator interface { + generate(data *githubData) error +} + // Generates files specific to Github or Dependabot according to status metadata: // .github/CODEOWNERS // .github/ALLOWLIST // .github/dependabot.yml +// .github/ISSUE_TEMPLATES/*.yaml (list of components) func main() { folder := flag.String("folder", ".", "folder investigated for codeowners") allowlistFilePath := flag.String("allowlist", "cmd/githubgen/allowlist.txt", "path to a file containing an allowlist of members outside the OpenTelemetry organization") - dependabotOnly := flag.Bool("dependabot", false, "only generate dependabot entries") flag.Parse() - if err := run(*folder, *allowlistFilePath, *dependabotOnly); err != nil { - log.Fatal(err) - } -} - -type DependabotSchedule struct { - Interval yaml.Node `yaml:"interval"` - Day yaml.Node `yaml:"day"` -} - -type DependabotUpdate struct { - PackageEcosystem yaml.Node `yaml:"package-ecosystem"` - Directory yaml.Node `yaml:"directory"` - Schedule DependabotSchedule `yaml:"schedule"` - Priority int `yaml:"-"` -} - -type DependabotData struct { - Version int `yaml:"version"` - Updates []DependabotUpdate `yaml:"updates"` -} - -func makePriority(status *Status) int { - // not an internal component such as pkg/**, and no distributions: - if (status.Class == "receiver" || status.Class == "processor" || status.Class == "exporter" || status.Class == "connector" || status.Class == "extension" || status.Class == "cmd") && - len(status.Distributions) == 0 && status.Class != "" { - return 1 - } - // start with a score of 2 - maxScore := 2 - for stability := range status.Stability { - switch stability { - case "deprecated": // stay with 2 - case "unmaintained": - return 1 - case "alpha": - if maxScore < 3 { - maxScore = 3 - } - case "beta": - if maxScore < 4 { - maxScore = 4 - } - case "stable": - if maxScore < 5 { - maxScore = 5 - } + var generators []generator + for _, arg := range flag.Args() { + switch arg { + case "issue-templates": + generators = append(generators, issueTemplatesGenerator{}) + case "dependabot": + generators = append(generators, dependabotGenerator{}) + case "codeowners": + generators = append(generators, codeownersGenerator{}) + default: + panic(fmt.Sprintf("Unknown generator: %s", arg)) } } - return maxScore -} - -func newDependabotUpdate(directory string, priority int) DependabotUpdate { - return DependabotUpdate{ - PackageEcosystem: yaml.Node{Value: "gomod", Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, - Directory: yaml.Node{Value: "/" + directory, Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, - Schedule: DependabotSchedule{ - Interval: yaml.Node{Value: "weekly", Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, - Day: yaml.Node{Value: "wednesday", Style: yaml.DoubleQuotedStyle, Kind: yaml.ScalarNode}, - }, - Priority: priority, + if len(generators) == 0 { + generators = []generator{issueTemplatesGenerator{}, dependabotGenerator{}, codeownersGenerator{}} + } + if err := run(*folder, *allowlistFilePath, generators); err != nil { + log.Fatal(err) } } -type Codeowners struct { +type codeowners struct { // Active codeowners Active []string `mapstructure:"active"` // Emeritus codeowners Emeritus []string `mapstructure:"emeritus"` } + type Status struct { Stability map[string][]string `mapstructure:"stability"` Distributions []string `mapstructure:"distributions"` Class string `mapstructure:"class"` Warnings []string `mapstructure:"warnings"` - Codeowners *Codeowners `mapstructure:"codeowners"` + Codeowners *codeowners `mapstructure:"codeowners"` } type metadata struct { // Type of the component. @@ -180,6 +75,15 @@ type metadata struct { Status *Status `mapstructure:"status"` } +type githubData struct { + folders []string + codeowners []string + allowlistFilePath string + maxLength int + components map[string]metadata + dependabotData *dependabotData +} + func loadMetadata(filePath string) (metadata, error) { cp, err := fileprovider.New().Retrieve(context.Background(), "file:"+filePath, nil) if err != nil { @@ -199,13 +103,13 @@ func loadMetadata(filePath string) (metadata, error) { return md, nil } -func run(folder string, allowlistFilePath string, dependabotOnly bool) error { +func run(folder string, allowlistFilePath string, generators []generator) error { components := map[string]metadata{} var foldersList []string - dependabotData := &DependabotData{ + dependabotData := &dependabotData{ Version: 2, - Updates: []DependabotUpdate{ + Updates: []dependabotUpdate{ newDependabotUpdate("", 5), }, } @@ -221,7 +125,7 @@ func run(folder string, allowlistFilePath string, dependabotOnly bool) error { return nil } currentFolder := filepath.Dir(path) - key := currentFolder + "/" + key := currentFolder components[key] = m foldersList = append(foldersList, key) _, err = os.Stat(filepath.Join(currentFolder, "go.mod")) @@ -249,165 +153,27 @@ func run(folder string, allowlistFilePath string, dependabotOnly bool) error { if err != nil { return err } - if !dependabotOnly { - if err = generateCodeownersFiles(foldersList, allCodeowners, allowlistFilePath, components, maxLength); err != nil { - return err - } - } - - sort.Slice(dependabotData.Updates, func(i, j int) bool { - return dependabotData.Updates[i].Priority > dependabotData.Updates[j].Priority - }) - removed := dependabotData.Updates[maxDependabotUpdates:] - dependabotData.Updates = dependabotData.Updates[:maxDependabotUpdates] - if len(removed) > 0 { - sort.Slice(removed, func(i, j int) bool { - return strings.Compare(removed[i].Directory.Value, removed[j].Directory.Value) < 0 - }) - fmt.Printf("The following modules were not added to Dependabot to keep within limits of %d\n", maxDependabotUpdates) - for _, update := range removed { - fmt.Printf(" - %q\n", update.Directory.Value) - } - } - - sort.Slice(dependabotData.Updates, func(i, j int) bool { - return strings.Compare(dependabotData.Updates[i].Directory.Value, dependabotData.Updates[j].Directory.Value) < 0 - }) - - var yamlContents bytes.Buffer - encoder := yaml.NewEncoder(&yamlContents) - encoder.SetIndent(2) - err = encoder.Encode(dependabotData) - if err != nil { - return err - } - err = os.WriteFile(filepath.Join(".github", "dependabot.yml"), append([]byte(dependabotHeader), yamlContents.Bytes()...), 0600) - if err != nil { - return err - } - - return nil -} -func generateCodeownersFiles(foldersList []string, allCodeowners map[string]struct{}, allowlistFilePath string, components map[string]metadata, maxLength int) error { - allowlistData, err := os.ReadFile(allowlistFilePath) - if err != nil { - return err - } - allowlistLines := strings.Split(string(allowlistData), "\n") - - allowlist := make(map[string]struct{}, len(allowlistLines)) - for _, line := range allowlistLines { - allowlist[line] = struct{}{} - } sort.Strings(foldersList) - var missingCodeowners []string - var duplicateCodeowners []string - members, err := getGithubMembers() - if err != nil { - return err + codeownersList := make([]string, 0, len(allCodeowners)) + for c := range allCodeowners { + codeownersList = append(codeownersList, c) } - for codeowner := range allCodeowners { - _, present := members[codeowner] + sort.Strings(codeownersList) - if !present { - _, allowed := allowlist[codeowner] - allowed = allowed || strings.HasPrefix(codeowner, "open-telemetry/") - if !allowed { - missingCodeowners = append(missingCodeowners, codeowner) - } - } else if _, ok := allowlist[codeowner]; ok { - duplicateCodeowners = append(duplicateCodeowners, codeowner) - } - } - if len(missingCodeowners) > 0 { - sort.Strings(missingCodeowners) - return fmt.Errorf("codeowners are not members: %s", strings.Join(missingCodeowners, ", ")) + data := &githubData{ + folders: foldersList, + codeowners: codeownersList, + allowlistFilePath: allowlistFilePath, + maxLength: maxLength, + components: components, + dependabotData: dependabotData, } - if len(duplicateCodeowners) > 0 { - sort.Strings(duplicateCodeowners) - return fmt.Errorf("codeowners members duplicate in allowlist: %s", strings.Join(duplicateCodeowners, ", ")) - } - - codeowners := codeownersHeader - deprecatedList := "## DEPRECATED components\n" - unmaintainedList := "\n## UNMAINTAINED components\n" - unmaintainedCodeowners := unmaintainedHeader - currentFirstSegment := "" -LOOP: - for _, key := range foldersList { - m := components[key] - for stability := range m.Status.Stability { - if stability == unmaintainedStatus { - unmaintainedList += key + "\n" - unmaintainedCodeowners += fmt.Sprintf("%s%s @open-telemetry/collector-contrib-approvers \n", key, strings.Repeat(" ", maxLength-len(key))) - continue LOOP - } - if stability == "deprecated" && (m.Status.Codeowners == nil || len(m.Status.Codeowners.Active) == 0) { - deprecatedList += key + "\n" - } - } - - if m.Status.Codeowners != nil { - parts := strings.Split(key, string(os.PathSeparator)) - firstSegment := parts[0] - if firstSegment != currentFirstSegment { - currentFirstSegment = firstSegment - codeowners += "\n" - } - owners := "" - for _, owner := range m.Status.Codeowners.Active { - owners += " " - owners += "@" + owner - } - codeowners += fmt.Sprintf("%s%s @open-telemetry/collector-contrib-approvers%s\n", key, strings.Repeat(" ", maxLength-len(key)), owners) + for _, g := range generators { + if err = g.generate(data); err != nil { + return err } } - - err = os.WriteFile(filepath.Join(".github", "CODEOWNERS"), []byte(codeowners+unmaintainedCodeowners), 0600) - if err != nil { - return err - } - err = os.WriteFile(filepath.Join(".github", "ALLOWLIST"), []byte(allowlistHeader+deprecatedList+unmaintainedList), 0600) - if err != nil { - return err - } return nil } - -func getGithubMembers() (map[string]struct{}, error) { - githubToken := os.Getenv("GITHUB_TOKEN") - if githubToken == "" { - return nil, fmt.Errorf("Set the environment variable `GITHUB_TOKEN` to a PAT token to authenticate") - } - client := github.NewTokenClient(context.Background(), githubToken) - var allUsers []*github.User - pageIndex := 0 - for { - users, resp, err := client.Organizations.ListMembers(context.Background(), "open-telemetry", - &github.ListMembersOptions{ - PublicOnly: false, - ListOptions: github.ListOptions{ - PerPage: 50, - Page: pageIndex, - }, - }, - ) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if len(users) == 0 { - break - } - allUsers = append(allUsers, users...) - pageIndex++ - } - - usernames := make(map[string]struct{}, len(allUsers)) - for _, u := range allUsers { - usernames[*u.Login] = struct{}{} - } - return usernames, nil -}