From 4b598832d13ebdf00b7195e6ceeb8795099df860 Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Sun, 19 Oct 2025 12:29:23 +0200 Subject: [PATCH 1/2] update libs, improve linting --- .golangci.yml | 25 ++--- Makefile | 16 ++- cmd/goose/browser_rate_limiter.go | 1 - cmd/goose/cache.go | 120 ++++++++++++--------- cmd/goose/github.go | 15 ++- cmd/goose/icons.go | 10 +- cmd/goose/loginitem_darwin.go | 1 - cmd/goose/main.go | 170 ++++++++++++++++-------------- cmd/goose/main_test.go | 6 +- cmd/goose/multihandler.go | 7 +- cmd/goose/notifications.go | 9 +- cmd/goose/pr_state.go | 1 - cmd/goose/ratelimit.go | 1 - cmd/goose/reliability.go | 24 ++--- cmd/goose/security.go | 13 ++- cmd/goose/settings.go | 1 - cmd/goose/sound.go | 1 - cmd/goose/sprinkler.go | 128 +++++++++++----------- cmd/goose/systray_interface.go | 4 +- cmd/goose/ui.go | 23 ++-- go.mod | 12 +-- go.sum | 12 +++ 22 files changed, 326 insertions(+), 274 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 74e86ed..3e2f4d9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,6 +40,7 @@ linters: - gochecknoglobals # checks that no global variables exist - cyclop # replaced by revive - gocyclo # replaced by revive + - forbidigo # needs configuration to be useful - funlen # replaced by revive - godox # TODO's are OK - ireturn # It's OK @@ -151,10 +152,10 @@ linters: nakedret: # Default: 30 - max-func-lines: 4 + max-func-lines: 7 nestif: - min-complexity: 12 + min-complexity: 15 nolintlint: # Exclude following linters from requiring an explanation. @@ -170,17 +171,11 @@ linters: rules: - name: add-constant severity: warning - disabled: false - exclude: [""] - arguments: - - max-lit-count: "5" - allow-strs: '"","\n"' - allow-ints: "0,1,2,3,24,30,365,1024,0o600,0o700,0o750,0o755" - allow-floats: "0.0,0.,1.0,1.,2.0,2." + disabled: true - name: cognitive-complexity - arguments: [55] + disabled: true # prefer maintidx - name: cyclomatic - arguments: [60] + disabled: true # prefer maintidx - name: function-length arguments: [150, 225] - name: line-length-limit @@ -212,8 +207,14 @@ linters: os-temp-dir: true varnamelen: - max-distance: 40 + max-distance: 75 min-name-length: 2 + check-receivers: false + ignore-names: + - r + - w + - f + - err exclusions: # Default: [] diff --git a/Makefile b/Makefile index 7a6bc52..c13fa13 100644 --- a/Makefile +++ b/Makefile @@ -204,7 +204,7 @@ LINTERS := FIXERS := GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml -GOLANGCI_LINT_VERSION ?= v2.3.1 +GOLANGCI_LINT_VERSION ?= v2.5.0 GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH) $(GOLANGCI_LINT_BIN): mkdir -p $(LINT_ROOT)/out/linters @@ -234,9 +234,19 @@ yamllint-lint: $(YAMLLINT_BIN) PYTHONPATH=$(YAMLLINT_ROOT)/dist $(YAMLLINT_ROOT)/dist/bin/yamllint . .PHONY: _lint $(LINTERS) -_lint: $(LINTERS) +_lint: + @exit_code=0; \ + for target in $(LINTERS); do \ + $(MAKE) $$target || exit_code=1; \ + done; \ + exit $$exit_code .PHONY: fix $(FIXERS) -fix: $(FIXERS) +fix: + @exit_code=0; \ + for target in $(FIXERS); do \ + $(MAKE) $$target || exit_code=1; \ + done; \ + exit $$exit_code # END: lint-install . diff --git a/cmd/goose/browser_rate_limiter.go b/cmd/goose/browser_rate_limiter.go index 9f0909b..7aef4c6 100644 --- a/cmd/goose/browser_rate_limiter.go +++ b/cmd/goose/browser_rate_limiter.go @@ -1,4 +1,3 @@ -// Package main implements browser rate limiting for PR auto-open feature. package main import ( diff --git a/cmd/goose/cache.go b/cmd/goose/cache.go index 61280d9..b5e7820 100644 --- a/cmd/goose/cache.go +++ b/cmd/goose/cache.go @@ -1,4 +1,3 @@ -// Package main - cache.go provides caching functionality for Turn API responses. package main import ( @@ -23,6 +22,70 @@ type cacheEntry struct { UpdatedAt time.Time `json:"updated_at"` } +// checkCache checks the cache for a PR and returns the cached data if valid. +// Returns (cachedData, cacheHit, hasRunningTests). +func (app *App) checkCache(cacheFile, url string, updatedAt time.Time) (cachedData *turn.CheckResponse, cacheHit bool, hasRunningTests bool) { + fileData, readErr := os.ReadFile(cacheFile) + if readErr != nil { + if !os.IsNotExist(readErr) { + slog.Debug("[CACHE] Cache file read error", "url", url, "error", readErr) + } + return nil, false, false + } + + var entry cacheEntry + if unmarshalErr := json.Unmarshal(fileData, &entry); unmarshalErr != nil { + slog.Warn("Failed to unmarshal cache data", "url", url, "error", unmarshalErr) + // Remove corrupted cache file + if removeErr := os.Remove(cacheFile); removeErr != nil { + slog.Error("Failed to remove corrupted cache file", "error", removeErr) + } + return nil, false, false + } + + // Check if cache is expired or PR updated + if time.Since(entry.CachedAt) >= cacheTTL || !entry.UpdatedAt.Equal(updatedAt) { + // Log why cache was invalid + if !entry.UpdatedAt.Equal(updatedAt) { + slog.Debug("[CACHE] Cache miss - PR updated", + "url", url, + "cached_pr_time", entry.UpdatedAt.Format(time.RFC3339), + "current_pr_time", updatedAt.Format(time.RFC3339)) + } else { + slog.Debug("[CACHE] Cache miss - TTL expired", + "url", url, + "cached_at", entry.CachedAt.Format(time.RFC3339), + "cache_age", time.Since(entry.CachedAt).Round(time.Second), + "ttl", cacheTTL) + } + return nil, false, false + } + + // Check for incomplete tests that should invalidate cache + cacheAge := time.Since(entry.CachedAt) + testState := entry.Data.PullRequest.TestState + isTestIncomplete := testState == "running" || testState == "queued" || testState == "pending" + if entry.Data != nil && isTestIncomplete && cacheAge < runningTestsCacheBypass { + slog.Debug("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh", + "url", url, + "test_state", testState, + "cache_age", cacheAge.Round(time.Minute), + "cached_at", entry.CachedAt.Format(time.RFC3339)) + return nil, false, true + } + + // Cache hit + slog.Debug("[CACHE] Cache hit", + "url", url, + "cached_at", entry.CachedAt.Format(time.RFC3339), + "cache_age", time.Since(entry.CachedAt).Round(time.Second), + "pr_updated_at", entry.UpdatedAt.Format(time.RFC3339)) + if app.healthMonitor != nil { + app.healthMonitor.recordCacheAccess(true) + } + return entry.Data, true, false +} + // turnData fetches Turn API data with caching. func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (*turn.CheckResponse, bool, error) { hasRunningTests := false @@ -45,57 +108,10 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) ( // Skip cache if --no-cache flag is set if !app.noCache { - // Try to read from cache (gracefully handle all cache errors) - if data, readErr := os.ReadFile(cacheFile); readErr == nil { - var entry cacheEntry - if unmarshalErr := json.Unmarshal(data, &entry); unmarshalErr != nil { - slog.Warn("Failed to unmarshal cache data", "url", url, "error", unmarshalErr) - // Remove corrupted cache file - if removeErr := os.Remove(cacheFile); removeErr != nil { - slog.Error("Failed to remove corrupted cache file", "error", removeErr) - } - } else if time.Since(entry.CachedAt) < cacheTTL && entry.UpdatedAt.Equal(updatedAt) { - // Check if cache is still valid (10 day TTL, but PR UpdatedAt is primary check) - // But invalidate cache for PRs with incomplete tests if cache entry is fresh (< 90 minutes old) - cacheAge := time.Since(entry.CachedAt) - testState := entry.Data.PullRequest.TestState - isTestIncomplete := testState == "running" || testState == "queued" || testState == "pending" - if entry.Data != nil && isTestIncomplete && cacheAge < runningTestsCacheBypass { - hasRunningTests = true - slog.Debug("[CACHE] Cache invalidated - tests incomplete and cache entry is fresh", - "url", url, - "test_state", testState, - "cache_age", cacheAge.Round(time.Minute), - "cached_at", entry.CachedAt.Format(time.RFC3339)) - // Don't return cached data - fall through to fetch fresh data with current time - } else { - slog.Debug("[CACHE] Cache hit", - "url", url, - "cached_at", entry.CachedAt.Format(time.RFC3339), - "cache_age", time.Since(entry.CachedAt).Round(time.Second), - "pr_updated_at", entry.UpdatedAt.Format(time.RFC3339)) - if app.healthMonitor != nil { - app.healthMonitor.recordCacheAccess(true) - } - return entry.Data, true, nil - } - } else { - // Log why cache was invalid - if !entry.UpdatedAt.Equal(updatedAt) { - slog.Debug("[CACHE] Cache miss - PR updated", - "url", url, - "cached_pr_time", entry.UpdatedAt.Format(time.RFC3339), - "current_pr_time", updatedAt.Format(time.RFC3339)) - } else if time.Since(entry.CachedAt) >= cacheTTL { - slog.Debug("[CACHE] Cache miss - TTL expired", - "url", url, - "cached_at", entry.CachedAt.Format(time.RFC3339), - "cache_age", time.Since(entry.CachedAt).Round(time.Second), - "ttl", cacheTTL) - } - } - } else if !os.IsNotExist(readErr) { - slog.Debug("[CACHE] Cache file read error", "url", url, "error", readErr) + if cachedData, cacheHit, runningTests := app.checkCache(cacheFile, url, updatedAt); cacheHit { + return cachedData, true, nil + } else if runningTests { + hasRunningTests = true } } diff --git a/cmd/goose/github.go b/cmd/goose/github.go index 762477d..2a926fe 100644 --- a/cmd/goose/github.go +++ b/cmd/goose/github.go @@ -1,4 +1,3 @@ -// Package main - github.go contains GitHub API integration functions. package main import ( @@ -62,7 +61,7 @@ func (app *App) initClients(ctx context.Context) error { // 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") + return errors.New("client or sprinkler not initialized") } // Get current user @@ -74,7 +73,7 @@ func (app *App) initSprinklerOrgs(ctx context.Context) error { user = app.targetUser } if user == "" { - return fmt.Errorf("no user configured") + return errors.New("no user configured") } slog.Info("[SPRINKLER] Fetching user's organizations", "user", user) @@ -136,7 +135,7 @@ func (app *App) initSprinklerOrgs(ctx context.Context) error { // Update sprinkler with all orgs at once if len(allOrgs) > 0 { app.sprinklerMonitor.updateOrgs(allOrgs) - if err := app.sprinklerMonitor.start(); err != nil { + if err := app.sprinklerMonitor.start(ctx); err != nil { return fmt.Errorf("start sprinkler: %w", err) } } @@ -287,7 +286,13 @@ func (app *App) executeGitHubQuery(ctx context.Context, query string, opts *gith return result, err } -func (app *App) executeGitHubQueryInternal(ctx context.Context, query string, opts *github.SearchOptions, result **github.IssuesSearchResult, resp **github.Response) error { +func (app *App) executeGitHubQueryInternal( + ctx context.Context, + query string, + opts *github.SearchOptions, + result **github.IssuesSearchResult, + resp **github.Response, +) error { return retry.Do(func() error { // Create timeout context for GitHub API call githubCtx, cancel := context.WithTimeout(ctx, 30*time.Second) diff --git a/cmd/goose/icons.go b/cmd/goose/icons.go index 73212b7..be91416 100644 --- a/cmd/goose/icons.go +++ b/cmd/goose/icons.go @@ -24,21 +24,17 @@ const ( // getIcon returns the icon bytes for the given type. func getIcon(iconType IconType) []byte { switch iconType { - case IconGoose: + case IconGoose, IconBoth: + // For both, we'll use the goose icon as primary return iconGoose case IconPopper: return iconPopper case IconCockroach: return iconCockroach - case IconSmiling: - return iconSmiling case IconWarning: return iconWarning case IconLock: return iconLock - case IconBoth: - // For both, we'll use the goose icon as primary - return iconGoose default: return iconSmiling } @@ -47,7 +43,7 @@ func getIcon(iconType IconType) []byte { // setTrayIcon updates the system tray icon based on PR counts. func (app *App) setTrayIcon(iconType IconType) { iconBytes := getIcon(iconType) - if iconBytes == nil || len(iconBytes) == 0 { + if len(iconBytes) == 0 { slog.Warn("Icon bytes are empty, skipping icon update", "type", iconType) return } diff --git a/cmd/goose/loginitem_darwin.go b/cmd/goose/loginitem_darwin.go index 5775c92..6d98073 100644 --- a/cmd/goose/loginitem_darwin.go +++ b/cmd/goose/loginitem_darwin.go @@ -1,6 +1,5 @@ //go:build darwin -// Package main - loginitem_darwin.go provides macOS-specific login item management. package main import ( diff --git a/cmd/goose/main.go b/cmd/goose/main.go index 4cd83f5..5db7335 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -84,9 +84,11 @@ type App struct { turnClient *turn.Client sprinklerMonitor *sprinklerMonitor previousBlockedPRs map[string]bool - authError string - lastFetchError string + githubCircuit *circuitBreaker + healthMonitor *healthMonitor cacheDir string + lastFetchError string + authError string targetUser string lastMenuTitles []string outgoing []PR @@ -94,17 +96,15 @@ type App struct { updateInterval time.Duration consecutiveFailures int mu sync.RWMutex - menuMutex sync.Mutex // Mutex to prevent concurrent menu rebuilds - updateMutex sync.Mutex // Mutex to prevent concurrent PR updates - enableAutoBrowser bool + updateMutex sync.Mutex + menuMutex sync.Mutex hideStaleIncoming bool - hasPerformedInitialDiscovery bool // Track if we've done the first poll to distinguish from real state changes + hasPerformedInitialDiscovery bool noCache bool enableAudioCues bool initialLoadComplete bool menuInitialized bool - healthMonitor *healthMonitor - githubCircuit *circuitBreaker + enableAutoBrowser bool } func main() { @@ -165,7 +165,10 @@ func main() { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, opts))) slog.Info("Starting Goose", "version", version, "commit", commit, "date", date) slog.Info("Configuration", "update_interval", updateInterval, "max_retries", maxRetries, "max_delay", maxRetryDelay) - slog.Info("Browser auto-open configuration", "startup_delay", browserOpenDelay, "max_per_minute", maxBrowserOpensMinute, "max_per_day", maxBrowserOpensDay) + slog.Info("Browser auto-open configuration", + "startup_delay", browserOpenDelay, + "max_per_minute", maxBrowserOpensMinute, + "max_per_day", maxBrowserOpensDay) ctx := context.Background() @@ -262,12 +265,13 @@ func main() { }), retry.Context(ctx), ) - if err != nil { + switch { + case err != nil: slog.Warn("Failed to load current user after retries", "maxRetries", maxRetries, "error", err) if app.authError == "" { app.authError = fmt.Sprintf("Failed to load user: %v", err) } - } else if user != nil { + case user != nil: app.currentUser = user // Log if we're using a different target user (sanitized) if app.targetUser != "" && app.targetUser != user.GetLogin() { @@ -280,7 +284,7 @@ func main() { slog.Warn("[SPRINKLER] Failed to initialize organizations", "error", err) } }() - } else { + default: slog.Warn("GitHub API returned nil user") } } else { @@ -302,6 +306,70 @@ func main() { }) } +// handleReauthentication attempts to re-authenticate when auth errors occur. +func (app *App) handleReauthentication(ctx context.Context) { + // Try to reinitialize clients which will attempt to get token via gh auth token + if err := app.initClients(ctx); err != nil { + slog.Warn("[CLICK] Re-authentication failed", "error", err) + app.mu.Lock() + app.authError = err.Error() + app.mu.Unlock() + return + } + + // Success! Clear auth error and reload user + slog.Info("[CLICK] Re-authentication successful") + app.mu.Lock() + app.authError = "" + app.mu.Unlock() + + // Load current user + if app.client != nil { + var user *github.User + err := retry.Do(func() error { + var retryErr error + user, _, retryErr = app.client.Users.Get(ctx, "") + if retryErr != nil { + slog.Warn("GitHub Users.Get failed (will retry)", "error", retryErr) + 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.Debug("[RETRY] Retrying GitHub API call", "attempt", n, "error", err) + }), + ) + if err == nil && user != nil { + if app.targetUser == "" { + app.targetUser = user.GetLogin() + slog.Info("Set target user to current user", "user", app.targetUser) + } + } + } + + // Update tooltip + tooltip := "Goose - Loading PRs..." + if app.targetUser != "" { + tooltip = fmt.Sprintf("Goose - Loading PRs... (@%s)", app.targetUser) + } + systray.SetTooltip(tooltip) + + // Rebuild menu to remove error state + app.rebuildMenu(ctx) + + // Start update loop if not already running + if !app.menuInitialized { + app.menuInitialized = true + go app.updateLoop(ctx) + } else { + // Just do a single update to refresh data + go app.updatePRs(ctx) + } +} + func (app *App) onReady(ctx context.Context) { slog.Info("System tray ready") @@ -334,67 +402,7 @@ func (app *App) onReady(ctx context.Context) { if hasAuthError { slog.Info("[CLICK] Auth error detected, attempting to re-authenticate") - go func() { - // Try to reinitialize clients which will attempt to get token via gh auth token - if err := app.initClients(ctx); err != nil { - slog.Warn("[CLICK] Re-authentication failed", "error", err) - app.mu.Lock() - app.authError = err.Error() - app.mu.Unlock() - } else { - // Success! Clear auth error and reload user - slog.Info("[CLICK] Re-authentication successful") - app.mu.Lock() - app.authError = "" - app.mu.Unlock() - - // Load current user - if app.client != nil { - var user *github.User - err := retry.Do(func() error { - var retryErr error - user, _, retryErr = app.client.Users.Get(ctx, "") - if retryErr != nil { - slog.Warn("GitHub Users.Get failed (will retry)", "error", retryErr) - 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.Debug("[RETRY] Retrying GitHub API call", "attempt", n, "error", err) - }), - ) - if err == nil && user != nil { - if app.targetUser == "" { - app.targetUser = user.GetLogin() - slog.Info("Set target user to current user", "user", app.targetUser) - } - } - } - - // Update tooltip - tooltip := "Goose - Loading PRs..." - if app.targetUser != "" { - tooltip = fmt.Sprintf("Goose - Loading PRs... (@%s)", app.targetUser) - } - systray.SetTooltip(tooltip) - - // Rebuild menu to remove error state - app.rebuildMenu(ctx) - - // Start update loop if not already running - if !app.menuInitialized { - app.menuInitialized = true - go app.updateLoop(ctx) - } else { - // Just do a single update to refresh data - go app.updatePRs(ctx) - } - } - }() + go app.handleReauthentication(ctx) } else { // Normal operation - check if we can perform a forced refresh app.mu.RLock() @@ -637,15 +645,15 @@ func (app *App) updatePRs(ctx context.Context) { "outgoing_count", len(outgoing)) // Log ALL outgoing PRs for debugging slog.Debug("[UPDATE] Listing ALL outgoing PRs for debugging") - for i, pr := range outgoing { + for i := range outgoing { slog.Debug("[UPDATE] Outgoing PR details", "index", i, - "repo", pr.Repository, - "number", pr.Number, - "blocked", pr.IsBlocked, - "updated_at", pr.UpdatedAt.Format(time.RFC3339), - "title", pr.Title, - "url", pr.URL) + "repo", outgoing[i].Repository, + "number", outgoing[i].Number, + "blocked", outgoing[i].IsBlocked, + "updated_at", outgoing[i].UpdatedAt.Format(time.RFC3339), + "title", outgoing[i].Title, + "url", outgoing[i].URL) } // Mark initial load as complete after first successful update if !app.initialLoadComplete { @@ -831,7 +839,7 @@ 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) { +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, diff --git a/cmd/goose/main_test.go b/cmd/goose/main_test.go index 75a1cd1..e8b9c1c 100644 --- a/cmd/goose/main_test.go +++ b/cmd/goose/main_test.go @@ -347,7 +347,11 @@ func TestTrayTitleUpdates(t *testing.T) { // Call setTrayTitle to get the actual title app.setTrayTitle() - actualTitle := app.systrayInterface.(*MockSystray).title + mockSystray, ok := app.systrayInterface.(*MockSystray) + if !ok { + t.Fatal("Failed to cast systrayInterface to MockSystray") + } + actualTitle := mockSystray.title // Adjust expected title based on platform expectedTitle := tt.expectedTitle diff --git a/cmd/goose/multihandler.go b/cmd/goose/multihandler.go index f562aaf..a4f0be9 100644 --- a/cmd/goose/multihandler.go +++ b/cmd/goose/multihandler.go @@ -21,10 +21,15 @@ func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool { } // Handle writes the record to all handlers. +// +//nolint:gocritic // record is an interface parameter, cannot change to pointer func (h *MultiHandler) Handle(ctx context.Context, record slog.Record) error { for _, handler := range h.handlers { if handler.Enabled(ctx, record.Level) { - handler.Handle(ctx, record) //nolint:errcheck // Continue logging to other destinations + if err := handler.Handle(ctx, record); err != nil { + // Continue logging to other destinations even if one fails + _ = err + } } } return nil diff --git a/cmd/goose/notifications.go b/cmd/goose/notifications.go index 3ec72f9..3ab2404 100644 --- a/cmd/goose/notifications.go +++ b/cmd/goose/notifications.go @@ -1,4 +1,3 @@ -// Package main - notifications.go provides simplified notification handling. package main import ( @@ -75,18 +74,18 @@ func (app *App) processNotifications(ctx context.Context) { // Send notification if isIncoming { - app.sendPRNotification(ctx, pr, "PR Blocked on You 🪿", "honk", &playedHonk) + app.sendPRNotification(ctx, &pr, "PR Blocked on You 🪿", "honk", &playedHonk) } else { // Add delay between different sound types in goroutine to avoid blocking if playedHonk && !playedRocket { time.Sleep(2 * time.Second) } - app.sendPRNotification(ctx, pr, "Your PR is Blocked 🚀", "rocket", &playedRocket) + app.sendPRNotification(ctx, &pr, "Your PR is Blocked 🚀", "rocket", &playedRocket) } // Auto-open if enabled if app.enableAutoBrowser && time.Since(app.startTime) > startupGracePeriod { - app.tryAutoOpenPR(ctx, pr, app.enableAutoBrowser, app.startTime) + app.tryAutoOpenPR(ctx, &pr, app.enableAutoBrowser, app.startTime) } } }() @@ -101,7 +100,7 @@ func (app *App) processNotifications(ctx context.Context) { } // sendPRNotification sends a notification for a single PR. -func (app *App) sendPRNotification(ctx context.Context, pr PR, title string, soundType string, playedSound *bool) { +func (app *App) sendPRNotification(ctx context.Context, pr *PR, title string, soundType string, playedSound *bool) { message := fmt.Sprintf("%s #%d: %s", pr.Repository, pr.Number, pr.Title) // Send desktop notification in a goroutine to avoid blocking diff --git a/cmd/goose/pr_state.go b/cmd/goose/pr_state.go index ac955b7..0e16ef8 100644 --- a/cmd/goose/pr_state.go +++ b/cmd/goose/pr_state.go @@ -1,4 +1,3 @@ -// Package main - pr_state.go provides simplified PR state management. package main import ( diff --git a/cmd/goose/ratelimit.go b/cmd/goose/ratelimit.go index a6d9ba7..9056c3b 100644 --- a/cmd/goose/ratelimit.go +++ b/cmd/goose/ratelimit.go @@ -1,4 +1,3 @@ -// Package main - ratelimit.go provides rate limiting functionality. package main import ( diff --git a/cmd/goose/reliability.go b/cmd/goose/reliability.go index f522716..0e59650 100644 --- a/cmd/goose/reliability.go +++ b/cmd/goose/reliability.go @@ -1,4 +1,3 @@ -// Package main - reliability.go provides reliability improvements and error recovery. package main import ( @@ -42,13 +41,13 @@ func safeExecute(operation string, fn func() error) (err error) { // circuitBreaker provides circuit breaker pattern for external API calls. type circuitBreaker struct { - mu sync.RWMutex lastFailureTime time.Time - timeout time.Duration name string - state string // "closed", "open", "half-open" + state string + timeout time.Duration failures int threshold int + mu sync.RWMutex } func newCircuitBreaker(name string, threshold int, timeout time.Duration) *circuitBreaker { @@ -66,13 +65,12 @@ func (cb *circuitBreaker) call(fn func() error) error { // Check if circuit is open if cb.state == "open" { - if time.Since(cb.lastFailureTime) > cb.timeout { - cb.state = "half-open" - slog.Info("[CIRCUIT] Circuit breaker transitioning to half-open", - "name", cb.name) - } else { + if time.Since(cb.lastFailureTime) <= cb.timeout { return fmt.Errorf("circuit breaker open for %s", cb.name) } + cb.state = "half-open" + slog.Info("[CIRCUIT] Circuit breaker transitioning to half-open", + "name", cb.name) } // Execute the function @@ -107,14 +105,14 @@ func (cb *circuitBreaker) call(fn func() error) error { // healthMonitor tracks application health metrics. type healthMonitor struct { - mu sync.RWMutex lastCheckTime time.Time uptime time.Time + app *App apiCalls int64 apiErrors int64 cacheHits int64 cacheMisses int64 - app *App // Reference to app for accessing sprinkler status + mu sync.RWMutex } func newHealthMonitor() *healthMonitor { @@ -146,7 +144,7 @@ func (hm *healthMonitor) recordCacheAccess(hit bool) { } } -func (hm *healthMonitor) getMetrics() map[string]interface{} { +func (hm *healthMonitor) getMetrics() map[string]any { hm.mu.RLock() defer hm.mu.RUnlock() @@ -161,7 +159,7 @@ func (hm *healthMonitor) getMetrics() map[string]interface{} { cacheHitRate = float64(hm.cacheHits) / float64(totalCacheAccess) * 100 } - return map[string]interface{}{ + return map[string]any{ "uptime": time.Since(hm.uptime), "api_calls": hm.apiCalls, "api_errors": hm.apiErrors, diff --git a/cmd/goose/security.go b/cmd/goose/security.go index 6b9f836..7ec2bf3 100644 --- a/cmd/goose/security.go +++ b/cmd/goose/security.go @@ -1,4 +1,3 @@ -// Package main - security.go provides security utilities and validation functions. package main import ( @@ -152,14 +151,14 @@ func validateGitHubPRURL(rawURL string) error { // Exception: %3D which is = in URL encoding, only as part of ?goose parameter if strings.Contains(rawURL, "%") { // Allow URL encoding only in the goose parameter value - if idx := strings.Index(rawURL, "?goose="); idx != -1 { - // Check if encoding is only in the goose parameter - if strings.Contains(rawURL[:idx], "%") { - return errors.New("URL contains encoded characters outside goose parameter") - } - } else { + idx := strings.Index(rawURL, "?goose=") + if idx == -1 { return errors.New("URL contains encoded characters") } + // Check if encoding is only in the goose parameter + if strings.Contains(rawURL[:idx], "%") { + return errors.New("URL contains encoded characters outside goose parameter") + } } // Reject URLs with fragments diff --git a/cmd/goose/settings.go b/cmd/goose/settings.go index 85e7731..a3ddb45 100644 --- a/cmd/goose/settings.go +++ b/cmd/goose/settings.go @@ -1,4 +1,3 @@ -// Package main - settings.go provides persistent settings storage. package main import ( diff --git a/cmd/goose/sound.go b/cmd/goose/sound.go index b51e94f..069993f 100644 --- a/cmd/goose/sound.go +++ b/cmd/goose/sound.go @@ -1,4 +1,3 @@ -// Package main - sound.go handles platform-specific sound playback. package main import ( diff --git a/cmd/goose/sprinkler.go b/cmd/goose/sprinkler.go index bf2f067..b410549 100644 --- a/cmd/goose/sprinkler.go +++ b/cmd/goose/sprinkler.go @@ -1,4 +1,3 @@ -// Package main - sprinkler.go contains real-time event monitoring via WebSocket. package main import ( @@ -27,29 +26,25 @@ const ( // sprinklerMonitor manages WebSocket event subscriptions for all user orgs. type sprinklerMonitor struct { + lastConnectedAt time.Time 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 + eventChan chan string + lastEventMap map[string]time.Time 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 + isConnected bool } // newSprinklerMonitor creates a new sprinkler monitor for real-time PR events. func newSprinklerMonitor(app *App, token string) *sprinklerMonitor { - ctx, cancel := context.WithCancel(context.Background()) return &sprinklerMonitor{ app: app, token: token, orgs: make([]string, 0), - ctx: ctx, - cancel: cancel, eventChan: make(chan string, eventChannelSize), lastEventMap: make(map[string]time.Time), } @@ -71,7 +66,7 @@ func (sm *sprinklerMonitor) updateOrgs(orgs []string) { } // start begins monitoring for PR events across all user orgs. -func (sm *sprinklerMonitor) start() error { +func (sm *sprinklerMonitor) start(ctx context.Context) error { sm.mu.Lock() defer sm.mu.Unlock() @@ -89,9 +84,13 @@ func (sm *sprinklerMonitor) start() error { "orgs", sm.orgs, "org_count", len(sm.orgs)) + // Create context with cancel for shutdown + monitorCtx, cancel := context.WithCancel(ctx) + sm.cancel = cancel + // Create logger that discards output unless debug mode var sprinklerLogger *slog.Logger - if slog.Default().Enabled(sm.ctx, slog.LevelDebug) { + if slog.Default().Enabled(ctx, slog.LevelDebug) { sprinklerLogger = slog.Default() } else { // Use a handler that discards all logs @@ -140,7 +139,7 @@ func (sm *sprinklerMonitor) start() error { slog.Info("[SPRINKLER] Starting event processor goroutine") // Start event processor - go sm.processEvents() + go sm.processEvents(monitorCtx) slog.Info("[SPRINKLER] Starting WebSocket client goroutine") // Start WebSocket client with error recovery @@ -156,7 +155,7 @@ func (sm *sprinklerMonitor) start() error { }() startTime := time.Now() - if err := wsClient.Start(sm.ctx); err != nil && !errors.Is(err, context.Canceled) { + if err := wsClient.Start(monitorCtx); err != nil && !errors.Is(err, context.Canceled) { slog.Error("[SPRINKLER] WebSocket client error", "error", err, "uptime", time.Since(startTime).Round(time.Second)) @@ -258,7 +257,7 @@ func (sm *sprinklerMonitor) handleEvent(event client.Event) { } // processEvents handles PR events by checking if they're blocking and notifying. -func (sm *sprinklerMonitor) processEvents() { +func (sm *sprinklerMonitor) processEvents(ctx context.Context) { defer func() { if r := recover(); r != nil { slog.Error("[SPRINKLER] Event processor panic", "panic", r) @@ -267,16 +266,16 @@ func (sm *sprinklerMonitor) processEvents() { for { select { - case <-sm.ctx.Done(): + case <-ctx.Done(): return case prURL := <-sm.eventChan: - sm.checkAndNotify(prURL) + sm.checkAndNotify(ctx, prURL) } } } // checkAndNotify checks if a PR is blocking and sends notification if needed. -func (sm *sprinklerMonitor) checkAndNotify(prURL string) { +func (sm *sprinklerMonitor) checkAndNotify(ctx context.Context, prURL string) { startTime := time.Now() // Get current user @@ -305,7 +304,7 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { err := retry.Do(func() error { var retryErr error - turnData, wasFromCache, retryErr = sm.app.turnData(sm.ctx, prURL, time.Now()) + turnData, wasFromCache, retryErr = sm.app.turnData(ctx, prURL, time.Now()) if retryErr != nil { slog.Debug("[SPRINKLER] Turn API call failed (will retry)", "repo", repo, "number", number, "error", retryErr) @@ -323,7 +322,7 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { "number", number, "error", err) }), - retry.Context(sm.ctx), + retry.Context(ctx), ) if err != nil { // Log error but don't block - the next polling cycle will catch it @@ -354,46 +353,7 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { // Skip closed/merged PRs and remove from lists immediately if prState == "closed" || prIsMerged { - slog.Info("[SPRINKLER] PR closed/merged, removing from lists", - "repo", repo, - "number", number, - "state", prState, - "merged", prIsMerged, - "url", prURL) - - // Remove from in-memory lists immediately - sm.app.mu.Lock() - originalIncoming := len(sm.app.incoming) - originalOutgoing := len(sm.app.outgoing) - - // Filter out this PR from incoming - filteredIncoming := make([]PR, 0, len(sm.app.incoming)) - for _, pr := range sm.app.incoming { - if pr.URL != prURL { - filteredIncoming = append(filteredIncoming, pr) - } - } - sm.app.incoming = filteredIncoming - - // Filter out this PR from outgoing - filteredOutgoing := make([]PR, 0, len(sm.app.outgoing)) - for _, pr := range sm.app.outgoing { - if pr.URL != prURL { - filteredOutgoing = append(filteredOutgoing, pr) - } - } - sm.app.outgoing = filteredOutgoing - sm.app.mu.Unlock() - - slog.Info("[SPRINKLER] Removed PR from lists", - "url", prURL, - "incoming_before", originalIncoming, - "incoming_after", len(sm.app.incoming), - "outgoing_before", originalOutgoing, - "outgoing_after", len(sm.app.outgoing)) - - // Update UI to reflect removal - sm.app.updateMenu(sm.ctx) + sm.removeClosedPR(ctx, prURL, repo, number, prState, prIsMerged) return } @@ -451,7 +411,7 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { "repo", repo, "number", number, "action", action.Kind) - go sm.app.updatePRs(sm.ctx) + go sm.app.updatePRs(ctx) return // Let the refresh handle everything } @@ -514,7 +474,7 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { "repo", repo, "number", number, "soundType", "honk") - sm.app.playSound(sm.ctx, "honk") + sm.app.playSound(ctx, "honk") } // Try auto-open if enabled @@ -522,7 +482,7 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { slog.Debug("[SPRINKLER] Attempting auto-open", "repo", repo, "number", number) - sm.app.tryAutoOpenPR(sm.ctx, PR{ + sm.app.tryAutoOpenPR(ctx, &PR{ URL: prURL, Repository: repo, Number: number, @@ -532,6 +492,50 @@ func (sm *sprinklerMonitor) checkAndNotify(prURL string) { } } +// removeClosedPR removes a closed or merged PR from the in-memory lists. +func (sm *sprinklerMonitor) removeClosedPR(ctx context.Context, prURL, repo string, number int, prState string, prIsMerged bool) { + slog.Info("[SPRINKLER] PR closed/merged, removing from lists", + "repo", repo, + "number", number, + "state", prState, + "merged", prIsMerged, + "url", prURL) + + // Remove from in-memory lists immediately + sm.app.mu.Lock() + originalIncoming := len(sm.app.incoming) + originalOutgoing := len(sm.app.outgoing) + + // Filter out this PR from incoming + filteredIncoming := make([]PR, 0, len(sm.app.incoming)) + for i := range sm.app.incoming { + if sm.app.incoming[i].URL != prURL { + filteredIncoming = append(filteredIncoming, sm.app.incoming[i]) + } + } + sm.app.incoming = filteredIncoming + + // Filter out this PR from outgoing + filteredOutgoing := make([]PR, 0, len(sm.app.outgoing)) + for i := range sm.app.outgoing { + if sm.app.outgoing[i].URL != prURL { + filteredOutgoing = append(filteredOutgoing, sm.app.outgoing[i]) + } + } + sm.app.outgoing = filteredOutgoing + sm.app.mu.Unlock() + + slog.Info("[SPRINKLER] Removed PR from lists", + "url", prURL, + "incoming_before", originalIncoming, + "incoming_after", len(sm.app.incoming), + "outgoing_before", originalOutgoing, + "outgoing_after", len(sm.app.outgoing)) + + // Update UI to reflect removal + sm.app.updateMenu(ctx) +} + // stop stops the sprinkler monitor. func (sm *sprinklerMonitor) stop() { sm.mu.Lock() diff --git a/cmd/goose/systray_interface.go b/cmd/goose/systray_interface.go index edc1223..889b64f 100644 --- a/cmd/goose/systray_interface.go +++ b/cmd/goose/systray_interface.go @@ -55,9 +55,9 @@ func (*RealSystray) Quit() { // MockSystray implements SystrayInterface for testing. type MockSystray struct { - mu sync.Mutex title string menuItems []string + mu sync.Mutex } func (m *MockSystray) ResetMenu() { @@ -89,7 +89,7 @@ func (m *MockSystray) SetTitle(title string) { m.title = title } -func (*MockSystray) SetIcon(iconBytes []byte) { +func (*MockSystray) SetIcon(_ []byte) { // No-op for testing } diff --git a/cmd/goose/ui.go b/cmd/goose/ui.go index 8a1816e..a93e1c9 100644 --- a/cmd/goose/ui.go +++ b/cmd/goose/ui.go @@ -1,4 +1,3 @@ -// Package main - ui.go handles system tray UI and menu management. package main import ( @@ -10,6 +9,7 @@ import ( "os/exec" "runtime" "sort" + "strconv" "strings" "time" @@ -65,12 +65,8 @@ func openURL(ctx context.Context, rawURL string) error { // Use rundll32 to open URL safely without cmd shell slog.Debug("Executing command", "command", "rundll32.exe url.dll,FileProtocolHandler", "url", rawURL) cmd = exec.CommandContext(ctx, "rundll32.exe", "url.dll,FileProtocolHandler", rawURL) - case "linux": - // Use xdg-open with full path - slog.Debug("Executing command", "command", "/usr/bin/xdg-open", "url", rawURL) - cmd = exec.CommandContext(ctx, "/usr/bin/xdg-open", rawURL) default: - // Try xdg-open for other Unix-like systems + // Use xdg-open with full path for Linux and other Unix-like systems slog.Debug("Executing command", "command", "/usr/bin/xdg-open", "url", rawURL) cmd = exec.CommandContext(ctx, "/usr/bin/xdg-open", rawURL) } @@ -221,10 +217,10 @@ func (app *App) setTrayTitle() { title = fmt.Sprintf("%d / %d", counts.IncomingBlocked, counts.OutgoingBlocked) iconType = IconBoth case counts.IncomingBlocked > 0: - title = fmt.Sprintf("%d", counts.IncomingBlocked) + title = strconv.Itoa(counts.IncomingBlocked) iconType = IconGoose default: - title = fmt.Sprintf("%d", counts.OutgoingBlocked) + title = strconv.Itoa(counts.OutgoingBlocked) if allOutgoingAreFixTests { iconType = IconCockroach } else { @@ -346,8 +342,11 @@ func (app *App) addPRSection(ctx context.Context, prs []PR, sectionTitle string, // Get the blocked time from state manager prState, hasState := app.stateManager.PRState(sortedPRs[prIndex].URL) - // Show emoji for PRs blocked within the last 5 minutes (but only for real state transitions, not initial discoveries) - if hasState && !prState.FirstBlockedAt.IsZero() && time.Since(prState.FirstBlockedAt) < blockedPRIconDuration && !prState.IsInitialDiscovery { + // Show emoji for PRs blocked within the last 5 minutes + // (but only for real state transitions, not initial discoveries) + if hasState && !prState.FirstBlockedAt.IsZero() && + time.Since(prState.FirstBlockedAt) < blockedPRIconDuration && + !prState.IsInitialDiscovery { timeSinceBlocked := time.Since(prState.FirstBlockedAt) // Use party popper for outgoing PRs, goose for incoming PRs if sectionTitle == "Outgoing" { @@ -533,7 +532,9 @@ func (app *App) generatePRSectionTitles(prs []PR, sectionTitle string, hiddenOrg if sortedPRs[prIndex].NeedsReview || sortedPRs[prIndex].IsBlocked { prState, hasState := app.stateManager.PRState(sortedPRs[prIndex].URL) - if hasState && !prState.FirstBlockedAt.IsZero() && time.Since(prState.FirstBlockedAt) < blockedPRIconDuration && !prState.IsInitialDiscovery { + if hasState && !prState.FirstBlockedAt.IsZero() && + time.Since(prState.FirstBlockedAt) < blockedPRIconDuration && + !prState.IsInitialDiscovery { timeSinceBlocked := time.Since(prState.FirstBlockedAt) if sectionTitle == "Outgoing" { title = fmt.Sprintf("🎉 %s", title) diff --git a/go.mod b/go.mod index 2d11a27..4857b5d 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,17 @@ go 1.24.0 require ( github.com/codeGROOVE-dev/retry v1.2.0 - github.com/codeGROOVE-dev/sprinkler v0.0.0-20251007014926-7fed60b70e75 - github.com/codeGROOVE-dev/turnclient v0.0.0-20251001194229-2aaea2e63cc7 + github.com/codeGROOVE-dev/sprinkler v0.0.0-20251018202454-6c749ec76260 + github.com/codeGROOVE-dev/turnclient v0.0.0-20251018202306-7cdc0d51856e github.com/energye/systray v1.0.2 github.com/gen2brain/beeep v0.11.1 github.com/google/go-github/v57 v57.0.0 - golang.org/x/oauth2 v0.31.0 + golang.org/x/oauth2 v0.32.0 ) require ( git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect - github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c // indirect + github.com/codeGROOVE-dev/prx v0.0.0-20251016165946-00c6c6e90c29 // 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 @@ -25,6 +25,6 @@ require ( github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index da479ee..8ba3003 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,20 @@ git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo= 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/prx v0.0.0-20251016165946-00c6c6e90c29 h1:MSBy3Ywr3ky/LXhDSFbeJXDdAsfMMrzNdMNehyTvSuA= +github.com/codeGROOVE-dev/prx v0.0.0-20251016165946-00c6c6e90c29/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-20251006230512-cc6accef7f7a h1:RNG9GGKOgAQx+GldnrF0YonTORNllzIdYkvOeZfnWWE= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251006230512-cc6accef7f7a/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251007014926-7fed60b70e75 h1:I+gBQZoB2QCiV++lNOxUNMiNvyfeyhN7oy89U/hTjtw= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251007014926-7fed60b70e75/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251018202454-6c749ec76260 h1:Br9fc+ebZllSA9IY4s2mq4yZoSkV44577EakZbvmJYk= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251018202454-6c749ec76260/go.mod h1:/kd3ncsRNldD0MUpbtp5ojIzfCkyeXB7JdOrpuqG7Gg= 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/codeGROOVE-dev/turnclient v0.0.0-20251018202306-7cdc0d51856e h1:3qoY6h8SgoeNsIYRM7P6PegTXAHPo8OSOapUunVP/Gs= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251018202306-7cdc0d51856e/go.mod h1:fYwtN9Ql6lY8t2WvCfENx+mP5FUwjlqwXCLx9CVLY20= 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= @@ -54,12 +60,18 @@ github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c h1:coVla7zpsycc+kA9NX github.com/tevino/abool v0.0.0-20220530134649-2bfc934cb23c/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3dacdece9ae4a11ad50df6f4fc1c1b73ebcc71b3 Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Sun, 19 Oct 2025 13:04:28 +0200 Subject: [PATCH 2/2] update sprinkler libs --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 4857b5d..5804cbf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/codeGROOVE-dev/retry v1.2.0 - github.com/codeGROOVE-dev/sprinkler v0.0.0-20251018202454-6c749ec76260 + github.com/codeGROOVE-dev/sprinkler v0.0.0-20251019110134-896b678fd945 github.com/codeGROOVE-dev/turnclient v0.0.0-20251018202306-7cdc0d51856e github.com/energye/systray v1.0.2 github.com/gen2brain/beeep v0.11.1 diff --git a/go.sum b/go.sum index 8ba3003..071383f 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/codeGROOVE-dev/sprinkler v0.0.0-20251007014926-7fed60b70e75 h1:I+gBQZ github.com/codeGROOVE-dev/sprinkler v0.0.0-20251007014926-7fed60b70e75/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251018202454-6c749ec76260 h1:Br9fc+ebZllSA9IY4s2mq4yZoSkV44577EakZbvmJYk= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251018202454-6c749ec76260/go.mod h1:/kd3ncsRNldD0MUpbtp5ojIzfCkyeXB7JdOrpuqG7Gg= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251019110134-896b678fd945 h1:uZhbGjrIEYfs6Bq2PQgbbtag5gAjMp/NQZGAQsL73m4= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251019110134-896b678fd945/go.mod h1:/kd3ncsRNldD0MUpbtp5ojIzfCkyeXB7JdOrpuqG7Gg= 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/codeGROOVE-dev/turnclient v0.0.0-20251018202306-7cdc0d51856e h1:3qoY6h8SgoeNsIYRM7P6PegTXAHPo8OSOapUunVP/Gs=