Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[githubgen] PR feedback follow-ups #655

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .chloggen/githubgen-enhancements.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: 'enhancement'

# The name of the component, or a single word describing the area of concern, (e.g. crosslink)
component: githubgen

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Enhanced githubgen tool with more options to better fit arbitrary repos, added unit tests

# One or more tracking issues related to the change
issues: [655]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
12 changes: 10 additions & 2 deletions githubgen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ $> ./githubgen
The equivalent of:

```shell
$> GITHUB_TOKEN=<mypattoken> githubgen --folder . [--allowlist cmd/githubgen/allowlist.txt]
$> githubgen --skipgithub --folder . --github-org "open-telemetry" \
--pretty-repo-name "OpenTelemetry Collector Contrib" \
--default-codeowner open-telemetry/opentelemetry-collector-approvers \
--allowlist cmd/githubgen/allowlist.txt
```

## Checking codeowners against OpenTelemetry membership via GitHub API
Expand All @@ -26,12 +29,17 @@ To authenticate, set the environment variable `GITHUB_TOKEN` to a PAT token.
If a PAT is not available you can use the `--skipgithub` flag to avoid checking
for membership in the GitHub organization.

Additionally, the GitHub organization name needs to be specified using
`--github-org <your-org>`, along with `--pretty-repo-name` to be filled
into CODEOWNERS files (e.g. "OpenTelemetry Collector Contrib")
and `--default-codeowner` to be used as the default owner for general files.

For each codeowner, the script will check if the user is registered as a member
of the OpenTelemetry organization.

If any codeowner is missing, it will stop and print names of missing codeowners.

These can be added to allowlist.txt as a workaround.

If a codeowner is present in allowlist.txt and also a member of the
If a codeowner is present in `allowlist.txt` and also a member of the
OpenTelemetry organization, the script will error out.
190 changes: 77 additions & 113 deletions githubgen/codeowners.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"

Expand All @@ -15,133 +16,36 @@
"go.opentelemetry.io/build-tools/githubgen/datatype"
)

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/guides/contributor/membership.md
#
#
# Learn about CODEOWNERS file format:
# https://help.github.com/en/articles/about-code-owners
#

##
# NOTE: New components MUST have one or more codeowners. Add codeowners to the component metadata.yaml and run make gengithub
##

## COMMON & SHARED components
internal/common

`

const unmaintainedHeader = `

## UNMAINTAINED components

`

const codeownersHeader = `# Code generated by githubgen. DO NOT EDIT.
#####################################################
#
# List of codeowners for OpenTelemetry Collector Contrib
#
#####################################################
#
# Learn about membership in OpenTelemetry community:
# https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md
#
#
# Learn about CODEOWNERS file format:
# https://help.github.com/en/articles/about-code-owners
#

* @open-telemetry/collector-contrib-approvers
`

const distributionCodeownersHeader = `
#####################################################
#
# List of distribution maintainers for OpenTelemetry Collector Contrib
#
#####################################################
`

type codeownersGenerator struct {
skipGithub bool
skipGithub bool
getGitHubMembers func(skipGithub bool, githubOrg string) (map[string]struct{}, error)
}

func (cg *codeownersGenerator) Generate(data datatype.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))
unusedAllowlist := make(map[string]struct{}, len(allowlistLines))
for _, line := range allowlistLines {
if line == "" {
continue
}
allowlist[line] = struct{}{}
unusedAllowlist[line] = struct{}{}
}
var missingCodeowners []string
var duplicateCodeowners []string
members, err := cg.getGithubMembers()
err = cg.verifyCodeOwnerOrgMembership(allowlistData, data)

Check warning on line 30 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L30

Added line #L30 was not covered by tests
if err != nil {
return err
}
for _, codeowner := range data.Codeowners {
_, present := members[codeowner]

if !present {
_, allowed := allowlist[codeowner]
delete(unusedAllowlist, 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 && !cg.skipGithub {
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, ", "))
}
if len(unusedAllowlist) > 0 {
var unused []string
for k := range unusedAllowlist {
unused = append(unused, k)
}
sort.Strings(unused)
return fmt.Errorf("unused members in allowlist: %s", strings.Join(unused, ", "))
}

codeowners := codeownersHeader
deprecatedList := "## DEPRECATED components\n"
unmaintainedList := "\n## UNMAINTAINED components\n"
codeowners := fmt.Sprintf(codeownersHeader, data.RepoName, data.DefaultCodeOwner)
deprecatedList := deprecatedListHeader
unmaintainedList := unmaintainedListHeader

Check warning on line 37 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L35-L37

Added lines #L35 - L37 were not covered by tests

unmaintainedCodeowners := unmaintainedHeader
currentFirstSegment := ""

Check warning on line 41 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L41

Added line #L41 was not covered by tests
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)))
unmaintainedCodeowners += fmt.Sprintf("%s/%s %s \n", key, strings.Repeat(" ", data.MaxLength-len(key)), data.DefaultCodeOwner)

Check warning on line 48 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L48

Added line #L48 was not covered by tests
continue LOOP
}
if stability == "deprecated" && (m.Status.Codeowners == nil || len(m.Status.Codeowners.Active) == 0) {
Expand All @@ -161,11 +65,11 @@
owners += " "
owners += "@" + owner
}
codeowners += fmt.Sprintf("%s/%s @open-telemetry/collector-contrib-approvers%s\n", key, strings.Repeat(" ", data.MaxLength-len(key)), owners)
codeowners += fmt.Sprintf("%s/%s %s%s\n", key, strings.Repeat(" ", data.MaxLength-len(key)), data.DefaultCodeOwner, owners)

Check warning on line 68 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L68

Added line #L68 was not covered by tests
}
}

codeowners += distributionCodeownersHeader
codeowners += fmt.Sprintf(distributionCodeownersHeader, data.RepoName)

Check warning on line 72 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L72

Added line #L72 was not covered by tests
longestName := 0
for _, dist := range data.Distributions {
if longestName < len(dist.Name) {
Expand All @@ -178,34 +82,94 @@
for _, m := range dist.Maintainers {
maintainers = append(maintainers, fmt.Sprintf("@%s", m))
}
codeowners += fmt.Sprintf("reports/distributions/%s.yaml%s @open-telemetry/collector-contrib-approvers %s\n", dist.Name, strings.Repeat(" ", longestName-len(dist.Name)), strings.Join(maintainers, " "))
codeowners += fmt.Sprintf("reports/distributions/%s.yaml%s %s %s\n", dist.Name, strings.Repeat(" ", longestName-len(dist.Name)), data.DefaultCodeOwner, strings.Join(maintainers, " "))

Check warning on line 85 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L85

Added line #L85 was not covered by tests
}

err = os.WriteFile(filepath.Join(".github", "CODEOWNERS"), []byte(codeowners+unmaintainedCodeowners), 0o600)
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(".github", "ALLOWLIST"), []byte(allowlistHeader+deprecatedList+unmaintainedList), 0o600)
err = os.WriteFile(filepath.Join(".github", "ALLOWLIST"), []byte(fmt.Sprintf(allowlistHeader, data.RepoName)+deprecatedList+unmaintainedList), 0o600)

Check warning on line 92 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L92

Added line #L92 was not covered by tests
if err != nil {
return err
}
return nil
}

func (cg *codeownersGenerator) getGithubMembers() (map[string]struct{}, error) {
if cg.skipGithub {
// verifyCodeOwnerOrgMembership verifies that all codeOwners are members of the defined GitHub organization
//
// If a codeOwner is not part of the GitHub org, that user will be looked for in the allowlist.
//
// The method returns an error if:
// - there are code owners that are not org members and not in the allowlist (only if skipGithub is set to false)
// - there are redundant entries in the allowlist
// - there are entries in the allowlist that are unused
func (cg *codeownersGenerator) verifyCodeOwnerOrgMembership(allowlistData []byte, data datatype.GithubData) error {
allowlist := strings.Split(string(allowlistData), "\n")
allowlist = slices.DeleteFunc(allowlist, func(s string) bool {
return s == ""
})
unusedAllowlist := append([]string{}, allowlist...)

var missingCodeowners []string
var duplicateCodeowners []string

members, err := cg.getGitHubMembers(cg.skipGithub, data.GitHubOrg)
if err != nil {
return err
}

Check warning on line 120 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L119-L120

Added lines #L119 - L120 were not covered by tests

// sort codeowners
for _, codeowner := range data.Codeowners {
_, ownerPresentInMembers := members[codeowner]

if !ownerPresentInMembers {
ownerInAllowlist := slices.Contains(allowlist, codeowner)
unusedAllowlist = slices.DeleteFunc(unusedAllowlist, func(s string) bool {
return s == codeowner
})

ownerInAllowlist = ownerInAllowlist || strings.HasPrefix(codeowner, "open-telemetry/")

if !ownerInAllowlist {
missingCodeowners = append(missingCodeowners, codeowner)
}
} else if slices.Contains(allowlist, codeowner) {
duplicateCodeowners = append(duplicateCodeowners, codeowner)
}
}

// error cases
if len(missingCodeowners) > 0 && !cg.skipGithub {
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, ", "))
}
if len(unusedAllowlist) > 0 {
unused := append([]string{}, unusedAllowlist...)
sort.Strings(unused)
return fmt.Errorf("unused members in allowlist: %s", strings.Join(unused, ", "))
}
return err
}

func GetGithubMembers(skipGithub bool, githubOrg string) (map[string]struct{}, error) {
if skipGithub {

Check warning on line 160 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L159-L160

Added lines #L159 - L160 were not covered by tests
// don't try to get organization members if no token is expected
return map[string]struct{}{}, nil
}
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)
client := github.NewClient(nil).WithAuthToken(githubToken)

Check warning on line 168 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L168

Added line #L168 was not covered by tests
var allUsers []*github.User
pageIndex := 0
for {
users, resp, err := client.Organizations.ListMembers(context.Background(), "open-telemetry",
users, resp, err := client.Organizations.ListMembers(context.Background(), githubOrg,

Check warning on line 172 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L172

Added line #L172 was not covered by tests
&github.ListMembersOptions{
PublicOnly: false,
ListOptions: github.ListOptions{
Expand Down
Loading
Loading