diff --git a/cmd/goose/browser_rate_limiter.go b/cmd/goose/browser_rate_limiter.go index 639fb37..9f0909b 100644 --- a/cmd/goose/browser_rate_limiter.go +++ b/cmd/goose/browser_rate_limiter.go @@ -37,15 +37,20 @@ func (b *BrowserRateLimiter) CanOpen(startTime time.Time, prURL string) bool { b.mu.Lock() defer b.mu.Unlock() + slog.Info("[BROWSER] CanOpen check", + "url", prURL, + "time_since_start", time.Since(startTime).Round(time.Second), + "startup_delay", b.startupDelay) + // Check if we've already opened this PR if b.openedPRs[prURL] { - slog.Debug("[BROWSER] Skipping auto-open: PR already opened", "url", prURL) + slog.Info("[BROWSER] Skipping auto-open: PR already opened", "url", prURL) return false } // Check startup delay if time.Since(startTime) < b.startupDelay { - slog.Debug("[BROWSER] Skipping auto-open: within startup delay period", + slog.Info("[BROWSER] Skipping auto-open: within startup delay period", "remaining", b.startupDelay-time.Since(startTime)) return false } @@ -57,18 +62,19 @@ func (b *BrowserRateLimiter) CanOpen(startTime time.Time, prURL string) bool { // Check per-minute limit if len(b.openedLastMinute) >= b.maxPerMinute { - slog.Debug("[BROWSER] Rate limit: per-minute limit reached", + slog.Info("[BROWSER] Rate limit: per-minute limit reached", "opened", len(b.openedLastMinute), "max", b.maxPerMinute) return false } // Check per-day limit if len(b.openedToday) >= b.maxPerDay { - slog.Debug("[BROWSER] Rate limit: daily limit reached", + slog.Info("[BROWSER] Rate limit: daily limit reached", "opened", len(b.openedToday), "max", b.maxPerDay) return false } + slog.Info("[BROWSER] CanOpen returning true", "url", prURL) return true } diff --git a/cmd/goose/cache.go b/cmd/goose/cache.go index 574b2a5..61280d9 100644 --- a/cmd/goose/cache.go +++ b/cmd/goose/cache.go @@ -161,6 +161,16 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) ( app.healthMonitor.recordAPICall(true) } + // Log Turn API response for debugging + if data != nil { + slog.Info("[TURN] API response details", + "url", url, + "test_state", data.PullRequest.TestState, + "state", data.PullRequest.State, + "merged", data.PullRequest.Merged, + "pending_checks", len(data.PullRequest.CheckSummary.Pending)) + } + // Save to cache (don't fail if caching fails) - skip if --no-cache is set // Don't cache when tests are incomplete - always re-poll to catch completion if !app.noCache { @@ -177,7 +187,7 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) ( slog.Debug("[CACHE] Skipping cache for PR with incomplete tests", "url", url, "test_state", testState, - "pending_checks", len(data.PullRequest.CheckSummary.PendingStatuses)) + "pending_checks", len(data.PullRequest.CheckSummary.Pending)) } if shouldCache { diff --git a/cmd/goose/github.go b/cmd/goose/github.go index 6f8023b..762477d 100644 --- a/cmd/goose/github.go +++ b/cmd/goose/github.go @@ -59,6 +59,91 @@ func (app *App) initClients(ctx context.Context) error { return nil } +// initSprinklerOrgs fetches the user's organizations and starts sprinkler monitoring. +func (app *App) initSprinklerOrgs(ctx context.Context) error { + if app.client == nil || app.sprinklerMonitor == nil { + return fmt.Errorf("client or sprinkler not initialized") + } + + // Get current user + user := "" + if app.currentUser != nil { + user = app.currentUser.GetLogin() + } + if app.targetUser != "" { + user = app.targetUser + } + if user == "" { + return fmt.Errorf("no user configured") + } + + slog.Info("[SPRINKLER] Fetching user's organizations", "user", user) + + // Fetch all orgs the user is a member of with retry + opts := &github.ListOptions{PerPage: 100} + var allOrgs []string + + for { + var orgs []*github.Organization + var resp *github.Response + + err := retry.Do(func() error { + // Create timeout context for API call + apiCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + var retryErr error + orgs, resp, retryErr = app.client.Organizations.List(apiCtx, user, opts) + if retryErr != nil { + slog.Debug("[SPRINKLER] Organizations.List failed (will retry)", "error", retryErr, "page", opts.Page) + return retryErr + } + return nil + }, + retry.Attempts(maxRetries), + retry.DelayType(retry.CombineDelay(retry.BackOffDelay, retry.RandomDelay)), + retry.MaxDelay(maxRetryDelay), + retry.OnRetry(func(n uint, err error) { + slog.Warn("[SPRINKLER] Organizations.List retry", "attempt", n+1, "error", err, "page", opts.Page) + }), + retry.Context(ctx), + ) + if err != nil { + // Gracefully degrade - continue without sprinkler if org fetch fails + slog.Warn("[SPRINKLER] Failed to fetch organizations after retries, sprinkler will not start", + "error", err, + "maxRetries", maxRetries) + return nil // Return nil to avoid blocking startup + } + + for _, org := range orgs { + if org.Login != nil { + allOrgs = append(allOrgs, *org.Login) + } + } + + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage + } + + slog.Info("[SPRINKLER] Discovered user organizations", + "user", user, + "orgs", allOrgs, + "count", len(allOrgs)) + + // Update sprinkler with all orgs at once + if len(allOrgs) > 0 { + app.sprinklerMonitor.updateOrgs(allOrgs) + if err := app.sprinklerMonitor.start(); err != nil { + return fmt.Errorf("start sprinkler: %w", err) + } + } + + return nil +} + // token retrieves the GitHub token from GITHUB_TOKEN env var or gh CLI. func (*App) token(ctx context.Context) (string, error) { // Check GITHUB_TOKEN environment variable first @@ -410,22 +495,6 @@ func (app *App) fetchPRsInternal(ctx context.Context) (incoming []PR, outgoing [ // Only log summary, not individual PRs slog.Info("[GITHUB] GitHub PR summary", "incoming", len(incoming), "outgoing", len(outgoing)) - // Update sprinkler monitor with discovered orgs - app.mu.RLock() - orgs := make([]string, 0, len(app.seenOrgs)) - for org := range app.seenOrgs { - orgs = append(orgs, org) - } - app.mu.RUnlock() - - if app.sprinklerMonitor != nil && len(orgs) > 0 { - app.sprinklerMonitor.updateOrgs(orgs) - // Start monitor if not already running - if err := app.sprinklerMonitor.start(); err != nil { - slog.Warn("[SPRINKLER] Failed to start monitor", "error", err) - } - } - // Fetch Turn API data // Always synchronous now for simplicity - Turn API calls are fast with caching app.fetchTurnDataSync(ctx, allIssues, user, &incoming, &outgoing) diff --git a/cmd/goose/icons.go b/cmd/goose/icons.go index aa8bdf3..73212b7 100644 --- a/cmd/goose/icons.go +++ b/cmd/goose/icons.go @@ -2,8 +2,6 @@ package main import ( "log/slog" - "os" - "path/filepath" ) // Icon variables are defined in platform-specific files: @@ -14,12 +12,13 @@ import ( type IconType int const ( - IconSmiling IconType = iota // No blocked PRs - IconGoose // Incoming PRs blocked - IconPopper // Outgoing PRs blocked - IconBoth // Both incoming and outgoing blocked - IconWarning // General error/warning - IconLock // Authentication error + IconSmiling IconType = iota // No blocked PRs + IconGoose // Incoming PRs blocked + IconPopper // Outgoing PRs blocked + IconCockroach // Outgoing PRs blocked (fix_tests only) + IconBoth // Both incoming and outgoing blocked + IconWarning // General error/warning + IconLock // Authentication error ) // getIcon returns the icon bytes for the given type. @@ -29,6 +28,8 @@ func getIcon(iconType IconType) []byte { return iconGoose case IconPopper: return iconPopper + case IconCockroach: + return iconCockroach case IconSmiling: return iconSmiling case IconWarning: @@ -43,17 +44,6 @@ func getIcon(iconType IconType) []byte { } } -// loadIconFromFile loads an icon from the filesystem (fallback if embed fails). -func loadIconFromFile(filename string) []byte { - iconPath := filepath.Join("icons", filename) - data, err := os.ReadFile(iconPath) - if err != nil { - slog.Warn("Failed to load icon file", "path", iconPath, "error", err) - return nil - } - return data -} - // setTrayIcon updates the system tray icon based on PR counts. func (app *App) setTrayIcon(iconType IconType) { iconBytes := getIcon(iconType) diff --git a/cmd/goose/icons/cockroach.ico b/cmd/goose/icons/cockroach.ico new file mode 100644 index 0000000..bb5dae7 Binary files /dev/null and b/cmd/goose/icons/cockroach.ico differ diff --git a/cmd/goose/icons/cockroach.png b/cmd/goose/icons/cockroach.png new file mode 100644 index 0000000..b5696f1 Binary files /dev/null and b/cmd/goose/icons/cockroach.png differ diff --git a/cmd/goose/icons_unix.go b/cmd/goose/icons_unix.go index 86210b1..9a82e46 100644 --- a/cmd/goose/icons_unix.go +++ b/cmd/goose/icons_unix.go @@ -22,3 +22,6 @@ var iconLock []byte //go:embed icons/warning.png var iconWarning []byte + +//go:embed icons/cockroach.png +var iconCockroach []byte diff --git a/cmd/goose/icons_windows.go b/cmd/goose/icons_windows.go index 1afb253..4a9af74 100644 --- a/cmd/goose/icons_windows.go +++ b/cmd/goose/icons_windows.go @@ -20,5 +20,8 @@ var iconSmiling []byte //go:embed icons/warning.ico var iconWarning []byte +//go:embed icons/cockroach.ico +var iconCockroach []byte + // lock.ico not yet created, using warning as fallback -var iconLock = iconWarning \ No newline at end of file +var iconLock = iconWarning diff --git a/cmd/goose/main.go b/cmd/goose/main.go index 06bc848..4cd83f5 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -227,6 +227,9 @@ func main() { githubCircuit: newCircuitBreaker("github", 5, 2*time.Minute), } + // Set app reference in health monitor for sprinkler status + app.healthMonitor.app = app + // Load saved settings app.loadSettings() @@ -270,6 +273,13 @@ func main() { if app.targetUser != "" && app.targetUser != user.GetLogin() { slog.Info("Querying PRs for different user", "targetUser", sanitizeForLog(app.targetUser)) } + + // Initialize sprinkler with user's organizations now that we have the user + go func() { + if err := app.initSprinklerOrgs(ctx); err != nil { + slog.Warn("[SPRINKLER] Failed to initialize organizations", "error", err) + } + }() } else { slog.Warn("GitHub API returned nil user") } @@ -822,7 +832,14 @@ func (app *App) updatePRsWithWait(ctx context.Context) { // tryAutoOpenPR attempts to open a PR in the browser if enabled and rate limits allow. func (app *App) tryAutoOpenPR(ctx context.Context, pr PR, autoBrowserEnabled bool, startTime time.Time) { + slog.Debug("[BROWSER] tryAutoOpenPR called", + "repo", pr.Repository, + "number", pr.Number, + "enabled", autoBrowserEnabled, + "time_since_start", time.Since(startTime).Round(time.Second)) + if !autoBrowserEnabled { + slog.Debug("[BROWSER] Auto-open disabled, skipping") return } diff --git a/cmd/goose/reliability.go b/cmd/goose/reliability.go index 5882181..f522716 100644 --- a/cmd/goose/reliability.go +++ b/cmd/goose/reliability.go @@ -114,6 +114,7 @@ type healthMonitor struct { apiErrors int64 cacheHits int64 cacheMisses int64 + app *App // Reference to app for accessing sprinkler status } func newHealthMonitor() *healthMonitor { @@ -174,10 +175,24 @@ func (hm *healthMonitor) getMetrics() map[string]interface{} { func (hm *healthMonitor) logMetrics() { metrics := hm.getMetrics() + + // Get sprinkler connection status + sprinklerConnected := false + sprinklerLastConnected := "" + if hm.app.sprinklerMonitor != nil { + connected, lastConnectedAt := hm.app.sprinklerMonitor.connectionStatus() + sprinklerConnected = connected + if !lastConnectedAt.IsZero() { + sprinklerLastConnected = time.Since(lastConnectedAt).Round(time.Second).String() + " ago" + } + } + slog.Info("[HEALTH] Application metrics", "uptime", metrics["uptime"], "api_calls", metrics["api_calls"], "api_errors", metrics["api_errors"], "error_rate_pct", fmt.Sprintf("%.1f", metrics["error_rate"]), - "cache_hit_rate_pct", fmt.Sprintf("%.1f", metrics["cache_hit_rate"])) + "cache_hit_rate_pct", fmt.Sprintf("%.1f", metrics["cache_hit_rate"]), + "sprinkler_connected", sprinklerConnected, + "sprinkler_last_connected", sprinklerLastConnected) } diff --git a/cmd/goose/sprinkler.go b/cmd/goose/sprinkler.go index 2c8dd90..bf2f067 100644 --- a/cmd/goose/sprinkler.go +++ b/cmd/goose/sprinkler.go @@ -27,16 +27,18 @@ const ( // sprinklerMonitor manages WebSocket event subscriptions for all user orgs. type sprinklerMonitor struct { - app *App - client *client.Client - cancel context.CancelFunc - eventChan chan string // Channel for PR URLs that need checking - lastEventMap map[string]time.Time // Track last event per URL to dedupe - token string - orgs []string - ctx context.Context - mu sync.RWMutex - isRunning bool + app *App + client *client.Client + cancel context.CancelFunc + eventChan chan string // Channel for PR URLs that need checking + lastEventMap map[string]time.Time // Track last event per URL to dedupe + token string + orgs []string + ctx context.Context + mu sync.RWMutex + isRunning bool + isConnected bool // Track WebSocket connection status + lastConnectedAt time.Time // Last successful connection time } // newSprinklerMonitor creates a new sprinkler monitor for real-time PR events. @@ -53,38 +55,19 @@ func newSprinklerMonitor(app *App, token string) *sprinklerMonitor { } } -// updateOrgs updates the list of organizations to monitor. +// updateOrgs sets the list of organizations to monitor. func (sm *sprinklerMonitor) updateOrgs(orgs []string) { sm.mu.Lock() defer sm.mu.Unlock() - // Check if orgs changed - if len(orgs) == len(sm.orgs) { - same := true - for i := range orgs { - if orgs[i] != sm.orgs[i] { - same = false - break - } - } - if same { - return // No change - } + if len(orgs) == 0 { + slog.Debug("[SPRINKLER] No organizations provided") + return } - slog.Info("[SPRINKLER] Updating monitored organizations", "orgs", orgs) + slog.Info("[SPRINKLER] Setting organizations", "orgs", orgs, "count", len(orgs)) sm.orgs = make([]string, len(orgs)) copy(sm.orgs, orgs) - - // Restart if running - if sm.isRunning { - slog.Info("[SPRINKLER] Restarting monitor with new org list") - sm.stop() - sm.ctx, sm.cancel = context.WithCancel(context.Background()) - if err := sm.start(); err != nil { - slog.Error("[SPRINKLER] Failed to restart", "error", err) - } - } } // start begins monitoring for PR events across all user orgs. @@ -127,9 +110,16 @@ func (sm *sprinklerMonitor) start() error { NoReconnect: false, Logger: sprinklerLogger, OnConnect: func() { + sm.mu.Lock() + sm.isConnected = true + sm.lastConnectedAt = time.Now() + sm.mu.Unlock() slog.Info("[SPRINKLER] WebSocket connected") }, OnDisconnect: func(err error) { + sm.mu.Lock() + sm.isConnected = false + sm.mu.Unlock() if err != nil && !errors.Is(err, context.Canceled) { slog.Warn("[SPRINKLER] WebSocket disconnected", "error", err) } @@ -269,6 +259,12 @@ func (sm *sprinklerMonitor) handleEvent(event client.Event) { // processEvents handles PR events by checking if they're blocking and notifying. func (sm *sprinklerMonitor) processEvents() { + defer func() { + if r := recover(); r != nil { + slog.Error("[SPRINKLER] Event processor panic", "panic", r) + } + }() + for { select { case <-sm.ctx.Done(): @@ -550,6 +546,13 @@ func (sm *sprinklerMonitor) stop() { sm.isRunning = false } +// connectionStatus returns the current WebSocket connection status. +func (sm *sprinklerMonitor) connectionStatus() (connected bool, lastConnectedAt time.Time) { + sm.mu.RLock() + defer sm.mu.RUnlock() + return sm.isConnected, sm.lastConnectedAt +} + // parseRepoAndNumberFromURL extracts repo and PR number from URL. func parseRepoAndNumberFromURL(url string) (repo string, number int) { // URL format: https://github.com/org/repo/pull/123 diff --git a/cmd/goose/ui.go b/cmd/goose/ui.go index dc76ef2..8a1816e 100644 --- a/cmd/goose/ui.go +++ b/cmd/goose/ui.go @@ -190,6 +190,21 @@ func (app *App) countPRs() PRCounts { func (app *App) setTrayTitle() { counts := app.countPRs() + // Check if all outgoing blocked PRs are fix_tests only + allOutgoingAreFixTests := false + if counts.OutgoingBlocked > 0 && counts.IncomingBlocked == 0 { + app.mu.RLock() + allFixTests := true + for i := range app.outgoing { + if app.outgoing[i].IsBlocked && app.outgoing[i].ActionKind != "fix_tests" { + allFixTests = false + break + } + } + app.mu.RUnlock() + allOutgoingAreFixTests = allFixTests + } + // Set title and icon based on PR state var title string var iconType IconType @@ -210,7 +225,11 @@ func (app *App) setTrayTitle() { iconType = IconGoose default: title = fmt.Sprintf("%d", counts.OutgoingBlocked) - iconType = IconPopper + if allOutgoingAreFixTests { + iconType = IconCockroach + } else { + iconType = IconPopper + } } } else { // All other platforms: icon only, no text @@ -223,7 +242,11 @@ func (app *App) setTrayTitle() { case counts.IncomingBlocked > 0: iconType = IconGoose default: - iconType = IconPopper + if allOutgoingAreFixTests { + iconType = IconCockroach + } else { + iconType = IconPopper + } } } diff --git a/go.mod b/go.mod index 5aba8e4..d7a1393 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ -module github.com/ready-to-review/goose +module github.com/codeGROOVE-dev/goose go 1.24.0 require ( github.com/codeGROOVE-dev/retry v1.2.0 - github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582 - github.com/codeGROOVE-dev/turnclient v0.0.0-20250922145707-664c2dcdf5b8 + github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001154245-068712aa969d + github.com/codeGROOVE-dev/turnclient v0.0.0-20251001194229-2aaea2e63cc7 github.com/energye/systray v1.0.2 github.com/gen2brain/beeep v0.11.1 github.com/google/go-github/v57 v57.0.0 @@ -14,11 +14,10 @@ require ( require ( git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect - github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 // indirect + github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c // indirect github.com/esiqveland/notify v0.13.3 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/jackmordaunt/icns/v3 v3.0.1 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect diff --git a/go.sum b/go.sum index 6151a8d..8781f1b 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE= git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo= -github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 h1:9eLzQdOaQEn30279ai3YjNdJOM/efbcYanWC9juAJ+M= -github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= +github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c h1:/rrjFoqwFqKNzc1f14vQt6QJ9U5tQ4Uh6U8hgixkSqw= +github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= github.com/codeGROOVE-dev/retry v1.2.0 h1:xYpYPX2PQZmdHwuiQAGGzsBm392xIMl4nfMEFApQnu8= github.com/codeGROOVE-dev/retry v1.2.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582 h1:IPCaNGRWdyMZKyjnjv+wdSmPmOZtKFD6SVaha5DuCqk= -github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= -github.com/codeGROOVE-dev/turnclient v0.0.0-20250922145707-664c2dcdf5b8 h1:3088TLJGgxzjM/bR1gafKQ609NMkBNlZe1Fd5SnRrrY= -github.com/codeGROOVE-dev/turnclient v0.0.0-20250922145707-664c2dcdf5b8/go.mod h1:7lBF4vS6T+D1rNjmJ+CNVrXALQvdwNfBVEy7vhIQtYk= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001154245-068712aa969d h1:tQX67dAzyWVAaNe0EhYpj/AmWZo1Wwhaah6KVj7M3qs= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001154245-068712aa969d/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251001194229-2aaea2e63cc7 h1:iRWrlJusl5FiEC1sCiPoW8IFei5bebAIornGtQFUHbc= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251001194229-2aaea2e63cc7/go.mod h1:Rt0k+aoZ13TvXKl9n2AeBabWIXZ/xIvurdwFTgyNk0w= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -23,8 +23,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=