@@ -4,76 +4,148 @@ namespace DNNCommunity.DNNDocs.Plugins.Providers
44{
55 public class GitHubApi
66 {
7- private readonly string token ;
7+ private readonly string ? token ;
88 private readonly GitHubClient client ;
99
10- private const int RateLimitThreshold = 1000 ; // Starts throtling if less than x calls remaining for the next hour.
11- private const int MaxDelayMs = 15000 ; // Maximum delay of 15 seconds.
10+ private const int RateLimitThreshold = 100 ; // Start warning if less than this many calls remain for the hour.
11+ private const int MaxDelayMs = 5000 ; // Maximum progressive-backoff delay of 5 seconds.
1212 private const double ExponentialBase = 1.5 ; // Exponential base for progressive backoff.
1313
1414 public GitHubApi ( )
1515 {
16- DotNetEnv . Env . Load ( ) ; // Loads .env file if it exists, into environment varialbes .
16+ DotNetEnv . Env . Load ( ) ; // Loads .env file if it exists, into environment variables .
1717 client = new GitHubClient ( new ProductHeaderValue ( "DNNDocsPlugin" ) ) ;
18-
19- // Optionally pull from env if not passed
18+
2019 var skip = Environment . GetEnvironmentVariable ( "SKIP_CONTRIBUTORS" ) == "true" ;
2120 if ( ! skip )
2221 {
23- this . token = Environment . GetEnvironmentVariable ( "GithubToken" ) ?? string . Empty ;
22+ // Prefer a PAT (ACCESS_TOKEN) for higher rate limits; fall back to GithubToken (GITHUB_TOKEN in CI).
23+ this . token = Environment . GetEnvironmentVariable ( "ACCESS_TOKEN" ) ;
24+ if ( string . IsNullOrEmpty ( this . token ) )
25+ {
26+ this . token = Environment . GetEnvironmentVariable ( "GithubToken" ) ;
27+ }
2428 }
2529
2630 if ( ! string . IsNullOrEmpty ( this . token ) )
2731 {
2832 client . Credentials = new Credentials ( this . token ) ;
2933 }
3034
31- var rateLimits = client . RateLimit . GetRateLimits ( ) . GetAwaiter ( ) . GetResult ( ) ;
32- Console . WriteLine ( $ "Remaining: { rateLimits . Resources . Core . Remaining } , Resets at: { rateLimits . Resources . Core . Reset } ") ;
35+ try
36+ {
37+ var rateLimits = client . RateLimit . GetRateLimits ( ) . GetAwaiter ( ) . GetResult ( ) ;
38+ Console . WriteLine ( $ "[RepoStats] GitHub API remaining: { rateLimits . Resources . Core . Remaining } /{ rateLimits . Resources . Core . Limit } , resets at: { rateLimits . Resources . Core . Reset } ") ;
39+ }
40+ catch ( Exception ex )
41+ {
42+ Console . WriteLine ( $ "[RepoStats] WARNING: Could not retrieve GitHub rate limit info: { ex . Message } ") ;
43+ }
3344 }
3445
3546 public async Task < IReadOnlyList < Contributor > > GetContributorsAsync ( )
3647 {
3748 if ( string . IsNullOrEmpty ( this . token ) )
49+ {
50+ Console . WriteLine ( "[RepoStats] No GitHub token configured — skipping contributor stats." ) ;
51+ return new List < Contributor > ( ) ;
52+ }
53+
54+ if ( ! await this . ThrottleIfNeeded ( ) )
3855 {
3956 return new List < Contributor > ( ) ;
4057 }
4158
42- await this . ThrottleIfNeeded ( ) ;
43- return await client . Repository . Statistics . GetContributors ( "DNNCommunity" , "DNNDocs" ) ;
59+ try
60+ {
61+ return await client . Repository . Statistics . GetContributors ( "DNNCommunity" , "DNNDocs" ) ;
62+ }
63+ catch ( RateLimitExceededException ex )
64+ {
65+ Console . WriteLine ( $ "[RepoStats] WARNING: GitHub API rate limit exceeded while fetching contributors. Skipping stats. Reset at: { ex . Reset } ") ;
66+ return new List < Contributor > ( ) ;
67+ }
68+ catch ( Exception ex )
69+ {
70+ Console . WriteLine ( $ "[RepoStats] WARNING: Failed to fetch contributors: { ex . Message } . Skipping stats.") ;
71+ return new List < Contributor > ( ) ;
72+ }
4473 }
4574
4675 public async Task < IReadOnlyList < GitHubCommit > > GetCommitsAsync ( string path = "" )
4776 {
4877 if ( string . IsNullOrEmpty ( this . token ) )
78+ {
79+ Console . WriteLine ( "[RepoStats] No GitHub token configured — skipping commit stats." ) ;
80+ return new List < GitHubCommit > ( ) ;
81+ }
82+
83+ if ( ! await this . ThrottleIfNeeded ( ) )
4984 {
5085 return new List < GitHubCommit > ( ) ;
5186 }
5287
53- var request = new CommitRequest ( ) ;
54- if ( ! string . IsNullOrEmpty ( path ) )
55- request . Path = path ;
88+ try
89+ {
90+ var request = new CommitRequest ( ) ;
91+ if ( ! string . IsNullOrEmpty ( path ) )
92+ request . Path = path ;
5693
57- await this . ThrottleIfNeeded ( ) ;
58- return await client . Repository . Commit . GetAll ( "DNNCommunity" , "DNNDocs" , request ) ;
94+ return await client . Repository . Commit . GetAll ( "DNNCommunity" , "DNNDocs" , request ) ;
95+ }
96+ catch ( RateLimitExceededException ex )
97+ {
98+ Console . WriteLine ( $ "[RepoStats] WARNING: GitHub API rate limit exceeded while fetching commits. Skipping stats. Reset at: { ex . Reset } ") ;
99+ return new List < GitHubCommit > ( ) ;
100+ }
101+ catch ( Exception ex )
102+ {
103+ Console . WriteLine ( $ "[RepoStats] WARNING: Failed to fetch commits: { ex . Message } . Skipping stats.") ;
104+ return new List < GitHubCommit > ( ) ;
105+ }
59106 }
60107
61- private async Task ThrottleIfNeeded ( )
108+ /// <summary>
109+ /// Checks the current rate limit. Returns <c>true</c> if it is safe to proceed with an API call,
110+ /// or <c>false</c> if the rate limit is exhausted and callers should skip their request.
111+ /// When nearing (but not at) the limit, a short progressive backoff delay is applied.
112+ /// </summary>
113+ private async Task < bool > ThrottleIfNeeded ( )
62114 {
63- var rateLimits = await client . RateLimit . GetRateLimits ( ) ;
64- var remaining = rateLimits . Resources . Core . Remaining ;
65- var limit = rateLimits . Resources . Core . Limit ;
66- var reset = rateLimits . Resources . Core . Reset ;
115+ try
116+ {
117+ var rateLimits = await client . RateLimit . GetRateLimits ( ) ;
118+ var remaining = rateLimits . Resources . Core . Remaining ;
119+ var limit = rateLimits . Resources . Core . Limit ;
120+ var reset = rateLimits . Resources . Core . Reset ;
67121
122+ if ( remaining == 0 )
123+ {
124+ var waitSeconds = ( int ) ( reset - DateTimeOffset . UtcNow ) . TotalSeconds + 5 ;
125+ Console . WriteLine ( $ "[RepoStats] WARNING: GitHub API rate limit fully exhausted (0/{ limit } ). Resets at { reset } UTC (~{ waitSeconds } s). Skipping stats to avoid blocking the build.") ;
126+ return false ;
127+ }
68128
69- if ( remaining < RateLimitThreshold )
70- {
71- int delayFactor = RateLimitThreshold - remaining ;
72- int delayMs = ( int ) Math . Pow ( ExponentialBase , delayFactor ) ;
73- delayMs = Math . Min ( delayMs , MaxDelayMs ) ;
129+ if ( remaining < RateLimitThreshold )
130+ {
131+ int delayFactor = RateLimitThreshold - remaining ;
132+ int delayMs = ( int ) Math . Min ( Math . Pow ( ExponentialBase , delayFactor ) , MaxDelayMs ) ;
133+
134+ Console . WriteLine ( $ "[RepoStats] Nearing GitHub API rate limit, applying backoff of { delayMs } ms. Remaining: { remaining } /{ limit } , resets at { reset } .") ;
135+ await Task . Delay ( delayMs ) ;
136+ }
74137
75- Console . WriteLine ( $ "[Throttle] Nearing rate limit, delaying { delayMs } ms... GitHub API remaining: { remaining } /{ limit } , resets at { reset } ") ;
76- await Task . Delay ( delayMs ) ;
138+ return true ;
139+ }
140+ catch ( RateLimitExceededException ex )
141+ {
142+ Console . WriteLine ( $ "[RepoStats] WARNING: GitHub API rate limit exceeded during throttle check. Skipping stats. Reset at: { ex . Reset } ") ;
143+ return false ;
144+ }
145+ catch ( Exception ex )
146+ {
147+ Console . WriteLine ( $ "[RepoStats] WARNING: Could not check rate limit ({ ex . Message } ). Proceeding cautiously.") ;
148+ return true ;
77149 }
78150 }
79151 }
0 commit comments