diff --git a/docs/scripts/update-contributors.js b/docs/scripts/update-contributors.js index 7222ffadd..1836b9fae 100644 --- a/docs/scripts/update-contributors.js +++ b/docs/scripts/update-contributors.js @@ -1,4 +1,5 @@ import fs from "node:fs/promises"; +import timers from "node:timers/promises"; import { URL } from "node:url"; const MAINTAINERS = { @@ -220,16 +221,61 @@ class UserFetchError extends Error { } } -async function fetchUserInfo(username) { - const res = await fetch(`https://api.github.com/users/${username}`, { +const BACKOFF_INTERVALS_MINUTES = [1, 2, 3, 5]; +const MAX_RETRIES = BACKOFF_INTERVALS_MINUTES.length - 1; + +async function rateLimitDelay({ retryAfter, ratelimitReset, retryCount }) { + let timeoutInMilliseconds = BACKOFF_INTERVALS_MINUTES[retryCount] * 1000; + if (retryAfter) { + timeoutInMilliseconds = retryAfter * 1000; + } else if (ratelimitRemaining === "0" && ratelimitReset) { + timeoutInMilliseconds = ratelimitReset * 1000 - Date.now(); + } + + const timeoutInSeconds = (timeoutInMilliseconds / 1000).toLocaleString(); + console.warn(`Waiting for ${timeoutInSeconds} seconds...`); + + await timers.setTimeout(timeoutInMilliseconds); +} + +async function fetchUserInfo(username, retryCount = 0) { + if (retryCount >= MAX_RETRIES) { + throw new Error(`Hit max retries (${MAX_RETRIES}) for fetching user ${username}`); + } + + const request = new Request(`https://api.github.com/users/${username}`, { headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, }); + if (process.env.GITHUB_TOKEN) { + request.headers.set("Authorization", `Bearer ${process.env.GITHUB_TOKEN}`); + } + + const res = await fetch(request); + if (!res.ok) { + const retryAfter = res.headers.get("retry-after"); // seconds + const ratelimitRemaining = res.headers.get("x-ratelimit-remaining"); // quantity of requests remaining + const ratelimitReset = res.headers.get("x-ratelimit-reset"); // UTC epoch seconds + + if (retryAfter || ratelimitRemaining === "0" || ratelimitReset) { + // See https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api?apiVersion=2022-11-28#handle-rate-limit-errors-appropriately + console.warn("Rate limited by GitHub API"); + + await rateLimitDelay({ + retryAfter, + ratelimitReset, + retryCount, + }); + + return await fetchUserInfo(username, retryCount + 1); + } + throw new UserFetchError(`${res.url} responded with ${res.status}`, res); } + return await res.json(); } @@ -282,4 +328,6 @@ async function main() { } } -main(); +if (import.meta.main) { + main(); +}