@@ -45,6 +45,17 @@ export async function triggerSecurityInsightsCheckForRepos(
4545 // for expiry) and isInvalid survive across continueAsNew batches.
4646 const tokenInfos : ITokenInfo [ ] = await initializeTokenInfos ( )
4747
48+ // Empty pool means CROWD_GITHUB_PERSONAL_ACCESS_TOKENS is misconfigured. Without this guard
49+ // an empty array would satisfy `tokenInfos.every(t => t.isInvalid)` and silently mark every
50+ // obsolete repo as failed for the continueAsNew chain, hiding the config error.
51+ if ( tokenInfos . length === 0 ) {
52+ throw ApplicationFailure . create ( {
53+ message : 'No GitHub tokens configured (CROWD_GITHUB_PERSONAL_ACCESS_TOKENS is empty)' ,
54+ type : 'TokenPoolEmpty' ,
55+ nonRetryable : true ,
56+ } )
57+ }
58+
4859 // Scale attempts to pool size so a repo isn't marked failed while untried tokens remain
4960 // (e.g. 6 PATs where the first 5 attempts all 403 on different tokens).
5061 const MAX_TOKEN_ATTEMPTS = Math . max ( 5 , tokenInfos . length )
@@ -88,6 +99,10 @@ export async function triggerSecurityInsightsCheckForRepos(
8899 repoAttemptedTokens . set ( repo . repoUrl , attemptedTokens )
89100 }
90101 while ( attempts < MAX_TOKEN_ATTEMPTS ) {
102+ // Refresh wall-clock time before each token selection. A long-running executeChild can
103+ // block the outer queue loop's refresh for >1h, leaving rate-limit cooldowns stuck when
104+ // this is the only in-flight task (small pool or last repo).
105+ currentTimeMs = await getCurrentTimeMs ( )
91106 const tokenInfo = getNextToken ( tokenInfos , currentTimeMs , attemptedTokens )
92107 if ( ! tokenInfo ) {
93108 // No untried, usable token remains for this repo. Fail only when either every token
0 commit comments