Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cmds #9

Merged
merged 20 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
01eccf7
Start creating cmd files for stale and prs needing CI
pmaslana May 3, 2024
f6f5378
Add additional commands for checking stale and pending PRs
pmaslana May 3, 2024
6529935
Return a list of URLS instead of PR name
pmaslana May 3, 2024
32d19c2
Change the format of checkPendingCI
pmaslana May 3, 2024
3f1850b
Run go mod tidy
pmaslana May 4, 2024
ffcb782
Added more refinements to the checkPendingCI.go file
pmaslana May 7, 2024
6310d0a
Fix error handling for CheckStalePrs and include a check for if a PR …
pmaslana May 8, 2024
697cf28
Change the cutoffDate time for checkStalePRs.go
pmaslana May 8, 2024
510e0f6
Changed the isStale timeout to 300s
pmaslana May 8, 2024
7bfd1c4
Add the appropriate deployment args to the notify_stale_pending_prs.y…
pmaslana May 8, 2024
fc6e017
Break out of the for loop when if *pullRequest.Number < fullRepo.Mini…
pmaslana May 8, 2024
b2b5421
In the checkForDismissedReviews, the code changes to only check the s…
pmaslana May 8, 2024
990cd39
Changed the check for dismissed to include other states of the review
pmaslana May 9, 2024
803d648
Add comments
pmaslana May 9, 2024
4325eaf
Add some details when printing errors
pmaslana May 13, 2024
5690aed
Fix a print line variable
pmaslana May 13, 2024
abe79ab
Add in a "/" between owner repo
pmaslana May 13, 2024
33670b6
Transition to make the context for the isStale function per iteration…
pmaslana May 14, 2024
3b6a497
Fix the comment for the context in isStale
pmaslana May 14, 2024
59c21f2
Add a sleep to the notifyPendingCI and notifyStale files if getting a…
pmaslana May 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions cmd/notifyPendingCI.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cmd

import (
"log"
"time"

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

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

var notifyPendingCICmd = &cobra.Command{
Use: "notify-pendingci",
Short: "Sends a Keybase message to a channel, alerting that a community PR is ready for CI to run",
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)

loop := viper.GetBool("loop")
loopDuration := viper.GetDuration("loop-time")
var listPendingPRs []string
for {
log.Println("Checking for community PRs that are waiting for CI to run")
listPendingPRs, err = github2.CheckForPendingCI(client, cfg.InternalTeam, cfg.CheckStalePending)
if err != nil {
log.Printf("The following error occurred while obtaining a list of pending PRs: %s", err)
time.Sleep(loopDuration)
continue
Starttoaster marked this conversation as resolved.
Show resolved Hide resolved
}
log.Printf("Pending PRs ready for CI: %v\n", listPendingPRs)

if !loop {
Starttoaster marked this conversation as resolved.
Show resolved Hide resolved
break
}

log.Printf("Waiting %s for next iteration\n", loopDuration.String())
time.Sleep(loopDuration)
}
},
}

func init() {
rootCmd.AddCommand(notifyPendingCICmd)
}
49 changes: 49 additions & 0 deletions cmd/notifyStale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"log"
"time"

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

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

var notifyStaleCmd = &cobra.Command{
Use: "notify-stale",
Short: "Sends a Keybase message to a channel, alerting that a community PR has not been updated in 7 days",
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)

loop := viper.GetBool("loop")
loopDuration := viper.GetDuration("loop-time")
var listPendingPRs []string
for {
log.Println("Checking for community PRs that have no update in the last 7 days")
_, err = github2.CheckStalePRs(client, cfg.InternalTeam, cfg.CheckStalePending)
if err != nil {
log.Printf("The following error occurred while obtaining a list of stale PRs: %s", err)
time.Sleep(loopDuration)
continue
Starttoaster marked this conversation as resolved.
Show resolved Hide resolved
}
log.Printf("Stale PRs: %v\n", listPendingPRs)
if !loop {
break
}

log.Printf("Waiting %s for next iteration\n", loopDuration.String())
time.Sleep(loopDuration)
}
},
}

func init() {
rootCmd.AddCommand(notifyStaleCmd)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
Expand All @@ -28,5 +29,6 @@ require (
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down Expand Up @@ -71,8 +75,8 @@ 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=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
12 changes: 9 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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"`
GithubToken string `yaml:"github_token"`
InternalTeam string `yaml:"internal_team"`
LabelConfig `yaml:",inline"`
CheckStalePending `yaml:",inline"`
}

// LabelConfig is the configuration options specific to labeling PRs
Expand All @@ -21,3 +22,8 @@ type CheckRepo struct {
Name string `yaml:"name"`
MinimumNumber int `yaml:"minimum_number"`
}

// CheckStalePending are config settings when checking a repo
type CheckStalePending struct {
CheckStalePending []CheckRepo `yaml:"check_stale_pending_repos"`
}
128 changes: 128 additions & 0 deletions internal/github/checkPendingCI.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package github

import (
"context"
"fmt"
"log"
"strconv"
"strings"
"time"

"github.com/google/go-github/v60/github" // Ensure your go-github library version matches

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

// CheckForPendingCI returns a list of PR URLs that are ready for CI to run but haven't started yet.
func CheckForPendingCI(githubClient *github.Client, internalTeam string, cfg config.CheckStalePending) ([]string, error) {
teamMembers, _ := GetTeamMemberList(githubClient, internalTeam)
var pendingPRs []string

for _, fullRepo := range cfg.CheckStalePending {
log.Println("Checking repository:", fullRepo.Name)
parts := strings.Split(fullRepo.Name, "/")
if len(parts) != 2 {
log.Printf("invalid repository name - must contain owner and repository: %s", fullRepo.Name)
continue
}
owner, repo := parts[0], parts[1]

// Fetch community PRs using the FindCommunityPRs function
communityPRs, err := FindCommunityPRs(cfg.CheckStalePending, teamMembers, githubClient)
if err != nil {
return nil, err
}

for _, pr := range communityPRs {
// Dynamic cutoff time based on the last commit to the PR
lastCommitTime, err := getLastCommitTime(githubClient, owner, repo, pr.GetNumber())
if err != nil {
log.Printf("Error retrieving last commit time for PR #%d in %s/%s: %v", pr.GetNumber(), owner, repo, err)
continue
}
cutoffTime := lastCommitTime.Add(2 * time.Hour) // 2 hours after the last commit

if time.Now().Before(cutoffTime) {
log.Printf("Skipping PR #%d from %s/%s repo as it's still within the 2-hour window from the last commit.", pr.GetNumber(), owner, repo)
continue
}

hasCIRuns, err := checkCIStatus(githubClient, owner, repo, pr.GetNumber())
if err != nil {
log.Printf("Error checking CI status for PR #%d in %s/%s: %v", pr.GetNumber(), owner, repo, err)
continue
}

teamMemberActivity, err := checkTeamMemberActivity(githubClient, owner, repo, pr.GetNumber(), teamMembers, lastCommitTime)
if err != nil {
log.Printf("Error checking team member activity for PR #%d in %s/%s: %v", pr.GetNumber(), owner, repo, err)
continue // or handle the error as needed
}
if !hasCIRuns || !teamMemberActivity {
log.Printf("PR #%d in %s/%s by %s is ready for CI since %v but no CI actions have started yet, or it requires re-approval.", pr.GetNumber(), owner, repo, pr.User.GetLogin(), pr.CreatedAt)
pendingPRs = append(pendingPRs, pr.GetHTMLURL())
}
}
}
return pendingPRs, nil
}

func getLastCommitTime(client *github.Client, owner, repo string, prNumber int) (time.Time, error) {
commits, _, err := client.PullRequests.ListCommits(context.Background(), owner, repo, prNumber, nil)
if err != nil {
return time.Time{}, err // Properly handle API errors
}
if len(commits) == 0 {
return time.Time{}, fmt.Errorf("no commits found for PR #%d", prNumber) // Handle case where no commits are found
}
// Requesting a list of commits will return the json list in descending order
lastCommit := commits[len(commits)-1]
commitDate := lastCommit.GetCommit().GetAuthor().GetDate() // commitDate is of type Timestamp

// Since GetDate() returns a Timestamp (not *Timestamp), use the address to call GetTime()
commitTime := commitDate.GetTime() // Correctly accessing GetTime(), which returns *time.Time

if commitTime == nil {
return time.Time{}, fmt.Errorf("commit time is nil for PR #%d", prNumber)
}
return *commitTime, nil // Safely dereference *time.Time to get time.Time
}

func checkCIStatus(client *github.Client, owner, repo string, prNumber int) (bool, error) {
checks, _, err := client.Checks.ListCheckRunsForRef(context.Background(), owner, repo, strconv.Itoa(prNumber), &github.ListCheckRunsOptions{})
if err != nil {
return false, err
}
return checks.GetTotal() > 0, nil
}

func checkTeamMemberActivity(client *github.Client, owner, repo string, prNumber int, teamMembers map[string]bool, lastCommitTime time.Time) (bool, error) {
comments, _, err := client.Issues.ListComments(context.Background(), owner, repo, prNumber, nil)
if err != nil {
return false, fmt.Errorf("failed to fetch comments: %w", err)
}

for _, comment := range comments {
if _, ok := teamMembers[comment.User.GetLogin()]; ok && comment.CreatedAt.After(lastCommitTime) {
// Check if the comment is after the last commit
return true, nil // Active and relevant participation
}
}

reviews, _, err := client.PullRequests.ListReviews(context.Background(), owner, repo, prNumber, nil)
if err != nil {
return false, fmt.Errorf("failed to fetch reviews: %w", err)
}

for _, review := range reviews {
if _, ok := teamMembers[review.User.GetLogin()]; ok && review.SubmittedAt.After(lastCommitTime) {
switch review.GetState() {
case "DISMISSED", "CHANGES_REQUESTED", "COMMENTED":
// Check if the review is after the last commit and is in one of the specified states
return true, nil
}
}
}

return false, nil // No recent relevant activity from team members
}
69 changes: 69 additions & 0 deletions internal/github/checkStalePRs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package github

import (
"context"
"fmt"
"log"
"time"

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

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

// CheckStalePRs will return a list of PR URLs that have not been updated in the last 7 days by internal team members.
func CheckStalePRs(githubClient *github.Client, internalTeam string, cfg config.CheckStalePending) ([]string, error) {
var stalePRUrls []string
cutoffDate := time.Now().Add(7 * 24 * time.Hour) // 7 days ago
teamMembers, err := GetTeamMemberList(githubClient, internalTeam)
if err != nil {
return nil, err
}
communityPRs, err := FindCommunityPRs(cfg.CheckStalePending, teamMembers, githubClient)
if err != nil {
return nil, err
}

for _, pr := range communityPRs {
repoName := pr.GetBase().GetRepo().GetFullName() // Get the full name of the repository
stale, err := isStale(githubClient, pr, teamMembers, cutoffDate) // Handle both returned values
if err != nil {
log.Printf("Error checking if PR in repo %s is stale: %v", repoName, err)
continue // Skip this PR or handle the error appropriately
}
if stale {
stalePRUrls = append(stalePRUrls, pr.GetHTMLURL()) // Append if PR is confirmed stale
}
}
return stalePRUrls, nil
}

// Checks if a PR is stale based on the last update from team members
func isStale(githubClient *github.Client, pr *github.PullRequest, teamMembers map[string]bool, cutoffDate time.Time) (bool, error) {
listOptions := &github.ListOptions{PerPage: 100}
for {
// Create a context for each request
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // 30 seconds timeout for each request

events, resp, err := githubClient.Issues.ListIssueTimeline(ctx, pr.Base.Repo.Owner.GetLogin(), pr.Base.Repo.GetName(), pr.GetNumber(), listOptions)
if err != nil {
cancel() // Explicitly cancel the context when an error occurs
return false, fmt.Errorf("failed to get timeline for PR #%d: %w", pr.GetNumber(), err)
}
for _, event := range events {
if event.Event == nil || event.Actor == nil || event.Actor.Login == nil {
continue
}
if (*event.Event == "commented" || *event.Event == "reviewed") && teamMembers[*event.Actor.Login] && event.CreatedAt.After(cutoffDate) {
cancel() // Clean up the context when returning within the loop
return false, nil
}
}
cancel() // Clean up the context at the end of the loop iteration
if resp.NextPage == 0 {
break
}
listOptions.Page = resp.NextPage
}
return true, nil
}
Loading