From dcfcc5f81c1a6604df15e38d6cac22cc645d6c25 Mon Sep 17 00:00:00 2001 From: Sriram Venkatesh Date: Sun, 6 May 2018 14:49:50 +1200 Subject: [PATCH] Reading from .shepherd.yml file --- .shepherd.yml | 21 ++++++++ main.go | 88 ++++++++++++------------------- shepherd/codeowners.go | 14 ++--- shepherd/repoTeam.go | 8 +-- shepherd/shepherd.go | 114 ++++++++++++++++++++++++++++++----------- shepherd/team.go | 13 +++-- 6 files changed, 155 insertions(+), 103 deletions(-) create mode 100644 .shepherd.yml diff --git a/.shepherd.yml b/.shepherd.yml new file mode 100644 index 0000000..4d6a2b3 --- /dev/null +++ b/.shepherd.yml @@ -0,0 +1,21 @@ +# Example .shepherd.yml + +include_user_repo: false +url: "" +dry_run: false +debug: false +organizations: + - orgName: "alfredcubed" + maintainer: "core-maintainers" + protected_branch: "master" +repos: + - name: + maintainer: + templates: + issue_template: + pr_template: + codeowner_template: + pr_msg_template: + + + \ No newline at end of file diff --git a/main.go b/main.go index 0d97325..8762ba1 100644 --- a/main.go +++ b/main.go @@ -4,23 +4,20 @@ import ( "flag" "fmt" "os" + "strings" "github.com/google/go-github/github" "github.com/sirupsen/logrus" + "github.com/spf13/viper" "github.com/srizzling/shepherd/shepherd" ) var version = "master" var ( - token string - baseURL string - org string - dryRun bool - maintainer string - pbranch string - - vrsn bool + token string + vrsnFlag bool + configuration shepherd.Config ) const ( @@ -42,60 +39,42 @@ developed with <3 by Sriram Venkatesh ) func init() { - // parse flags - flag.StringVar(&token, "token", os.Getenv("GITHUB_TOKEN"), "required: GitHub API token (or env var GITHUB_TOKEN)") - flag.StringVar(&org, "org", "", "required: organization to look through") - flag.StringVar(&pbranch, "branch", "master", "branch to protect") - flag.StringVar(&baseURL, "url", "", "optional: GitHub Enterprise URL") - flag.StringVar(&maintainer, "maintainer", "", "required: team to set as CODEOWNERS") - flag.BoolVar(&dryRun, "dryrun", false, "optional: do not change branch settings just print the changes that would occur") - - flag.BoolVar(&vrsn, "version", false, "optional: print version and exit") - - // Exit safely when version is used - if vrsn { - fmt.Printf(BANNER, version) - os.Exit(0) + // Intialize Viper + viper.SetConfigName(".shepherd") + viper.AddConfigPath(".") + viper.SetConfigType("yaml") + viper.SetEnvPrefix("shepherd") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() // read in environment variables that match + viper.BindEnv("GITHUB_TOKEN") + + if err := viper.ReadInConfig(); err != nil { + logrus.Fatalf("Error reading config file, %s", err) + panic(err) } - flag.Usage = func() { - fmt.Fprint(os.Stderr, fmt.Sprintf(BANNER, version)) - flag.PrintDefaults() + if err := viper.Unmarshal(&configuration); err != nil { + logrus.Fatalf("Error marshalling config file, %s", err) + panic(err) } - flag.Parse() + token = configuration.GithubToken if token == "" { - usageAndExit("GitHub token cannot be empty.", 1) - } - - if org == "" { - usageAndExit("no organization provided", 1) - } - - if maintainer == "" { - usageAndExit("no organization provided", 1) + usageAndExit("Error! Github Token is required", 1) } - } func main() { // intialize bot - bot, err := shepherd.NewBot(baseURL, token, maintainer, org) - if err != nil { - logrus.Fatal(err) - panic(err) - } - - //Retreive repos that are owned by the org - repos, err := bot.RetreiveRepos() + bot, err := shepherd.NewBot(configuration) if err != nil { logrus.Fatal(err) panic(err) } - for _, repo := range repos { - err = handleRepo(bot, repo) + for repo, repoConfig := range bot.Repos { + err = handleRepo(bot, repo, repoConfig) if err != nil { logrus.Fatal(err) panic(err) @@ -104,8 +83,9 @@ func main() { } // a function that will be applied to each repo on an org -func handleRepo(bot *shepherd.ShepardBot, repo *github.Repository) error { - b, err := bot.GetBranch(repo, pbranch) +func handleRepo(bot *shepherd.ShepardBot, repo *github.Repository, repoConfig shepherd.RepoConfig) error { + b, err := bot.GetBranch(repo, repoConfig.ProtectedBranch) + dryRun := configuration.DryRun if err != nil { return err } @@ -119,7 +99,7 @@ func handleRepo(bot *shepherd.ShepardBot, repo *github.Repository) error { fmt.Printf("[UPDATE REQUIRED] %s: A codeowner file was not found, a PR should be created\n", *repo.FullName) if !dryRun { - pr, err := bot.DoCreateCodeowners(repo, b) + pr, err := bot.DoCreateCodeowners(repo, b, repoConfig.GHMaintainer) if err != nil { return err } @@ -134,23 +114,23 @@ func handleRepo(bot *shepherd.ShepardBot, repo *github.Repository) error { fmt.Printf("[OK] %s: CODEOWNERS file already exists in repo\n", *repo.FullName) //Need to assign team to the repo even its in the org to be a "maintainer" - repoManagement, err := bot.CheckTeamRepoManagement(repo) + repoManagement, err := bot.CheckTeamRepoManagement(repo, repoConfig.GHMaintainer) if err != nil { return err } if repoManagement { - fmt.Printf("[OK] %s: is already managed by %s\n", *repo.FullName, maintainer) + fmt.Printf("[OK] %s: is already managed by %s\n", *repo.FullName, repoConfig.Maintainer) } else { - fmt.Printf("[UPDATE REQUIRED] %s: needs to updated to be managed by %s\n", *repo.FullName, maintainer) + fmt.Printf("[UPDATE REQUIRED] %s: needs to updated to be managed by %s\n", *repo.FullName, repoConfig.Maintainer) if !dryRun { - err = bot.DoTeamRepoManagement(repo) + err = bot.DoTeamRepoManagement(repo, repoConfig.GHMaintainer) if err != nil { return err } - fmt.Printf("[OK] %s: is now managed by %s\n", *repo.FullName, maintainer) + fmt.Printf("[OK] %s: is now managed by %s\n", *repo.FullName, repoConfig.Maintainer) } } diff --git a/shepherd/codeowners.go b/shepherd/codeowners.go index 53bedf9..e15ea21 100644 --- a/shepherd/codeowners.go +++ b/shepherd/codeowners.go @@ -18,9 +18,9 @@ func (s *ShepardBot) createBranch(repo *github.Repository, refObj *github.Refere return err } -func (s *ShepardBot) commitFileToBranch(repo *github.Repository, branchName string) error { +func (s *ShepardBot) commitFileToBranch(repo *github.Repository, branchName string, maintainer *github.Team) error { content := []byte( - fmt.Sprintf("* @%s/%s", s.org.GetLogin(), s.maintainerTeam.GetName()), + fmt.Sprintf("* @%s/%s", maintainer.GetOrganization().GetLogin(), maintainer.GetName()), ) _, _, err := s.gClient.Repositories.CreateFile( @@ -38,8 +38,8 @@ func (s *ShepardBot) commitFileToBranch(repo *github.Repository, branchName stri return err } -func (s *ShepardBot) createPR(repo *github.Repository, branchName string, branch *github.Branch) (*github.PullRequest, error) { - prMessage := fmt.Sprintf("Hi there @%s!,\n\nI'm your helpful shepherd and I've found that you are missing an important CODEOWNERS file which is mandated to be included for repos within this org (this ensures that the maintainers are pinged to review PR as they come in).\n\nThis PR is automatically created by [shepherd](https://github.com/srizzling/shepherd)\n\nThanks,\nShepard Bot", s.maintainerTeam.GetName()) +func (s *ShepardBot) createPR(repo *github.Repository, branchName string, branch *github.Branch, maintainer *github.Team) (*github.PullRequest, error) { + prMessage := fmt.Sprintf("Hi there @%s!,\n\nI'm your helpful shepherd and I've found that you are missing an important CODEOWNERS file which is mandated to be included for repos within this org (this ensures that the maintainers are pinged to review PR as they come in).\n\nThis PR is automatically created by [shepherd](https://github.com/srizzling/shepherd)\n\nThanks,\nShepard Bot", maintainer.GetName()) // Create a PR with the branch created newPR := &github.NewPullRequest{ @@ -56,7 +56,7 @@ func (s *ShepardBot) createPR(repo *github.Repository, branchName string, branch // DoCreateCodeowners function will create a CODEOWNERS file in a branch, create a PR against the repo // and set the reviewer (of the CODEOWNERS PR) as the maintainer team configured -func (s *ShepardBot) DoCreateCodeowners(repo *github.Repository, branch *github.Branch) (*github.PullRequest, error) { +func (s *ShepardBot) DoCreateCodeowners(repo *github.Repository, branch *github.Branch, maintainer *github.Team) (*github.PullRequest, error) { // Create a branch on the repo, from current master sRand, err := randomstrings.GenerateRandomString(5) if err != nil { @@ -77,13 +77,13 @@ func (s *ShepardBot) DoCreateCodeowners(repo *github.Repository, branch *github. } // Commit CODEOWNERS file to Branch - err = s.commitFileToBranch(repo, branchName) + err = s.commitFileToBranch(repo, branchName, maintainer) if err != nil { return nil, err } // Create PR with newly created branch - pr, err := s.createPR(repo, branchName, branch) + pr, err := s.createPR(repo, branchName, branch, maintainer) if err != nil { return nil, err } diff --git a/shepherd/repoTeam.go b/shepherd/repoTeam.go index f8cf67c..abbd5b1 100644 --- a/shepherd/repoTeam.go +++ b/shepherd/repoTeam.go @@ -7,11 +7,11 @@ import ( ) // CheckTeamRepoManagement verifies if the team is an admin of the project -func (s *ShepardBot) CheckTeamRepoManagement(repo *github.Repository) (bool, error) { +func (s *ShepardBot) CheckTeamRepoManagement(repo *github.Repository, maintainer *github.Team) (bool, error) { _, response, err := s.gClient.Organizations.IsTeamRepo( s.ctx, - *s.maintainerTeam.ID, + *maintainer.ID, *repo.Owner.Login, *repo.Name, ) @@ -33,11 +33,11 @@ func (s *ShepardBot) CheckTeamRepoManagement(repo *github.Repository) (bool, err } // DoTeamRepoManagement sets the team as an admin of the repo -func (s *ShepardBot) DoTeamRepoManagement(repo *github.Repository) error { +func (s *ShepardBot) DoTeamRepoManagement(repo *github.Repository, maintainer *github.Team) error { opt := &github.OrganizationAddTeamRepoOptions{ Permission: "admin", } - _, err := s.gClient.Organizations.AddTeamRepo(s.ctx, *s.maintainerTeam.ID, *repo.Owner.Login, *repo.Name, opt) + _, err := s.gClient.Organizations.AddTeamRepo(s.ctx, *maintainer.ID, *repo.Owner.Login, *repo.Name, opt) return err } diff --git a/shepherd/shepherd.go b/shepherd/shepherd.go index ec3c347..7509688 100644 --- a/shepherd/shepherd.go +++ b/shepherd/shepherd.go @@ -12,10 +12,45 @@ import ( // ShepardBot is the main bot object that gets created type ShepardBot struct { - gClient *github.Client - ctx context.Context - maintainerTeam *github.Team - org *github.Organization + gClient *github.Client + ctx context.Context + Repos map[*github.Repository]RepoConfig +} + +// Config struct holds the configuration data to do things +type Config struct { + IncludeUserRepo bool + BaseURL string + DryRun bool + Debug bool + Organizations []OrganizationsConfig + Repos []RepoConfig + GithubToken string `mapstructure:"GITHUB_TOKEN" yaml:"github_token, omitempty"` +} + +// OrganizationsConfig is the config section for configuring organization +type OrganizationsConfig struct { + OrgName string `mapstructure:"OrgName" yaml:"orgName, omitempty"` + Maintainer string + Labels []Label + Templates map[string]string + ProtectedBranch string `mapstructure:"protected_branch" yaml:"protected_branch"` +} + +// ReposConfig is the config section for configuring organization +type RepoConfig struct { + Name string + Maintainer string + Labels []Label + ProtectedBranch string + Templates map[string]string + GHMaintainer *github.Team + GHLabels []*github.Label +} + +type Label struct { + Name string + Color string } // ShepardError is a generic error container for reporting errors/http status code errors from the Github API @@ -31,10 +66,10 @@ func (e *ShepardError) Error() string { } // NewBot creates a new ShepardBot based off the baseURL(provide empty string if you want to default to basic github) -func NewBot(baseURL string, token string, maintainerTeamName string, orgName string) (*ShepardBot, error) { +func NewBot(config Config) (*ShepardBot, error) { // initialize a new github client ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, + &oauth2.Token{AccessToken: config.GithubToken}, ) ctx := context.Background() tc := oauth2.NewClient(ctx, ts) @@ -42,9 +77,9 @@ func NewBot(baseURL string, token string, maintainerTeamName string, orgName str client := github.NewClient(tc) // Setup baseUrl for github enterprise, else default to Github.com. - if baseURL != "" { + if config.BaseURL != "" { var err error - client.BaseURL, err = url.Parse(baseURL + "/api/v3/") + client.BaseURL, err = url.Parse(config.BaseURL + "/api/v3/") if err != nil { return nil, err } @@ -55,30 +90,39 @@ func NewBot(baseURL string, token string, maintainerTeamName string, orgName str ctx: ctx, } - // set github org to bot - err := bot.setOrg(orgName) - if err != nil { - return nil, err + repoMap := make(map[*github.Repository]RepoConfig) + for _, org := range config.Organizations { + orgMap, err := bot.retreiveRepoOnOrg(org) + if err != nil { + return nil, err + } + repoMap = mapUnion(repoMap, orgMap) } + bot.Repos = repoMap + return bot, nil +} - // set maintainer team to org - err = bot.setMaintainerTeam(maintainerTeamName) - if err != nil { - return nil, err +func mapUnion(m1, m2 map[*github.Repository]RepoConfig) map[*github.Repository]RepoConfig { + for ia, va := range m1 { + m2[ia] = va } - - return bot, nil + return m2 } -// RetreiveRepos returns a list of repos within the organization -func (s *ShepardBot) RetreiveRepos() ([]*github.Repository, error) { +// RetreiveReposOnOrg returns a list of repos within the organization +func (s *ShepardBot) retreiveRepoOnOrg(org OrganizationsConfig) (map[*github.Repository]RepoConfig, error) { opt := &github.RepositoryListByOrgOptions{ ListOptions: github.ListOptions{PerPage: 10}, } + orgObj, _, err := s.gClient.Organizations.Get(s.ctx, org.OrgName) + if err != nil { + return nil, err + } + var allRepos []*github.Repository for { - repos, resp, err := s.gClient.Repositories.ListByOrg(s.ctx, *s.org.Login, opt) + repos, resp, err := s.gClient.Repositories.ListByOrg(s.ctx, *orgObj.Login, opt) if err != nil { return nil, err } @@ -89,7 +133,24 @@ func (s *ShepardBot) RetreiveRepos() ([]*github.Repository, error) { opt.Page = resp.NextPage } - return allRepos, nil + repoMap := make(map[*github.Repository]RepoConfig) + + for _, r := range allRepos { + mTeam, err := s.getMaintainerTeam(orgObj, org.Maintainer) + if err != nil { + return nil, err + } + + repoMap[r] = RepoConfig{ + Name: r.GetFullName(), + Maintainer: org.Maintainer, + Labels: org.Labels, + GHMaintainer: mTeam, + GHLabels: nil, + ProtectedBranch: org.ProtectedBranch, + } + } + return repoMap, nil } // GetBranch function return a branch obj depending on the name provided @@ -100,12 +161,3 @@ func (s *ShepardBot) GetBranch(repo *github.Repository, branchName string) (*git } return branch, nil } - -func (s *ShepardBot) setOrg(orgName string) error { - org, _, err := s.gClient.Organizations.Get(s.ctx, orgName) - if err != nil { - return err - } - s.org = org - return nil -} diff --git a/shepherd/team.go b/shepherd/team.go index 0a9071e..d272727 100644 --- a/shepherd/team.go +++ b/shepherd/team.go @@ -27,20 +27,19 @@ func (s *ShepardBot) retreiveTeams(orgName string) ([]*github.Team, error) { return allTeams, nil } -func (s *ShepardBot) setMaintainerTeam(maintainerTeamName string) error { - teams, err := s.retreiveTeams(s.org.GetLogin()) +func (s *ShepardBot) getMaintainerTeam(org *github.Organization, maintainerTeamName string) (*github.Team, error) { + teams, err := s.retreiveTeams(org.GetLogin()) if err != nil { - return err + return nil, err } for _, team := range teams { - if strings.EqualFold(maintainerTeamName, s.org.GetLogin()+"/"+*team.Name) { - s.maintainerTeam = team - return nil + if strings.EqualFold(maintainerTeamName, *team.Name) { + return team, nil } } errMsg := fmt.Sprintf("Team (%s) not found within org", maintainerTeamName) err = errors.New(errMsg) - return err + return nil, err }