Skip to content

Instantly share code, notes, and snippets.

@tarkatronic
Created May 3, 2023 19:56
Show Gist options
  • Save tarkatronic/a609d949e9cb83f2d72e0aa027ed855f to your computer and use it in GitHub Desktop.
Save tarkatronic/a609d949e9cb83f2d72e0aa027ed855f to your computer and use it in GitHub Desktop.
GitHub Ownership Collector
// 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
}
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