Created
May 3, 2023 19:56
-
-
Save tarkatronic/a609d949e9cb83f2d72e0aa027ed855f to your computer and use it in GitHub Desktop.
GitHub Ownership Collector
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Based on https://betterstack.com/community/guides/logging/zerolog/ | |
package logger | |
import ( | |
"io" | |
"os" | |
"runtime/debug" | |
"strconv" | |
"sync" | |
"time" | |
"github.com/rs/zerolog" | |
) | |
var once sync.Once | |
var log zerolog.Logger | |
func Get() zerolog.Logger { | |
once.Do(func() { | |
var logLevel int8 | |
zerolog.TimeFieldFormat = time.RFC3339Nano | |
parsed, err := strconv.ParseInt(os.Getenv("LOG_LEVEL"), 10, 8) | |
if err != nil { | |
logLevel = int8(zerolog.InfoLevel) // Default to INFO | |
} else { | |
logLevel = int8(parsed) | |
} | |
var output io.Writer = zerolog.ConsoleWriter{ | |
Out: os.Stdout, | |
TimeFormat: time.RFC3339, | |
} | |
buildInfo, _ := debug.ReadBuildInfo() | |
log = zerolog.New(output). | |
Level(zerolog.Level(logLevel)). | |
With(). | |
Timestamp(). | |
Str("go_version", buildInfo.GoVersion). | |
Logger() | |
}) | |
return log | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"os" | |
"github.com/joho/godotenv" | |
"github.com/shurcooL/githubv4" | |
"logger" | |
"golang.org/x/exp/slices" | |
"golang.org/x/oauth2" | |
) | |
type RepoCollaborator struct { | |
PermissionSources []struct { | |
Permission string | |
Source struct { | |
Typename string `graphql:"__typename"` | |
Repository struct { | |
Name string | |
} `graphql:"... on Repository"` | |
Team struct { | |
CombinedSlug string | |
} `graphql:"... on Team"` | |
} | |
} | |
Node struct { | |
Login string | |
} | |
} | |
type OrgRepository struct { | |
Name string | |
IsArchived bool | |
Collaborators struct { | |
Edges []RepoCollaborator | |
} `graphql:"collaborators(first: 100)"` | |
} | |
type OrgOwnershipQuery struct { | |
Organization struct { | |
Repositories struct { | |
TotalCount int | |
PageInfo struct { | |
EndCursor githubv4.String | |
HasNextPage bool | |
} | |
Nodes []OrgRepository | |
} `graphql:"repositories(first: 20, orderBy: {field: NAME, direction: ASC}, isFork: false, isLocked: false, after: $reposCursor)"` | |
} `graphql:"organization(login: $login)"` | |
} | |
func main() { | |
log := logger.Get() | |
err := godotenv.Load(".env") | |
if err != nil { | |
log.Info().Err(err).Msg(".env file not loaded.") | |
} | |
ghTokenSource := oauth2.StaticTokenSource( | |
&oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")}, | |
) | |
ghOrgLogin := os.Getenv("GITHUB_ORG") | |
httpClient := oauth2.NewClient(context.Background(), ghTokenSource) | |
ghClient := githubv4.NewClient(httpClient) | |
var ownershipQuery OrgOwnershipQuery | |
queryVars := map[string]interface{}{ | |
"login": githubv4.String(ghOrgLogin), | |
"reposCursor": (*githubv4.String)(nil), | |
} | |
/* | |
{ | |
"repo1": { | |
"individuals": ["foo", "bar"], | |
"teams": ["blah", "baz"], | |
} | |
} | |
*/ | |
owners := map[string]map[string][]string{} | |
for { | |
log.Info().Msg("Querying GitHub API for repository ownership...") | |
err := ghClient.Query(context.Background(), &ownershipQuery, queryVars) | |
if err != nil { | |
log.Panic().Err(err).Msg("Failed to query GitHub!") | |
} | |
for _, repo := range ownershipQuery.Organization.Repositories.Nodes { | |
individualOwners := []string{} | |
teamOwners := []string{} | |
if repo.IsArchived { | |
continue // Skip archived repositories; we don't need to report on these. | |
} | |
for _, collaborator := range repo.Collaborators.Edges { | |
orgAdmin := false | |
for _, source := range collaborator.PermissionSources { | |
switch source.Source.Typename { | |
case "Organization": | |
if source.Permission == "ADMIN" { | |
orgAdmin = true | |
} | |
case "Repository": | |
if orgAdmin { | |
log.Debug().Str("login", collaborator.Node.Login).Msg("Skipping org admin.") | |
continue // Org admins also get implicit admin to all repos | |
} | |
if source.Permission == "ADMIN" || source.Permission == "WRITE" { | |
if !slices.Contains(individualOwners, collaborator.Node.Login) { | |
individualOwners = append(individualOwners, collaborator.Node.Login) | |
} | |
} | |
case "Team": | |
if source.Permission == "ADMIN" || source.Permission == "WRITE" { | |
if !slices.Contains(teamOwners, source.Source.Team.CombinedSlug) { | |
teamOwners = append(teamOwners, source.Source.Team.CombinedSlug) | |
} | |
} | |
} | |
} | |
} | |
owners[repo.Name] = map[string][]string{ | |
"individuals": individualOwners, | |
"teams": teamOwners, | |
} | |
} | |
if !ownershipQuery.Organization.Repositories.PageInfo.HasNextPage { | |
break | |
} | |
queryVars["reposCursor"] = githubv4.NewString(ownershipQuery.Organization.Repositories.PageInfo.EndCursor) | |
} | |
log.Info().Any("owners", owners).Msg("Ownership info loaded.") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment