Skip to content

Commit 24d0730

Browse files
committed
fix: classify 429 as rate limit and scope 403 permission failures per repo
Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
1 parent 5bfa6ab commit 24d0730

2 files changed

Lines changed: 25 additions & 13 deletions

File tree

services/apps/security_best_practices_worker/src/activities/index.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ export async function saveOSPSBaselineInsightsToRedis(
183183
await redisCache.set(key, JSON.stringify(insights), 60 * 60 * 24) // 1 day
184184
}
185185

186-
// GitHub returns 403 for both rate-limit exhaustion AND persistent permission/SAML failures.
187-
// Only rate-limit responses should trigger the 1h token-rotation cooldown; permission failures
188-
// must be marked as invalid so the token isn't retried in a loop.
186+
// GitHub returns 403 for rate-limit exhaustion AND persistent permission/SAML failures, and
187+
// 429 for primary rate-limit hits. Only 401 proves the token is globally invalid — SAML/repo
188+
// permissions vary per org, so a permission 403 on one repo doesn't mean the token is unusable
189+
// elsewhere.
189190
function classifyTokenError(output: string, source: string): void {
190191
// Wall-clock timestamp of the failure. Passed via ApplicationFailure.details so the
191192
// workflow can persist an accurate rateLimitedAt — workflow code can't call Date.now()
@@ -201,25 +202,30 @@ function classifyTokenError(output: string, source: string): void {
201202
details: [failedAtMs],
202203
})
203204
}
204-
if (!output.includes('403')) return
205205

206-
// Rate-limit 403s from GitHub carry a distinctive body: "API rate limit exceeded" or
207-
// "secondary rate limit". Anything else (SAML enforcement, missing scopes, resource
208-
// not accessible) is a permission problem and the token won't recover.
209-
const isRateLimit = /rate limit|rate_limit|secondary rate/i.test(output)
206+
const has403 = output.includes('403')
207+
const has429 = output.includes('429')
208+
if (!has403 && !has429) return
209+
210+
// Rate-limit responses carry a distinctive body: "API rate limit exceeded" or
211+
// "secondary rate limit". 429 is always a rate-limit signal. 403 without those markers
212+
// is a permission problem (SAML enforcement, missing scopes, resource not accessible).
213+
const isRateLimit = has429 || /rate limit|rate_limit|secondary rate/i.test(output)
210214
if (isRateLimit) {
211-
svc.log.warn(`Detected 403 rate-limit in ${source} - token rate-limited!`)
215+
svc.log.warn(`Detected rate-limit in ${source} - token rate-limited!`)
212216
throw ApplicationFailure.create({
213217
message: 'GitHub token rate-limited',
214218
type: 'Token403Error',
215219
nonRetryable: true,
216220
details: [failedAtMs],
217221
})
218222
}
219-
svc.log.warn(`Detected 403 permission error in ${source} - token lacks required access!`)
223+
// Permission 403: this token can't access THIS repo, but may work for others. Signal the
224+
// workflow to try a different token without marking this one globally invalid.
225+
svc.log.warn(`Detected 403 permission error in ${source} - token lacks access to this repo!`)
220226
throw ApplicationFailure.create({
221-
message: 'GitHub token lacks required permissions',
222-
type: 'TokenAuthError',
227+
message: 'GitHub token lacks required permissions for this repo',
228+
type: 'TokenRepoAccessError',
223229
nonRetryable: true,
224230
details: [failedAtMs],
225231
})

services/apps/security_best_practices_worker/src/workflows/triggerSecurityInsightsCheckForRepos.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export async function triggerSecurityInsightsCheckForRepos(
8989
initialInterval: 2000,
9090
backoffCoefficient: 2,
9191
maximumInterval: 30000,
92-
nonRetryableErrorTypes: ['Token403Error', 'TokenAuthError'],
92+
nonRetryableErrorTypes: ['Token403Error', 'TokenAuthError', 'TokenRepoAccessError'],
9393
},
9494
args: [
9595
{
@@ -120,6 +120,12 @@ export async function triggerSecurityInsightsCheckForRepos(
120120
tokenInfo.isInvalid = true
121121
attempts++
122122
continue // retry with a different token
123+
} else if (appFailure?.type === 'TokenRepoAccessError') {
124+
// Token can't access this repo (SAML/perms) but may work elsewhere — don't taint
125+
// the token's global state, just try the next one for this repo.
126+
console.warn(`Token lacks access to repo ${repo.repoUrl}, trying different token`)
127+
attempts++
128+
continue
123129
} else {
124130
console.error(`Failed to process repo ${repo.repoUrl}:`, error)
125131
// we retried this error using the retry policy (because it's not a token non-retryable error)

0 commit comments

Comments
 (0)