-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from Chia-Network/add-cmds
Add cmds
- Loading branch information
Showing
11 changed files
with
406 additions
and
69 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
log.Printf("Pending PRs ready for CI: %v\n", listPendingPRs) | ||
|
||
if !loop { | ||
break | ||
} | ||
|
||
log.Printf("Waiting %s for next iteration\n", loopDuration.String()) | ||
time.Sleep(loopDuration) | ||
} | ||
}, | ||
} | ||
|
||
func init() { | ||
rootCmd.AddCommand(notifyPendingCICmd) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.