Skip to content

Commit

Permalink
Label PR scaffolding (essentially dry run, without labeling for now) (#3
Browse files Browse the repository at this point in the history
)

* Scaffold command

* Check PRs to label

* Add an example config

* Add option to skip users

* Swap to separate embedded config struct for the label options, so they can be passed to the handler
  • Loading branch information
cmmarslender authored Mar 28, 2024
1 parent 6f47ce3 commit d34bd9e
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ main
bin/
.idea/*
*.log
config.yml
33 changes: 33 additions & 0 deletions cmd/labelPRs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cmd

import (
"log"

"github.com/google/go-github/v60/github"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/chia-network/github-bot/internal/config"
"github.com/chia-network/github-bot/internal/label"
)

// labelPRsCmd represents the labelPRs command
var labelPRsCmd = &cobra.Command{
Use: "label-prs",
Short: "Adds community and internal labels to pull requests in designated repos",
Run: func(cmd *cobra.Command, args []string) {
cfg, err := config.LoadConfig(viper.GetString("config"))
if err != nil {
log.Fatalf("error loading config: %s\n", err.Error())
}
client := github.NewClient(nil).WithAuthToken(cfg.GithubToken)
err = label.PullRequests(client, cfg.InternalTeam, cfg.LabelConfig)
if err != nil {
log.Fatalln(err.Error())
}
},
}

func init() {
rootCmd.AddCommand(labelPRsCmd)
}
28 changes: 12 additions & 16 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
"github.com/spf13/viper"
)

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "github-bot",
Expand All @@ -28,26 +26,24 @@ func Execute() {
}

func init() {
var cfgFile string

cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.yml", "config file to load")

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.github-bot.yaml)")
cobra.CheckErr(viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")))
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)

// Search config in home directory with name ".github-bot" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".github-bot")
}
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)

// Search config in home directory with name ".github-bot" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".github-bot")

viper.SetEnvPrefix("GITHUB_BOT")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
Expand Down
20 changes: 20 additions & 0 deletions example-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
github_token: ghp_abc123

# Shared Settings
# Team that contains all "internal" members
internal_team: "my-org/my-team"

# If empty, internal label will not be added
label_internal: "internal-pr"
# If empty, external label will not be added
label_external: "community-pr"
# Repos to check for labeling
label_check_repos:
- name: "my-org/repo1"
# Only PRs with a number higher than this value will be labeled
minimum_number: 0
- name: "my-org/repo2"
minimum_number: 1000
# PRs opened by these users will not be labeled
label_skip_users:
- "dependabot[bot]"
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ module github.com/chia-network/github-bot
go 1.22.1

require (
github.com/google/go-github/v60 v60.0.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
)

require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
Expand Down
10 changes: 8 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
Expand Down Expand Up @@ -65,6 +70,7 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
23 changes: 23 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

// Config defines the config for all aspects of the bot
type Config struct {
GithubToken string `yaml:"github_token"`
InternalTeam string `yaml:"internal_team"`
LabelConfig `yaml:",inline"`
}

// LabelConfig is the configuration options specific to labeling PRs
type LabelConfig struct {
LabelInternal string `yaml:"label_internal"`
LabelExternal string `yaml:"label_external"`
LabelCheckRepos []CheckRepo `yaml:"label_check_repos"`
LabelSkipUsers []string `yaml:"label_skip_users"`
LabelSkipMap map[string]bool
}

// CheckRepo is config settings when checking a repo
type CheckRepo struct {
Name string `yaml:"name"`
MinimumNumber int `yaml:"minimum_number"`
}
29 changes: 29 additions & 0 deletions internal/config/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package config

import (
"os"

"gopkg.in/yaml.v3"
)

// LoadConfig loads config from the given path
func LoadConfig(path string) (*Config, error) {
configBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}

config := &Config{}

err = yaml.Unmarshal(configBytes, config)
if err != nil {
return nil, err
}

config.LabelSkipMap = map[string]bool{}
for _, user := range config.LabelSkipUsers {
config.LabelSkipMap[user] = true
}

return config, nil
}
124 changes: 124 additions & 0 deletions internal/label/pullrequests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package label

import (
"context"
"fmt"
"log"
"strings"

"github.com/google/go-github/v60/github"

"github.com/chia-network/github-bot/internal/config"
)

// PullRequests applies internal or community labels to pull requests
// Internal is determined by checking if the PR author is a member of the specified internalTeam
func PullRequests(githubClient *github.Client, internalTeam string, cfg config.LabelConfig) error {
teamMembers := map[string]bool{}

teamParts := strings.Split(internalTeam, "/")
if len(teamParts) != 2 {
return fmt.Errorf("invalid team name - must contain org and team : %s", internalTeam)
}

teamOpts := &github.TeamListTeamMembersOptions{
Role: "all",
ListOptions: github.ListOptions{
Page: 0,
PerPage: 100,
},
}

for {
teamOpts.ListOptions.Page++
members, resp, err := githubClient.Teams.ListTeamMembersBySlug(context.TODO(), teamParts[0], teamParts[1], teamOpts)
if err != nil {
return fmt.Errorf("error getting team %s: %w", internalTeam, err)
}

for _, member := range members {
teamMembers[*member.Login] = true
}

if resp.NextPage == 0 {
break
}
}

for _, fullRepo := range cfg.LabelCheckRepos {
log.Println("checking repos")
parts := strings.Split(fullRepo.Name, "/")
if len(parts) != 2 {
return fmt.Errorf("invalid repository name - must contain owner and repository: %s", fullRepo.Name)
}
opts := &github.PullRequestListOptions{
State: "open",
Sort: "created",
Direction: "desc",
ListOptions: github.ListOptions{
Page: 0,
PerPage: 100,
},
}
for {
lowestNumber := 0
opts.ListOptions.Page++
owner := parts[0]
repo := parts[1]
pullRequests, resp, err := githubClient.PullRequests.List(context.TODO(), owner, repo, opts)
if err != nil {
return fmt.Errorf("error listing pull requests: %w", err)
}

for _, pullRequest := range pullRequests {
lowestNumber = *pullRequest.Number
if *pullRequest.Number < fullRepo.MinimumNumber {
// Break, not continue, since our order ensures PR numbers are getting smaller
break
}
if *pullRequest.Draft {
continue
}
user := *pullRequest.User.Login
if cfg.LabelSkipMap[user] {
continue
}
var label string
if teamMembers[user] {
label = cfg.LabelInternal
} else {
label = cfg.LabelExternal
}

if label != "" {
log.Printf("Pull Request %d by %s will be labeled %s\n", *pullRequest.Number, user, label)
hasLabel := false
for _, existingLabel := range pullRequest.Labels {
if *existingLabel.Name == label {
log.Println(" Already labeled, skipping...")
hasLabel = true
break
}
}

if !hasLabel {
allLabels := []string{label}
for _, labelP := range pullRequest.Labels {
allLabels = append(allLabels, *labelP.Name)
}
_, _, err := githubClient.Issues.AddLabelsToIssue(context.TODO(), owner, repo, *pullRequest.Number, allLabels)
if err != nil {
return fmt.Errorf("error adding labels to pull request %d: %w", *pullRequest.Number, err)
}
}
}
}

if resp.NextPage == 0 || lowestNumber <= fullRepo.MinimumNumber {
break
}
}
}

return nil
}

0 comments on commit d34bd9e

Please sign in to comment.