From c5caafe9b81b055fe0587d5d2f865dc6b6663129 Mon Sep 17 00:00:00 2001 From: Aparna Jyothi Date: Wed, 12 Nov 2025 16:39:16 +0530 Subject: [PATCH 1/3] retry logic --- dist/setup/index.js | 59 +++++++++++++++++++++++------ src/install-graalpy.ts | 84 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 116 insertions(+), 27 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index f8f14af58..35290b1d2 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -97299,7 +97299,6 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re } let releaseData = findRelease(releases, graalpyVersion, architecture, false); if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) { - // check for pre-release core.info([ `Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`, `Trying pre-release versions` @@ -97321,8 +97320,7 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re else { downloadDir = await tc.extractTar(graalpyPath); } - // root folder in archive can have unpredictable name so just take the first folder - // downloadDir is unique folder under TEMP and can't contain any other folders + // folder name in archive is unpredictable const archiveName = fs_1.default.readdirSync(downloadDir)[0]; const toolDir = path.join(downloadDir, archiveName); let installDir = toolDir; @@ -97336,17 +97334,54 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re } catch (err) { if (err instanceof Error) { - // Rate limit? - if (err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { - core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); - } - else { - core.info(err.message); + const isRateLimit = err instanceof tc.HTTPError && + (err.httpStatusCode === 403 || err.httpStatusCode === 429); + if (isRateLimit) { + core.warning(`Rate limit or restricted access response received: HTTP ${err.httpStatusCode}`); + let lastStatus; + for (let attempt = 1; attempt <= 3; attempt++) { + core.info(`Retry attempt ${attempt} of 3 due to rate limit...`); + await new Promise(res => setTimeout(res, 2000 * attempt)); + try { + const retryPath = await tc.downloadTool(downloadUrl, undefined, AUTH); + core.info(`Retry succeeded.`); + // Extract retry archive + let retryExtractDir; + if (utils_1.IS_WINDOWS) { + retryExtractDir = await tc.extractZip(retryPath); + } + else { + retryExtractDir = await tc.extractTar(retryPath); + } + const archiveName = fs_1.default.readdirSync(retryExtractDir)[0]; + const toolDir = path.join(retryExtractDir, archiveName); + let installDir = toolDir; + if (!(0, utils_1.isNightlyKeyword)(resolvedGraalPyVersion)) { + installDir = await tc.cacheDir(toolDir, 'GraalPy', resolvedGraalPyVersion, architecture); + } + const binaryPath = path.join(installDir, 'bin'); + await createGraalPySymlink(binaryPath, resolvedGraalPyVersion); + await installPip(binaryPath); + return { installDir, resolvedGraalPyVersion }; + } + catch (retryErr) { + if (retryErr instanceof tc.HTTPError) { + lastStatus = retryErr.httpStatusCode; + core.warning(`Retry ${attempt} failed. HTTP ${lastStatus}`); + } + else { + core.warning(`Retry ${attempt} failed: ${retryErr}`); + } + if (attempt === 3) { + core.error(`All retries failed. Last HTTP status code: ${lastStatus ?? 'unknown'}`); + throw retryErr; + } + } + } } - if (err.stack !== undefined) { + core.info(err.message); + if (err.stack) core.debug(err.stack); - } } throw err; } diff --git a/src/install-graalpy.ts b/src/install-graalpy.ts index b1029539c..e11339531 100644 --- a/src/install-graalpy.ts +++ b/src/install-graalpy.ts @@ -38,7 +38,6 @@ export async function installGraalPy( let releaseData = findRelease(releases, graalpyVersion, architecture, false); if (allowPreReleases && (!releaseData || !releaseData.foundAsset)) { - // check for pre-release core.info( [ `Stable GraalPy version ${graalpyVersion} with arch ${architecture} not found`, @@ -69,12 +68,11 @@ export async function installGraalPy( downloadDir = await tc.extractTar(graalpyPath); } - // root folder in archive can have unpredictable name so just take the first folder - // downloadDir is unique folder under TEMP and can't contain any other folders + // folder name in archive is unpredictable const archiveName = fs.readdirSync(downloadDir)[0]; - const toolDir = path.join(downloadDir, archiveName); let installDir = toolDir; + if (!isNightlyKeyword(resolvedGraalPyVersion)) { installDir = await tc.cacheDir( toolDir, @@ -91,21 +89,77 @@ export async function installGraalPy( return {installDir, resolvedGraalPyVersion}; } catch (err) { if (err instanceof Error) { - // Rate limit? - if ( + const isRateLimit = err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429) - ) { - core.info( - `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` + (err.httpStatusCode === 403 || err.httpStatusCode === 429); + + if (isRateLimit) { + core.warning( + `Rate limit or restricted access response received: HTTP ${err.httpStatusCode}` ); - } else { - core.info(err.message); - } - if (err.stack !== undefined) { - core.debug(err.stack); + + let lastStatus: number | undefined; + + for (let attempt = 1; attempt <= 3; attempt++) { + core.info(`Retry attempt ${attempt} of 3 due to rate limit...`); + await new Promise(res => setTimeout(res, 2000 * attempt)); + + try { + const retryPath = await tc.downloadTool( + downloadUrl, + undefined, + AUTH + ); + core.info(`Retry succeeded.`); + + // Extract retry archive + let retryExtractDir; + if (IS_WINDOWS) { + retryExtractDir = await tc.extractZip(retryPath); + } else { + retryExtractDir = await tc.extractTar(retryPath); + } + + const archiveName = fs.readdirSync(retryExtractDir)[0]; + const toolDir = path.join(retryExtractDir, archiveName); + let installDir = toolDir; + + if (!isNightlyKeyword(resolvedGraalPyVersion)) { + installDir = await tc.cacheDir( + toolDir, + 'GraalPy', + resolvedGraalPyVersion, + architecture + ); + } + + const binaryPath = path.join(installDir, 'bin'); + await createGraalPySymlink(binaryPath, resolvedGraalPyVersion); + await installPip(binaryPath); + + return {installDir, resolvedGraalPyVersion}; + } catch (retryErr) { + if (retryErr instanceof tc.HTTPError) { + lastStatus = retryErr.httpStatusCode; + core.warning(`Retry ${attempt} failed. HTTP ${lastStatus}`); + } else { + core.warning(`Retry ${attempt} failed: ${retryErr}`); + } + + if (attempt === 3) { + core.error( + `All retries failed. Last HTTP status code: ${lastStatus ?? 'unknown'}` + ); + throw retryErr; + } + } + } } + + core.info(err.message); + if (err.stack) core.debug(err.stack); } + throw err; } } From 8a7c92121d4fba7fa2ba1c55a1866f378dc3e45f Mon Sep 17 00:00:00 2001 From: Aparna Jyothi Date: Wed, 19 Nov 2025 17:07:54 +0530 Subject: [PATCH 2/3] updated retry --- dist/setup/index.js | 100 ++++++++++++-------------- src/install-graalpy.ts | 154 +++++++++++++++++++---------------------- 2 files changed, 116 insertions(+), 138 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index 35290b1d2..7cdc655f7 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -97291,6 +97291,33 @@ const fs_1 = __importDefault(__nccwpck_require__(9896)); const utils_1 = __nccwpck_require__(1798); const TOKEN = core.getInput('token'); const AUTH = !TOKEN ? undefined : `token ${TOKEN}`; +/* + * Generic retry wrapper + */ +async function retry(fn, retries = 3, delayMs = 2000) { + let lastErr; + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await fn(); + } + catch (err) { + lastErr = err; + const status = err?.statusCode || + err?.httpStatusCode || + err?.response?.message?.statusCode; + const retryable = !status || status >= 500 || status === 429 || status === 403; + core.warning(`Attempt ${attempt} failed: ${err.message}. ` + + (retryable && attempt < retries + ? `Retrying in ${delayMs}ms...` + : `No more retries.`)); + if (!retryable || attempt === retries) + break; + await new Promise(res => setTimeout(res, delayMs)); + delayMs *= 2; // exponential backoff + } + } + throw lastErr; +} async function installGraalPy(graalpyVersion, architecture, allowPreReleases, releases) { let downloadDir; releases = releases ?? (await getAvailableGraalPyVersions()); @@ -97312,7 +97339,8 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re const downloadUrl = `${foundAsset.browser_download_url}`; core.info(`Downloading GraalPy from "${downloadUrl}" ...`); try { - const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH); + // ⭐ Wrapped in retry + const graalpyPath = await retry(() => tc.downloadTool(downloadUrl, undefined, AUTH), 4, 2000); core.info('Extracting downloaded archive...'); if (utils_1.IS_WINDOWS) { downloadDir = await tc.extractZip(graalpyPath); @@ -97320,7 +97348,6 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re else { downloadDir = await tc.extractTar(graalpyPath); } - // folder name in archive is unpredictable const archiveName = fs_1.default.readdirSync(downloadDir)[0]; const toolDir = path.join(downloadDir, archiveName); let installDir = toolDir; @@ -97334,54 +97361,16 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re } catch (err) { if (err instanceof Error) { - const isRateLimit = err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429); - if (isRateLimit) { - core.warning(`Rate limit or restricted access response received: HTTP ${err.httpStatusCode}`); - let lastStatus; - for (let attempt = 1; attempt <= 3; attempt++) { - core.info(`Retry attempt ${attempt} of 3 due to rate limit...`); - await new Promise(res => setTimeout(res, 2000 * attempt)); - try { - const retryPath = await tc.downloadTool(downloadUrl, undefined, AUTH); - core.info(`Retry succeeded.`); - // Extract retry archive - let retryExtractDir; - if (utils_1.IS_WINDOWS) { - retryExtractDir = await tc.extractZip(retryPath); - } - else { - retryExtractDir = await tc.extractTar(retryPath); - } - const archiveName = fs_1.default.readdirSync(retryExtractDir)[0]; - const toolDir = path.join(retryExtractDir, archiveName); - let installDir = toolDir; - if (!(0, utils_1.isNightlyKeyword)(resolvedGraalPyVersion)) { - installDir = await tc.cacheDir(toolDir, 'GraalPy', resolvedGraalPyVersion, architecture); - } - const binaryPath = path.join(installDir, 'bin'); - await createGraalPySymlink(binaryPath, resolvedGraalPyVersion); - await installPip(binaryPath); - return { installDir, resolvedGraalPyVersion }; - } - catch (retryErr) { - if (retryErr instanceof tc.HTTPError) { - lastStatus = retryErr.httpStatusCode; - core.warning(`Retry ${attempt} failed. HTTP ${lastStatus}`); - } - else { - core.warning(`Retry ${attempt} failed: ${retryErr}`); - } - if (attempt === 3) { - core.error(`All retries failed. Last HTTP status code: ${lastStatus ?? 'unknown'}`); - throw retryErr; - } - } - } + if (err instanceof tc.HTTPError && + (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { + core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); } - core.info(err.message); - if (err.stack) + else { + core.info(err.message); + } + if (err.stack !== undefined) { core.debug(err.stack); + } } throw err; } @@ -97393,12 +97382,12 @@ async function getAvailableGraalPyVersions() { headers.authorization = AUTH; } /* - Get releases first. - */ + * Stable releases with retry + */ let url = 'https://api.github.com/repos/oracle/graalpython/releases'; const result = []; do { - const response = await http.getJson(url, headers); + const response = await retry(() => http.getJson(url, headers), 4, 1500); if (!response.result) { throw new Error(`Unable to retrieve the list of available GraalPy versions from '${url}'`); } @@ -97406,12 +97395,12 @@ async function getAvailableGraalPyVersions() { url = (0, utils_1.getNextPageUrl)(response); } while (url); /* - Add pre-release builds. - */ + * Pre-release builds with retry + */ url = 'https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases'; do { - const response = await http.getJson(url, headers); + const response = await retry(() => http.getJson(url, headers), 4, 1500); if (!response.result) { throw new Error(`Unable to retrieve the list of available GraalPy versions from '${url}'`); } @@ -97490,9 +97479,6 @@ function findAsset(item, architecture, platform) { const graalpyExt = platform == 'win32' ? 'zip' : 'tar.gz'; const found = item.assets.filter(file => file.name.startsWith('graalpy') && file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.${graalpyExt}`)); - /* - In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant. - */ found.sort((f1, f2) => f1.name.length - f2.name.length); return found[0]; } diff --git a/src/install-graalpy.ts b/src/install-graalpy.ts index e11339531..0a0581b63 100644 --- a/src/install-graalpy.ts +++ b/src/install-graalpy.ts @@ -21,6 +21,45 @@ import { const TOKEN = core.getInput('token'); const AUTH = !TOKEN ? undefined : `token ${TOKEN}`; +/* + * Generic retry wrapper + */ +async function retry( + fn: () => Promise, + retries = 3, + delayMs = 2000 +): Promise { + let lastErr; + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await fn(); + } catch (err: any) { + lastErr = err; + + const status = + err?.statusCode || + err?.httpStatusCode || + err?.response?.message?.statusCode; + + const retryable = + !status || status >= 500 || status === 429 || status === 403; + + core.warning( + `Attempt ${attempt} failed: ${err.message}. ` + + (retryable && attempt < retries + ? `Retrying in ${delayMs}ms...` + : `No more retries.`) + ); + + if (!retryable || attempt === retries) break; + + await new Promise(res => setTimeout(res, delayMs)); + delayMs *= 2; // exponential backoff + } + } + throw lastErr; +} + export async function installGraalPy( graalpyVersion: string, architecture: string, @@ -59,7 +98,12 @@ export async function installGraalPy( core.info(`Downloading GraalPy from "${downloadUrl}" ...`); try { - const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH); + // ⭐ Wrapped in retry + const graalpyPath = await retry( + () => tc.downloadTool(downloadUrl, undefined, AUTH), + 4, + 2000 + ); core.info('Extracting downloaded archive...'); if (IS_WINDOWS) { @@ -68,11 +112,10 @@ export async function installGraalPy( downloadDir = await tc.extractTar(graalpyPath); } - // folder name in archive is unpredictable const archiveName = fs.readdirSync(downloadDir)[0]; + const toolDir = path.join(downloadDir, archiveName); let installDir = toolDir; - if (!isNightlyKeyword(resolvedGraalPyVersion)) { installDir = await tc.cacheDir( toolDir, @@ -89,77 +132,20 @@ export async function installGraalPy( return {installDir, resolvedGraalPyVersion}; } catch (err) { if (err instanceof Error) { - const isRateLimit = + if ( err instanceof tc.HTTPError && - (err.httpStatusCode === 403 || err.httpStatusCode === 429); - - if (isRateLimit) { - core.warning( - `Rate limit or restricted access response received: HTTP ${err.httpStatusCode}` + (err.httpStatusCode === 403 || err.httpStatusCode === 429) + ) { + core.info( + `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` ); - - let lastStatus: number | undefined; - - for (let attempt = 1; attempt <= 3; attempt++) { - core.info(`Retry attempt ${attempt} of 3 due to rate limit...`); - await new Promise(res => setTimeout(res, 2000 * attempt)); - - try { - const retryPath = await tc.downloadTool( - downloadUrl, - undefined, - AUTH - ); - core.info(`Retry succeeded.`); - - // Extract retry archive - let retryExtractDir; - if (IS_WINDOWS) { - retryExtractDir = await tc.extractZip(retryPath); - } else { - retryExtractDir = await tc.extractTar(retryPath); - } - - const archiveName = fs.readdirSync(retryExtractDir)[0]; - const toolDir = path.join(retryExtractDir, archiveName); - let installDir = toolDir; - - if (!isNightlyKeyword(resolvedGraalPyVersion)) { - installDir = await tc.cacheDir( - toolDir, - 'GraalPy', - resolvedGraalPyVersion, - architecture - ); - } - - const binaryPath = path.join(installDir, 'bin'); - await createGraalPySymlink(binaryPath, resolvedGraalPyVersion); - await installPip(binaryPath); - - return {installDir, resolvedGraalPyVersion}; - } catch (retryErr) { - if (retryErr instanceof tc.HTTPError) { - lastStatus = retryErr.httpStatusCode; - core.warning(`Retry ${attempt} failed. HTTP ${lastStatus}`); - } else { - core.warning(`Retry ${attempt} failed: ${retryErr}`); - } - - if (attempt === 3) { - core.error( - `All retries failed. Last HTTP status code: ${lastStatus ?? 'unknown'}` - ); - throw retryErr; - } - } - } + } else { + core.info(err.message); + } + if (err.stack !== undefined) { + core.debug(err.stack); } - - core.info(err.message); - if (err.stack) core.debug(err.stack); } - throw err; } } @@ -173,14 +159,18 @@ export async function getAvailableGraalPyVersions() { } /* - Get releases first. - */ + * Stable releases with retry + */ let url: string | null = 'https://api.github.com/repos/oracle/graalpython/releases'; const result: IGraalPyManifestRelease[] = []; do { - const response: ifm.TypedResponse = - await http.getJson(url, headers); + const response: ifm.TypedResponse = await retry( + () => http.getJson(url!, headers), + 4, + 1500 + ); + if (!response.result) { throw new Error( `Unable to retrieve the list of available GraalPy versions from '${url}'` @@ -191,13 +181,17 @@ export async function getAvailableGraalPyVersions() { } while (url); /* - Add pre-release builds. - */ + * Pre-release builds with retry + */ url = 'https://api.github.com/repos/graalvm/graal-languages-ea-builds/releases'; do { - const response: ifm.TypedResponse = - await http.getJson(url, headers); + const response: ifm.TypedResponse = await retry( + () => http.getJson(url!, headers), + 4, + 1500 + ); + if (!response.result) { throw new Error( `Unable to retrieve the list of available GraalPy versions from '${url}'` @@ -335,9 +329,7 @@ export function findAsset( file.name.startsWith('graalpy') && file.name.endsWith(`-${graalpyPlatform}-${graalpyArch}.${graalpyExt}`) ); - /* - In the future there could be more variants of GraalPy for a single release. Pick the shortest name, that one is the most likely to be the primary variant. - */ + found.sort((f1, f2) => f1.name.length - f2.name.length); return found[0]; } From fd849401e807714db5fbcb55aee4283e70f67afc Mon Sep 17 00:00:00 2001 From: Aparna Jyothi Date: Tue, 25 Nov 2025 17:28:42 +0530 Subject: [PATCH 3/3] rate limit retry --- dist/setup/index.js | 41 +++++++++++++++++++++--------- src/install-graalpy.ts | 57 ++++++++++++++++++++++++++++++++---------- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index 7cdc655f7..fa990fdaa 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -97292,9 +97292,12 @@ const utils_1 = __nccwpck_require__(1798); const TOKEN = core.getInput('token'); const AUTH = !TOKEN ? undefined : `token ${TOKEN}`; /* - * Generic retry wrapper + * Improved Generic retry wrapper with: + * - Retry-After support + * - Jitter + * - Network error retries (ECONNRESET, ETIMEDOUT, etc.) */ -async function retry(fn, retries = 3, delayMs = 2000) { +async function retry(fn, retries = 4, baseDelay = 2000) { let lastErr; for (let attempt = 1; attempt <= retries; attempt++) { try { @@ -97304,16 +97307,30 @@ async function retry(fn, retries = 3, delayMs = 2000) { lastErr = err; const status = err?.statusCode || err?.httpStatusCode || + err?.response?.status || err?.response?.message?.statusCode; - const retryable = !status || status >= 500 || status === 429 || status === 403; - core.warning(`Attempt ${attempt} failed: ${err.message}. ` + - (retryable && attempt < retries - ? `Retrying in ${delayMs}ms...` - : `No more retries.`)); - if (!retryable || attempt === retries) + const retryAfter = err?.response?.headers?.['retry-after'] || + err?.response?.headers?.['Retry-After']; + const networkRetryable = err?.code && + ['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND'].includes(err.code); + const statusRetryable = !status || status >= 500 || status === 429 || status === 403; + const retryable = statusRetryable || networkRetryable; + if (!retryable || attempt === retries) { + core.warning(`Attempt ${attempt} failed with non-retryable error: ${err.message}`); break; - await new Promise(res => setTimeout(res, delayMs)); - delayMs *= 2; // exponential backoff + } + // Use Retry-After header if available + let delay = baseDelay * Math.pow(2, attempt - 1); + if (retryAfter) { + delay = parseInt(retryAfter, 10) * 1000; + } + // Add jitter + delay += Math.floor(Math.random() * 1000); + core.warning(`Attempt ${attempt} failed: ${err.message}. ` + + `Status: ${status}. ` + + (retryAfter ? `Retry-After: ${retryAfter}s. ` : '') + + `Retrying in ${delay}ms...`); + await new Promise(res => setTimeout(res, delay)); } } throw lastErr; @@ -97339,7 +97356,7 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re const downloadUrl = `${foundAsset.browser_download_url}`; core.info(`Downloading GraalPy from "${downloadUrl}" ...`); try { - // ⭐ Wrapped in retry + // ⭐ Wrapped in improved retry const graalpyPath = await retry(() => tc.downloadTool(downloadUrl, undefined, AUTH), 4, 2000); core.info('Extracting downloaded archive...'); if (utils_1.IS_WINDOWS) { @@ -97363,7 +97380,7 @@ async function installGraalPy(graalpyVersion, architecture, allowPreReleases, re if (err instanceof Error) { if (err instanceof tc.HTTPError && (err.httpStatusCode === 403 || err.httpStatusCode === 429)) { - core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); + core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`); } else { core.info(err.message); diff --git a/src/install-graalpy.ts b/src/install-graalpy.ts index 0a0581b63..6518a1c8b 100644 --- a/src/install-graalpy.ts +++ b/src/install-graalpy.ts @@ -22,14 +22,18 @@ const TOKEN = core.getInput('token'); const AUTH = !TOKEN ? undefined : `token ${TOKEN}`; /* - * Generic retry wrapper + * Improved Generic retry wrapper with: + * - Retry-After support + * - Jitter + * - Network error retries (ECONNRESET, ETIMEDOUT, etc.) */ async function retry( fn: () => Promise, - retries = 3, - delayMs = 2000 + retries = 4, + baseDelay = 2000 ): Promise { let lastErr; + for (let attempt = 1; attempt <= retries; attempt++) { try { return await fn(); @@ -39,24 +43,51 @@ async function retry( const status = err?.statusCode || err?.httpStatusCode || + err?.response?.status || err?.response?.message?.statusCode; - const retryable = + const retryAfter = + err?.response?.headers?.['retry-after'] || + err?.response?.headers?.['Retry-After']; + + const networkRetryable = + err?.code && + ['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'ENOTFOUND'].includes( + err.code + ); + + const statusRetryable = !status || status >= 500 || status === 429 || status === 403; + const retryable = statusRetryable || networkRetryable; + + if (!retryable || attempt === retries) { + core.warning( + `Attempt ${attempt} failed with non-retryable error: ${err.message}` + ); + break; + } + + // Use Retry-After header if available + let delay = baseDelay * Math.pow(2, attempt - 1); + if (retryAfter) { + delay = parseInt(retryAfter, 10) * 1000; + } + + // Add jitter + delay += Math.floor(Math.random() * 1000); + core.warning( `Attempt ${attempt} failed: ${err.message}. ` + - (retryable && attempt < retries - ? `Retrying in ${delayMs}ms...` - : `No more retries.`) + `Status: ${status}. ` + + (retryAfter ? `Retry-After: ${retryAfter}s. ` : '') + + `Retrying in ${delay}ms...` ); - if (!retryable || attempt === retries) break; - - await new Promise(res => setTimeout(res, delayMs)); - delayMs *= 2; // exponential backoff + await new Promise(res => setTimeout(res, delay)); } } + throw lastErr; } @@ -98,7 +129,7 @@ export async function installGraalPy( core.info(`Downloading GraalPy from "${downloadUrl}" ...`); try { - // ⭐ Wrapped in retry + // ⭐ Wrapped in improved retry const graalpyPath = await retry( () => tc.downloadTool(downloadUrl, undefined, AUTH), 4, @@ -137,7 +168,7 @@ export async function installGraalPy( (err.httpStatusCode === 403 || err.httpStatusCode === 429) ) { core.info( - `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` + `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` ); } else { core.info(err.message);