99 "os"
1010 "os/exec"
1111 "path/filepath"
12+ "regexp"
1213 "runtime"
1314 "strings"
1415 "sync"
@@ -20,9 +21,23 @@ import (
2021 "golang.org/x/oauth2"
2122)
2223
24+ // githubTokenRegex matches valid GitHub token formats.
25+ var githubTokenRegex = regexp .MustCompile (`^(ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})$` )
26+
27+ // validateGitHubToken validates a GitHub token format.
28+ func validateGitHubToken (token string ) error {
29+ if token == "" {
30+ return errors .New ("empty token" )
31+ }
32+ if ! githubTokenRegex .MatchString (token ) {
33+ return errors .New ("invalid GitHub token format" )
34+ }
35+ return nil
36+ }
37+
2338// initClients initializes GitHub and Turn API clients.
2439func (app * App ) initClients (ctx context.Context ) error {
25- token , err := app .githubToken (ctx )
40+ token , err := app .token (ctx )
2641 if err != nil {
2742 return fmt .Errorf ("get github token: %w" , err )
2843 }
@@ -44,8 +59,18 @@ func (app *App) initClients(ctx context.Context) error {
4459 return nil
4560}
4661
47- // githubToken retrieves the GitHub token using gh CLI.
48- func (* App ) githubToken (ctx context.Context ) (string , error ) {
62+ // token retrieves the GitHub token from GITHUB_TOKEN env var or gh CLI.
63+ func (* App ) token (ctx context.Context ) (string , error ) {
64+ // Check GITHUB_TOKEN environment variable first
65+ if token := os .Getenv ("GITHUB_TOKEN" ); token != "" {
66+ token = strings .TrimSpace (token )
67+ if err := validateGitHubToken (token ); err != nil {
68+ return "" , fmt .Errorf ("invalid GITHUB_TOKEN: %w" , err )
69+ }
70+ log .Println ("Using GitHub token from GITHUB_TOKEN environment variable" )
71+ return token , nil
72+ }
73+
4974 // Only check absolute paths for security - never use PATH
5075 var trustedPaths []string
5176 switch runtime .GOOS {
@@ -179,10 +204,19 @@ func (app *App) executeGitHubQuery(ctx context.Context, query string, opts *gith
179204 return result , nil
180205}
181206
207+ // prResult holds the result of a Turn API query for a PR.
208+ type prResult struct {
209+ err error
210+ turnData * turn.CheckResponse
211+ url string
212+ isOwner bool
213+ wasFromCache bool
214+ }
215+
182216// fetchPRsInternal is the implementation for PR fetching.
183217// It returns GitHub data immediately and starts Turn API queries in the background (when waitForTurn=false),
184218// or waits for Turn data to complete (when waitForTurn=true).
185- func (app * App ) fetchPRsInternal (ctx context.Context , waitForTurn bool ) (incoming []PR , outgoing []PR , err error ) {
219+ func (app * App ) fetchPRsInternal (ctx context.Context , waitForTurn bool ) (incoming []PR , outgoing []PR , _ error ) {
186220 // Use targetUser if specified, otherwise use authenticated user
187221 user := app .currentUser .GetLogin ()
188222 if app .targetUser != "" {
@@ -200,9 +234,9 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
200234
201235 // Run both queries in parallel
202236 type queryResult struct {
237+ err error
203238 query string
204239 issues []* github.Issue
205- err error
206240 }
207241
208242 queryResults := make (chan queryResult , 2 )
@@ -236,11 +270,13 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
236270 // Collect results from both queries
237271 var allIssues []* github.Issue
238272 seenURLs := make (map [string ]bool )
273+ var queryErrors []error
239274
240275 for range 2 {
241276 result := <- queryResults
242277 if result .err != nil {
243278 log .Printf ("[GITHUB] Query failed: %s - %v" , result .query , result .err )
279+ queryErrors = append (queryErrors , result .err )
244280 // Continue processing other query results even if one fails
245281 continue
246282 }
@@ -257,6 +293,11 @@ func (app *App) fetchPRsInternal(ctx context.Context, waitForTurn bool) (incomin
257293 }
258294 log .Printf ("[GITHUB] Both searches completed in %v, found %d unique PRs" , time .Since (searchStart ), len (allIssues ))
259295
296+ // If both queries failed, return an error
297+ if len (queryErrors ) == 2 {
298+ return nil , nil , fmt .Errorf ("all GitHub queries failed: %v" , queryErrors )
299+ }
300+
260301 // Limit PRs for performance
261302 if len (allIssues ) > maxPRsToProcess {
262303 log .Printf ("Limiting to %d PRs for performance (total: %d)" , maxPRsToProcess , len (allIssues ))
@@ -359,13 +400,6 @@ func (app *App) updatePRData(url string, needsReview bool, isOwner bool, actionR
359400// fetchTurnDataSync fetches Turn API data synchronously and updates PRs directly.
360401func (app * App ) fetchTurnDataSync (ctx context.Context , issues []* github.Issue , user string , incoming * []PR , outgoing * []PR ) {
361402 turnStart := time .Now ()
362- type prResult struct {
363- err error
364- turnData * turn.CheckResponse
365- url string
366- isOwner bool
367- wasFromCache bool
368- }
369403
370404 // Create a channel for results
371405 results := make (chan prResult , len (issues ))
@@ -455,17 +489,7 @@ func (app *App) fetchTurnDataSync(ctx context.Context, issues []*github.Issue, u
455489
456490// fetchTurnDataAsync fetches Turn API data in the background and updates PRs as results arrive.
457491func (app * App ) fetchTurnDataAsync (ctx context.Context , issues []* github.Issue , user string ) {
458- // Log start of Turn API queries
459- // Start Turn API queries in background
460-
461492 turnStart := time .Now ()
462- type prResult struct {
463- err error
464- turnData * turn.CheckResponse
465- url string
466- isOwner bool
467- wasFromCache bool
468- }
469493
470494 // Create a channel for results
471495 results := make (chan prResult , len (issues ))
0 commit comments