Skip to content

Commit c8d95ec

Browse files
authored
Merge pull request #15 from ready-to-review/token-env
Add support for GITHUB_TOKEN environment variable
2 parents 782fd40 + d0cd492 commit c8d95ec

File tree

4 files changed

+63
-17
lines changed

4 files changed

+63
-17
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ You can also visit the web-based equivalent at https://dash.ready-to-review.dev/
2424

2525
## macOS Quick Start ⚡ (How to Get Honked At)
2626

27+
### Option 1: Using GitHub CLI (Default)
28+
2729
Install dependencies: the [GitHub CLI, aka "gh"](https://cli.github.com/) and [Go](https://go.dev/):
2830

2931
```bash
@@ -38,6 +40,21 @@ git clone https://github.com/ready-to-review/goose.git
3840
cd goose && make run
3941
```
4042

43+
### Option 2: Using a GitHub Token (More Control)
44+
45+
If you want more control over which repositories the goose can access, you can use a GitHub personal access token instead:
46+
47+
1. Create a [GitHub personal access token](https://github.com/settings/tokens) with `repo` scope
48+
2. Set the `GITHUB_TOKEN` environment variable:
49+
50+
```bash
51+
export GITHUB_TOKEN=your_token_here
52+
git clone https://github.com/ready-to-review/goose.git
53+
cd goose && make run
54+
```
55+
56+
When `GITHUB_TOKEN` is set, the goose will use it directly instead of the GitHub CLI, giving you precise control over repository access.
57+
4158
## Known Issues
4259

4360
- Blocking logic isn't 100% accurate (we're working on it)

cache.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ type cacheEntry struct {
2525

2626
// turnData fetches Turn API data with caching.
2727
func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (*turn.CheckResponse, bool, error) {
28+
// Validate URL before processing
29+
if err := validateURL(url); err != nil {
30+
return nil, false, fmt.Errorf("invalid URL: %w", err)
31+
}
32+
2833
// Create cache key from URL and updated timestamp
2934
key := fmt.Sprintf("%s-%s", url, updatedAt.Format(time.RFC3339))
3035
hash := sha256.Sum256([]byte(key))
@@ -93,11 +98,11 @@ func (app *App) turnData(ctx context.Context, url string, updatedAt time.Time) (
9398
if cacheData, marshalErr := json.Marshal(entry); marshalErr != nil {
9499
log.Printf("Failed to marshal cache data for %s: %v", url, marshalErr)
95100
} else {
96-
// Ensure cache directory exists
101+
// Ensure cache directory exists with secure permissions
97102
if dirErr := os.MkdirAll(filepath.Dir(cacheFile), 0o700); dirErr != nil {
98103
log.Printf("Failed to create cache directory: %v", dirErr)
99104
} else if writeErr := os.WriteFile(cacheFile, cacheData, 0o600); writeErr != nil {
100-
log.Printf("Failed to write cache file for %s: %v", url, writeErr)
105+
log.Printf("Failed to write cache file: %v", writeErr)
101106
}
102107
}
103108
}

github.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,19 @@ func (app *App) initClients(ctx context.Context) error {
4444
return nil
4545
}
4646

47-
// githubToken retrieves the GitHub token using gh CLI.
47+
// githubToken retrieves the GitHub token from GITHUB_TOKEN env var or gh CLI.
4848
func (*App) githubToken(ctx context.Context) (string, error) {
49+
// First check for GITHUB_TOKEN environment variable
50+
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
51+
token = strings.TrimSpace(token)
52+
if err := validateGitHubToken(token); err != nil {
53+
return "", fmt.Errorf("invalid GITHUB_TOKEN: %w", err)
54+
}
55+
log.Println("Using GitHub token from GITHUB_TOKEN environment variable")
56+
return token, nil
57+
}
58+
59+
// Fall back to gh CLI if GITHUB_TOKEN is not set
4960
// Only check absolute paths for security - never use PATH
5061
var trustedPaths []string
5162
switch runtime.GOOS {
@@ -85,8 +96,11 @@ func (*App) githubToken(ctx context.Context) (string, error) {
8596
const executableMask = 0o111
8697
if info.Mode().IsRegular() && info.Mode()&executableMask != 0 {
8798
// Verify it's actually the gh binary by running version command
88-
cmd := exec.Command(path, "version") //nolint:noctx // Quick version check doesn't need context
99+
// Use timeout to prevent hanging
100+
versionCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
101+
cmd := exec.CommandContext(versionCtx, path, "version")
89102
output, err := cmd.Output()
103+
cancel() // Call cancel immediately after command execution
90104
if err == nil && strings.Contains(string(output), "gh version") {
91105
log.Printf("Found and verified gh at: %s", path)
92106
ghPath = path
@@ -97,25 +111,21 @@ func (*App) githubToken(ctx context.Context) (string, error) {
97111
}
98112

99113
if ghPath == "" {
100-
return "", errors.New("gh cli not found in trusted locations")
114+
return "", errors.New("gh cli not found in trusted locations and GITHUB_TOKEN not set")
101115
}
102116

103117
log.Printf("Executing command: %s auth token", ghPath)
104118
cmd := exec.CommandContext(ctx, ghPath, "auth", "token")
105119
output, err := cmd.CombinedOutput()
106120
if err != nil {
107-
log.Printf("gh command failed with output: %s", string(output))
108-
return "", fmt.Errorf("exec 'gh auth token': %w (output: %s)", err, string(output))
121+
log.Printf("gh command failed: %v", err)
122+
return "", fmt.Errorf("exec 'gh auth token': %w", err)
109123
}
110124
token := strings.TrimSpace(string(output))
111-
if token == "" {
112-
return "", errors.New("empty github token")
113-
}
114-
const minTokenLength = 20
115-
if len(token) < minTokenLength {
116-
return "", fmt.Errorf("invalid github token length: %d", len(token))
125+
if err := validateGitHubToken(token); err != nil {
126+
return "", fmt.Errorf("invalid token from gh CLI: %w", err)
117127
}
118-
log.Println("Successfully obtained GitHub token")
128+
log.Println("Successfully obtained GitHub token from gh CLI")
119129
return token, nil
120130
}
121131

@@ -200,9 +210,9 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
200210

201211
// Run both queries in parallel
202212
type queryResult struct {
213+
err error
203214
query string
204215
issues []*github.Issue
205-
err error
206216
}
207217

208218
queryResults := make(chan queryResult, 2)
@@ -236,11 +246,13 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
236246
// Collect results from both queries
237247
var allIssues []*github.Issue
238248
seenURLs := make(map[string]bool)
249+
var queryErrors []error
239250

240251
for range 2 {
241252
result := <-queryResults
242253
if result.err != nil {
243254
log.Printf("[GITHUB] Query failed: %s - %v", result.query, result.err)
255+
queryErrors = append(queryErrors, result.err)
244256
// Continue processing other query results even if one fails
245257
continue
246258
}
@@ -257,6 +269,11 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
257269
}
258270
log.Printf("[GITHUB] Both searches completed in %v, found %d unique PRs", time.Since(searchStart), len(allIssues))
259271

272+
// If both queries failed, return an error
273+
if len(queryErrors) == 2 {
274+
return nil, nil, fmt.Errorf("all GitHub queries failed: %v", queryErrors)
275+
}
276+
260277
// Limit PRs for performance
261278
if len(allIssues) > maxPRsToProcess {
262279
log.Printf("Limiting to %d PRs for performance (total: %d)", maxPRsToProcess, len(allIssues))

main.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ func main() {
130130
flag.DurationVar(&updateInterval, "interval", defaultUpdateInterval, "Update interval (e.g. 30s, 1m, 5m)")
131131
flag.Parse()
132132

133+
// Validate target user if provided
134+
if targetUser != "" {
135+
if err := validateGitHubUsername(targetUser); err != nil {
136+
log.Fatalf("Invalid target user: %v", err)
137+
}
138+
}
139+
133140
// Validate update interval
134141
if updateInterval < minUpdateInterval {
135142
log.Printf("Update interval %v too short, using minimum of %v", updateInterval, minUpdateInterval)
@@ -197,9 +204,9 @@ func main() {
197204
}
198205
app.currentUser = user
199206

200-
// Log if we're using a different target user
207+
// Log if we're using a different target user (sanitized)
201208
if app.targetUser != "" && app.targetUser != user.GetLogin() {
202-
log.Printf("Querying PRs for user '%s' instead of authenticated user '%s'", app.targetUser, user.GetLogin())
209+
log.Printf("Querying PRs for user '%s' instead of authenticated user", sanitizeForLog(app.targetUser))
203210
}
204211

205212
log.Println("Starting systray...")

0 commit comments

Comments
 (0)