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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d08f45f
add template for repo name
mowies Jan 7, 2025
1ab42fa
fix tests, add chlog
mowies Jan 7, 2025
5e4b833
add default code owner flag and use it
mowies Jan 7, 2025
5798dda
move constants to separate file
mowies Jan 7, 2025
1523fdb
update deprecated github client
mowies Jan 7, 2025
2c7f2e5
extract function
mowies Jan 7, 2025
e1593fe
make func more testable, generate test file
mowies Jan 8, 2025
381a580
add happy path test
mowies Jan 8, 2025
be0a1e9
mock getgithubmembers
mowies Jan 8, 2025
ce8d29f
refactoring, make unit test more better
mowies Jan 8, 2025
b03dd5e
refactoring with unit test
mowies Jan 8, 2025
da851b3
make github org for membership check configurable, make path suffix t…
mowies Jan 9, 2025
cce762c
add back to versions.yaml
mowies Jan 9, 2025
0e4b0ea
add more cmd line flag defaults
mowies Jan 9, 2025
475b102
adjust changelog msg
mowies Jan 9, 2025
9495f13
add better method doc comment
mowies Jan 9, 2025
f9bcd7f
update readme with new flags
mowies Jan 9, 2025
6ccccbf
fix test
mowies Jan 9, 2025
59acb6d
make precommit
mowies Jan 9, 2025
2f1d3f0
fix templates
mowies Jan 9, 2025
5dc0c8a
fix markdown line length
mowies Jan 9, 2025
ddcfbd5
remove pretty repo name from input flags and file templates
mowies Jan 13, 2025
e261de5
remove otel specific membership hint from file templates, remove hard…
mowies Jan 13, 2025
01f42dd
fix test
mowies Jan 13, 2025
d370fe0
Update githubgen/README.md
mowies Jan 13, 2025
b518bbe
exchange confmap provider metadata reading for normal yaml unmarshal
mowies Jan 14, 2025
37a30b1
move constant
mowies Jan 14, 2025
385d687
use default code owners instead of exiting with error when a componen…
mowies Jan 14, 2025
497a059
handle file read/writing relative to the configured folder
mowies Jan 14, 2025
1d91af6
linter
mowies Jan 14, 2025
5e3bc75
distributions unit test
mowies Jan 14, 2025
3fcee5f
add license header
mowies Jan 14, 2025
f847dd4
refactoring plus test
mowies Jan 14, 2025
874b307
typo
mowies Jan 14, 2025
f4ceb2d
another unit test
mowies Jan 14, 2025
e3a64e0
adapt codeowners file to latest PR
mowies Jan 20, 2025
7bde94b
adapt error message
mowies Jan 20, 2025
7b9da9d
Merge branch 'main' into githubgen-follow-up-1
mowies Jan 20, 2025
9b31bb3
fix linter issue
mowies Jan 20, 2025
7e2fc41
Merge branch 'main' into githubgen-follow-up-1
mowies Jan 20, 2025
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:
10 changes: 8 additions & 2 deletions githubgen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ $> ./githubgen
The equivalent of:

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

## Checking codeowners against OpenTelemetry membership via GitHub API
Expand All @@ -26,12 +28,16 @@ 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 `--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.
221 changes: 96 additions & 125 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,142 +16,45 @@
"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.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 _, folder := range data.Folders {
m := data.Components[folder]

Check warning on line 44 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L43-L44

Added lines #L43 - L44 were not covered by tests
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)))
unmaintainedList += folder + "/\n"
unmaintainedCodeowners += fmt.Sprintf("%s/%s %s\n", folder, strings.Repeat(" ", data.MaxLength-len(folder)), data.DefaultCodeOwner)

Check warning on line 48 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L47-L48

Added lines #L47 - L48 were not covered by tests
continue LOOP
}
if stability == "deprecated" && (m.Status.Codeowners == nil || len(m.Status.Codeowners.Active) == 0) {
deprecatedList += key + "/\n"
deprecatedList += folder + "/\n"

Check warning on line 52 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L52

Added line #L52 was not covered by tests
}
}

if m.Status.Codeowners != nil {
parts := strings.Split(key, string(os.PathSeparator))
parts := strings.Split(folder, string(os.PathSeparator))

Check warning on line 57 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L57

Added line #L57 was not covered by tests
firstSegment := parts[0]
if firstSegment != currentFirstSegment {
currentFirstSegment = firstSegment
Expand All @@ -159,59 +63,126 @@
owners := ""
for _, owner := range m.Status.Codeowners.Active {
owners += " "
owners += "@" + owner
if !strings.HasPrefix(owner, "@") {
owners += "@" + owner
}

Check warning on line 68 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L66-L68

Added lines #L66 - L68 were not covered by tests
}
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", strings.TrimPrefix(folder, data.RootFolder+"/"), strings.Repeat(" ", data.MaxLength-len(folder)), data.DefaultCodeOwner, owners)

Check warning on line 70 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L70

Added line #L70 was not covered by tests
}
}

codeowners += distributionCodeownersHeader
longestName := 0
for _, dist := range data.Distributions {
if longestName < len(dist.Name) {
longestName = len(dist.Name)
}
}
longestName := cg.longestNameSpaces(data)

Check warning on line 75 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L75

Added line #L75 was not covered by tests

for _, dist := range data.Distributions {
var maintainers []string
for _, m := range dist.Maintainers {
maintainers = append(maintainers, fmt.Sprintf("@%s", m))
}

distribution := fmt.Sprintf("\nreports/distributions/%s.yaml%s @open-telemetry/collector-contrib-approvers", dist.Name, strings.Repeat(" ", longestName-len(dist.Name)))
distribution := fmt.Sprintf("\nreports/distributions/%s.yaml%s %s", dist.Name, strings.Repeat(" ", longestName-len(dist.Name)), data.DefaultCodeOwner)

Check warning on line 83 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L83

Added line #L83 was not covered by tests
if len(maintainers) > 0 {
distribution += fmt.Sprintf(" %s", strings.Join(maintainers, " "))
}

codeowners += distribution
}

err = os.WriteFile(filepath.Join(".github", "CODEOWNERS"), []byte(codeowners+unmaintainedCodeowners), 0o600)
err = os.WriteFile(filepath.Join(data.RootFolder, ".github", "CODEOWNERS"), []byte(codeowners+unmaintainedCodeowners), 0o600)

Check warning on line 91 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L91

Added line #L91 was not covered by tests
if err != nil {
return err
}
err = os.WriteFile(filepath.Join(".github", "ALLOWLIST"), []byte(allowlistHeader+deprecatedList+unmaintainedList), 0o600)
err = os.WriteFile(filepath.Join(data.RootFolder, ".github", "ALLOWLIST"), []byte(allowlistHeader+deprecatedList+unmaintainedList), 0o600)

Check warning on line 95 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L95

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

func (cg *codeownersGenerator) getGithubMembers() (map[string]struct{}, error) {
if cg.skipGithub {
func (cg *codeownersGenerator) longestNameSpaces(data datatype.GithubData) int {
longestName := 0
for _, dist := range data.Distributions {
if longestName < len(dist.Name) {
longestName = len(dist.Name)
}
}
return longestName
}

// 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 133 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L132-L133

Added lines #L132 - L133 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, data.GitHubOrg+"/")

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 173 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L172-L173

Added lines #L172 - L173 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 181 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L181

Added line #L181 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 185 in githubgen/codeowners.go

View check run for this annotation

Codecov / codecov/patch

githubgen/codeowners.go#L185

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