Skip to content

Commit

Permalink
feat: support autodiscovery
Browse files Browse the repository at this point in the history
Added support for autodiscovery of repos from a list of organizations and users.

Changed configuration from env variables to a flat yaml file.

Updated readme to support new changes.

Changed how the output looks, added emojis.
  • Loading branch information
Jmainguy committed Mar 24, 2024
1 parent 70f2199 commit 8debbdf
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 139 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.19'
go-version: '1.21'

- name: Get Build Tools
run: |
Expand Down Expand Up @@ -46,7 +46,7 @@ jobs:
- name: install go
uses: actions/setup-go@v5
with:
go-version: '1.19'
go-version: '1.21'

- name: git checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-please.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: "1.20"
go-version: "1.21"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
Expand Down
62 changes: 40 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,30 @@
Application to check Github for Pull Requests, that are not Drafts, in repos the user cares about.

## Usage
The program takes no arguments, and is configured via ENV variables.
The program is configured via a YAML file located at `$HOME/.config/ghreport/config.yaml`.

* ghreportToken: Should be set to a Github API Token with access to the repos you are checking
* token: Should be set to a Github API Token with access to the repos you are checking
* Set permissions for token to repo - full control of private repositories, enable SSO if your repos require it
* ![Github Personal Access Token Permissions](https://github.com/Jmainguy/ghreport/blob/main/docs/permissions.png?raw=true)
* subscribedRepos: Should be set to a space delimmited list of Github Repos you want to check
* One of subscribedRepos, autoDiscover.organizations, or autoDiscover.users must be set if you wish to have any results. You can set all three if you wish.
* topic limits what is returned from organizations and users to just that topic, this is an optional field.

Example configuration in ~/.bashrc
```
export ghreportToken=e0e9eac4e84446df6f3db180d07bfb222e91234
export subscribedRepos="Jmainguy/ghreport Jmainguy/bible Jmainguy/ghReview Jmainguy/bak"
```
Here's an example configuration:

Additionally if you have a long list of repos to watch you can use this format when setting the environment variable:
```
export subscribedRepos="\
somesite/aebot \
somesite/ansible-okta-aws-auth \
somesite/blahblah"
```yaml
autoDiscover:
organizations:
- name: your_organization_name
topic: topic_to_watch
users:
- name: your_username
topic: topic_to_watch
subscribedRepos:
- Jmainguy/ghreport
- Jmainguy/bible
- Jmainguy/ghReview
- Jmainguy/bak
token: e0e9eac4e84446df6f3db180d07bfb222e91234
```
Running the progam
Expand All @@ -32,14 +37,27 @@ ghreport
Sample output

```
[jmainguy@jmainguy-7410 ghreport]$ ghreport
https://github.com/Jmainguy/stockop/pull/9: createdAt 2023-04-07 01:10:22 +0000 UTC
```

## Linux / macOS homebrew install

```/bin/bash
brew install jmainguy/tap/ghreport
jmainguy@fedora:~/Github/ghreport$ ./ghreport
https://github.com/Jmainguy/statuscode/pull/32
author: renovate
Age: 3 days
reviewDecision: ❌
mergeable ✅
https://github.com/Jmainguy/statuscode/pull/33
author: renovate
Age: 3 days
reviewDecision: ✅
mergeable ✅
https://github.com/Standouthost/Multicraft/pull/9
author: TheWebGamer
Age: 3321 days
reviewDecision: ✅
mergeable ❌
https://github.com/Standouthost/Multicraft/pull/28
author: ungarscool1
Age: 2700 days
reviewDecision: 😅
mergeable ✅
```

## Releases
Expand Down
129 changes: 129 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"fmt"
"os"
"path/filepath"
"strings"

"gopkg.in/yaml.v2"
)

// Config : configuration for what orgs / users / repos and token to use
type Config struct {
AutoDiscover struct {
Organizations []struct {
Name string `yaml:"name"`
Topic string `yaml:"topic"`
} `yaml:"organizations"`
Users []struct {
Name string `yaml:"name"`
Topic string `yaml:"topic"`
} `yaml:"users"`
} `yaml:"autoDiscover"`
SubscribedRepos []string `yaml:"subscribedRepos"`
Token string `yaml:"token"`
}

func ensureConfigDirExists(configDir string) error {
// Check if config directory exists
_, err := os.Stat(configDir)
if os.IsNotExist(err) {
// Config directory doesn't exist, create it
err := os.MkdirAll(configDir, 0700) // 0700 means only the owner can read, write, and execute
if err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
} else if err != nil {
// Some error occurred while checking the existence of the directory
return fmt.Errorf("failed to check config directory: %v", err)
}

return nil
}

func ensureConfigFileExists(configFilePath string) error {
_, err := os.Stat(configFilePath)
if os.IsNotExist(err) {
// Config file doesn't exist, create it with default values
defaultConfig := Config{
SubscribedRepos: []string{},
Token: "",
}
configBytes, err := yaml.Marshal(defaultConfig)
if err != nil {
return fmt.Errorf("failed to marshal default config: %v", err)
}
err = os.WriteFile(configFilePath, configBytes, 0600) // 0600 means only the owner can read and write
if err != nil {
return fmt.Errorf("failed to create config file: %v", err)
}
} else if err != nil {
// Some error occurred while checking the existence of the file
return fmt.Errorf("failed to check config file: %v", err)
}

return nil
}

func getConfigFilePath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
configDir := filepath.Join(homeDir, ".config/ghreport")
if err := ensureConfigDirExists(configDir); err != nil {
return "", err
}
configFilePath := filepath.Join(configDir, "config.yaml")
if err := ensureConfigFileExists(configFilePath); err != nil {
return "", err
}
return configFilePath, nil
}

func readConfigFile(configFilePath string) (*Config, error) {
file, err := os.Open(configFilePath)
if err != nil {
return nil, err
}
defer file.Close()

var config Config
decoder := yaml.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
return nil, err
}

return &config, nil
}

func getConfig() (*Config, error) {
// Check if config file exists
configFilePath, err := getConfigFilePath()
if err == nil {
if _, err := os.Stat(configFilePath); err == nil {
// Config file exists, read values from there
config, err := readConfigFile(configFilePath)
if err == nil {
return config, nil
}
}
}

// If config file doesn't exist or there was an error reading it, fallback to environment variables
var config Config
envSubscribedRepos := os.Getenv("subscribedRepos")
if envSubscribedRepos == "" {
return &config, fmt.Errorf("env variable subscribedRepos is not defined")
}
config.SubscribedRepos = strings.Split(envSubscribedRepos, " ")
envToken := os.Getenv("ghreportToken")
if envToken == "" {
return &config, fmt.Errorf("env variable ghreportToken is not defined")
}
config.Token = envToken

return &config, nil

}
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
module github.com/jmainguy/ghreport

go 1.19
go 1.21

require (
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
github.com/stretchr/testify v1.8.4
golang.org/x/oauth2 v0.17.0
golang.org/x/oauth2 v0.18.0
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/stretchr/objx v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.32.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
15 changes: 9 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJLvBliNGfcQgUmhlniWBDXC79oRxfZA0=
Expand All @@ -28,8 +29,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand All @@ -52,10 +53,12 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
54 changes: 29 additions & 25 deletions helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,13 @@ package main
import (
"context"
"fmt"
"os"
"strings"
"time"

"github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
)

func getEnvVariables() ([]string, string, error) {
envSubscribedRepos := os.Getenv("subscribedRepos")
if envSubscribedRepos == "" {
return nil, "", fmt.Errorf("env variable subscribedRepos is not defined")
}

subscribedRepos := strings.Split(envSubscribedRepos, " ")

envToken := os.Getenv("ghreportToken")
if envToken == "" {
return nil, "", fmt.Errorf("env variable ghreportToken is not defined")
}

return subscribedRepos, envToken, nil
}

func createGithubClient(envToken string) *githubv4.Client {
src := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: envToken},
Expand All @@ -49,14 +33,34 @@ func getOwnerAndRepo(ownerRepo string) (string, string, error) {
return ownerAndRepo[0], ownerAndRepo[1], nil
}

func compareSlices(a, b []string) bool {
if len(a) != len(b) {
return false
func formatTimeElapsed(duration time.Duration) string {
if duration.Hours() >= 24 {
return fmt.Sprintf("%.0f days", duration.Hours()/24)
} else if duration.Hours() >= 1 {
return fmt.Sprintf("%.0f hours", duration.Hours())
} else {
return fmt.Sprintf("%.0f minutes", duration.Minutes())
}
for i, v := range a {
if v != b[i] {
return false
}
}

func getReviewDecisionEmoji(decision string) string {
switch decision {
case "APPROVED":
return "✅" // Green checkmark emoji
case "CHANGES_REQUESTED":
return "❌" // Red x emoji
case "REVIEW_REQUIRED":
return "🔍" // Magnifying glass emoji or any other appropriate emoji
default:
return "😅" // Emoji indicating everything is okay, but no review was requested
}
}

func getMergeableEmoji(mergeable string) string {
if mergeable == "MERGEABLE" {
return "✅" // Green checkmark emoji
} else if mergeable == "CONFLICTING" {
return "❌" // Red x emoji
}
return true
return ""
}
Loading

0 comments on commit 8debbdf

Please sign in to comment.