Skip to content

Commit a962aff

Browse files
authored
Merge pull request #17 from ready-to-review/token-env
Fix lint & missing files
2 parents e7182fe + 4427ee2 commit a962aff

File tree

8 files changed

+265
-89
lines changed

8 files changed

+265
-89
lines changed

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,29 @@ cd goose && make run
4444

4545
If you want more control over which repositories the goose can access, you can use a GitHub personal access token instead:
4646

47-
1. Create a [GitHub personal access token](https://github.com/settings/tokens) with access to read pull-requests and repo metadata.
48-
2. Set the `GITHUB_TOKEN` environment variable:
47+
For maximum security, use a [fine-grained personal access token](https://github.com/settings/personal-access-tokens/new):
48+
49+
1. Go to GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens
50+
2. Create a new token with:
51+
- **Expiration**: Set a short expiration (30-90 days recommended)
52+
- **Repository access**: Select only the specific repositories you want to monitor
53+
- **Permissions**:
54+
- Pull requests: Read
55+
- Metadata: Read
56+
3. Copy the token (starts with `github_pat_`)
57+
58+
If you need broader access, you can use a [classic token](https://github.com/settings/tokens):
59+
- Create with `repo` scope (grants full repository access - use with caution)
60+
61+
#### Using the Token
4962

5063
```bash
5164
export GITHUB_TOKEN=your_token_here
5265
git clone https://github.com/ready-to-review/goose.git
5366
cd goose && make run
5467
```
5568

56-
When `GITHUB_TOKEN` is set, the goose will use it directly instead of the GitHub CLI, giving you precise control over repository access.
69+
When `GITHUB_TOKEN` is set, the goose will use it directly instead of the GitHub CLI, giving you precise control over repository access. Fine-grained tokens are strongly recommended for better security.
5770

5871
## Known Issues
5972

cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
6464
var data *turn.CheckResponse
6565
err := retry.Do(func() error {
6666
// Create timeout context for Turn API call
67-
turnCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
67+
turnCtx, cancel := context.WithTimeout(ctx, turnAPITimeout)
6868
defer cancel()
6969

7070
var retryErr error

github.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222

2323
// initClients initializes GitHub and Turn API clients.
2424
func (app *App) initClients(ctx context.Context) error {
25-
token, err := app.githubToken(ctx)
25+
token, err := app.token(ctx)
2626
if err != nil {
2727
return fmt.Errorf("get github token: %w", err)
2828
}
@@ -44,8 +44,8 @@ func (app *App) initClients(ctx context.Context) error {
4444
return nil
4545
}
4646

47-
// githubToken retrieves the GitHub token from GITHUB_TOKEN env var or gh CLI.
48-
func (*App) githubToken(ctx context.Context) (string, error) {
47+
// token retrieves the GitHub token from GITHUB_TOKEN env var or gh CLI.
48+
func (*App) token(ctx context.Context) (string, error) {
4949
// First check for GITHUB_TOKEN environment variable
5050
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
5151
token = strings.TrimSpace(token)
@@ -390,7 +390,10 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
390390
// Use a WaitGroup to track goroutines
391391
var wg sync.WaitGroup
392392

393-
// Process PRs in parallel
393+
// Create semaphore to limit concurrent Turn API calls
394+
sem := make(chan struct{}, maxConcurrentTurnAPICalls)
395+
396+
// Process PRs in parallel with concurrency limit
394397
for _, issue := range issues {
395398
if !issue.IsPullRequest() {
396399
continue
@@ -400,6 +403,10 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
400403
go func(issue *github.Issue) {
401404
defer wg.Done()
402405

406+
// Acquire semaphore
407+
sem <- struct{}{}
408+
defer func() { <-sem }()
409+
403410
url := issue.GetHTMLURL()
404411
updatedAt := issue.GetUpdatedAt().Time
405412

@@ -490,7 +497,10 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
490497
// Use a WaitGroup to track goroutines
491498
var wg sync.WaitGroup
492499

493-
// Process PRs in parallel
500+
// Create semaphore to limit concurrent Turn API calls
501+
sem := make(chan struct{}, maxConcurrentTurnAPICalls)
502+
503+
// Process PRs in parallel with concurrency limit
494504
for _, issue := range issues {
495505
if !issue.IsPullRequest() {
496506
continue
@@ -500,6 +510,10 @@ func (app *App) fetchTurnDataAsync(ctx context.Context, issues []*github.Issue,
500510
go func(issue *github.Issue) {
501511
defer wg.Done()
502512

513+
// Acquire semaphore
514+
sem <- struct{}{}
515+
defer func() { <-sem }()
516+
503517
url := issue.GetHTMLURL()
504518
updatedAt := issue.GetUpdatedAt().Time
505519

main.go

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,19 @@ const (
4747
// Retry settings for external API calls - exponential backoff with jitter up to 2 minutes.
4848
maxRetryDelay = 2 * time.Minute
4949
maxRetries = 10 // Should reach 2 minutes with exponential backoff
50+
51+
// Failure thresholds.
52+
minorFailureThreshold = 3
53+
majorFailureThreshold = 10
54+
panicFailureIncrement = 10
55+
56+
// Notification settings.
57+
reminderInterval = 24 * time.Hour
58+
historyRetentionDays = 30
59+
60+
// Turn API settings.
61+
turnAPITimeout = 10 * time.Second
62+
maxConcurrentTurnAPICalls = 10
5063
)
5164

5265
// PR represents a pull request with metadata.
@@ -270,7 +283,7 @@ func (app *App) updateLoop(ctx context.Context) {
270283

271284
// Update failure count
272285
app.mu.Lock()
273-
app.consecutiveFailures += 10 // Treat panic as critical failure
286+
app.consecutiveFailures += panicFailureIncrement // Treat panic as critical failure
274287
app.mu.Unlock()
275288

276289
// Signal app to quit after panic
@@ -308,10 +321,6 @@ func (app *App) updatePRs(ctx context.Context) {
308321
app.mu.Unlock()
309322

310323
// Progressive degradation based on failure count
311-
const (
312-
minorFailureThreshold = 3
313-
majorFailureThreshold = 10
314-
)
315324
var title, tooltip string
316325
switch {
317326
case failureCount == 1:
@@ -475,10 +484,6 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
475484
app.mu.Unlock()
476485

477486
// Progressive degradation based on failure count
478-
const (
479-
minorFailureThreshold = 3
480-
majorFailureThreshold = 10
481-
)
482487
var title, tooltip string
483488
switch {
484489
case failureCount == 1:
@@ -541,35 +546,6 @@ func (app *App) updatePRsWithWait(ctx context.Context) {
541546
app.checkForNewlyBlockedPRs(ctx)
542547
}
543548

544-
// shouldNotifyForPR determines if we should send a notification for a PR.
545-
func shouldNotifyForPR(
546-
_ string,
547-
isBlocked bool,
548-
prevState NotificationState,
549-
hasHistory bool,
550-
reminderInterval time.Duration,
551-
enableReminders bool,
552-
) (shouldNotify bool, reason string) {
553-
if !hasHistory && isBlocked {
554-
return true, "newly blocked"
555-
}
556-
557-
if !hasHistory {
558-
return false, ""
559-
}
560-
561-
switch {
562-
case isBlocked && !prevState.WasBlocked:
563-
return true, "became blocked"
564-
case !isBlocked && prevState.WasBlocked:
565-
return false, "unblocked"
566-
case isBlocked && prevState.WasBlocked && enableReminders && time.Since(prevState.LastNotified) > reminderInterval:
567-
return true, "reminder"
568-
default:
569-
return false, ""
570-
}
571-
}
572-
573549
// processPRNotifications handles notification logic for a single PR.
574550
func (app *App) processPRNotifications(
575551
ctx context.Context,
@@ -582,7 +558,24 @@ func (app *App) processPRNotifications(
582558
reminderInterval time.Duration,
583559
) {
584560
prevState, hasHistory := notificationHistory[pr.URL]
585-
shouldNotify, notifyReason := shouldNotifyForPR(pr.URL, isBlocked, prevState, hasHistory, reminderInterval, app.enableReminders)
561+
562+
// Determine if we should notify (inlined from shouldNotifyForPR)
563+
var shouldNotify bool
564+
var notifyReason string
565+
switch {
566+
case !hasHistory && isBlocked:
567+
shouldNotify, notifyReason = true, "newly blocked"
568+
case !hasHistory:
569+
shouldNotify, notifyReason = false, ""
570+
case isBlocked && !prevState.WasBlocked:
571+
shouldNotify, notifyReason = true, "became blocked"
572+
case !isBlocked && prevState.WasBlocked:
573+
shouldNotify, notifyReason = false, "unblocked"
574+
case isBlocked && prevState.WasBlocked && app.enableReminders && time.Since(prevState.LastNotified) > reminderInterval:
575+
shouldNotify, notifyReason = true, "reminder"
576+
default:
577+
shouldNotify, notifyReason = false, ""
578+
}
586579

587580
// Update state for unblocked PRs
588581
if notifyReason == "unblocked" {
@@ -669,8 +662,7 @@ func (app *App) checkForNewlyBlockedPRs(ctx context.Context) {
669662
now := time.Now()
670663
staleThreshold := now.Add(-stalePRThreshold)
671664

672-
// Reminder interval for re-notifications (24 hours)
673-
const reminderInterval = 24 * time.Hour
665+
// Use reminder interval constant from package level
674666

675667
currentBlockedPRs := make(map[string]bool)
676668
playedIncomingSound := false

ratelimit.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Package main - ratelimit.go provides rate limiting functionality.
2+
package main
3+
4+
import (
5+
"sync"
6+
"time"
7+
)
8+
9+
// RateLimiter implements a simple token bucket rate limiter.
10+
type RateLimiter struct {
11+
lastRefill time.Time
12+
mu sync.Mutex
13+
refillRate time.Duration
14+
tokens int
15+
maxTokens int
16+
}
17+
18+
// NewRateLimiter creates a new rate limiter.
19+
func NewRateLimiter(maxTokens int, refillRate time.Duration) *RateLimiter {
20+
return &RateLimiter{
21+
tokens: maxTokens,
22+
maxTokens: maxTokens,
23+
refillRate: refillRate,
24+
lastRefill: time.Now(),
25+
}
26+
}
27+
28+
// Allow checks if an operation is allowed under the rate limit.
29+
func (r *RateLimiter) Allow() bool {
30+
r.mu.Lock()
31+
defer r.mu.Unlock()
32+
33+
// Refill tokens based on elapsed time
34+
now := time.Now()
35+
elapsed := now.Sub(r.lastRefill)
36+
tokensToAdd := int(elapsed / r.refillRate)
37+
38+
if tokensToAdd > 0 {
39+
r.tokens = minInt(r.tokens+tokensToAdd, r.maxTokens)
40+
r.lastRefill = now
41+
}
42+
43+
// Check if we have tokens available
44+
if r.tokens > 0 {
45+
r.tokens--
46+
return true
47+
}
48+
49+
return false
50+
}
51+
52+
// minInt returns the minimum of two integers.
53+
func minInt(a, b int) int {
54+
if a < b {
55+
return a
56+
}
57+
return b
58+
}

0 commit comments

Comments
 (0)