Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 53 additions & 15 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97291,6 +97291,50 @@ const fs_1 = __importDefault(__nccwpck_require__(9896));
const utils_1 = __nccwpck_require__(1798);
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;
/*
* Improved Generic retry wrapper with:
* - Retry-After support
* - Jitter
* - Network error retries (ECONNRESET, ETIMEDOUT, etc.)
*/
async function retry(fn, retries = 4, baseDelay = 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?.status ||
err?.response?.message?.statusCode;
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}. ` +
`Status: ${status}. ` +
(retryAfter ? `Retry-After: ${retryAfter}s. ` : '') +
`Retrying in ${delay}ms...`);
await new Promise(res => setTimeout(res, delay));
}
}
throw lastErr;
}
async function installGraalPy(graalpyVersion, architecture, allowPreReleases, releases) {
let downloadDir;
releases = releases ?? (await getAvailableGraalPyVersions());
Expand All @@ -97299,7 +97343,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`
Expand All @@ -97313,16 +97356,15 @@ 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 improved 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);
}
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
const archiveName = fs_1.default.readdirSync(downloadDir)[0];
const toolDir = path.join(downloadDir, archiveName);
let installDir = toolDir;
Expand All @@ -97336,10 +97378,9 @@ 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`);
core.info(`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`);
}
else {
core.info(err.message);
Expand All @@ -97358,25 +97399,25 @@ 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}'`);
}
result.push(...response.result);
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}'`);
}
Expand Down Expand Up @@ -97455,9 +97496,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];
}
Expand Down
111 changes: 94 additions & 17 deletions src/install-graalpy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,76 @@ import {
const TOKEN = core.getInput('token');
const AUTH = !TOKEN ? undefined : `token ${TOKEN}`;

/*
* Improved Generic retry wrapper with:
* - Retry-After support
* - Jitter
* - Network error retries (ECONNRESET, ETIMEDOUT, etc.)
*/
async function retry<T>(
fn: () => Promise<T>,
retries = 4,
baseDelay = 2000
): Promise<T> {
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?.status ||
err?.response?.message?.statusCode;

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}. ` +
`Status: ${status}. ` +
(retryAfter ? `Retry-After: ${retryAfter}s. ` : '') +
`Retrying in ${delay}ms...`
);

await new Promise(res => setTimeout(res, delay));
}
}

throw lastErr;
}

export async function installGraalPy(
graalpyVersion: string,
architecture: string,
Expand All @@ -38,7 +108,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`,
Expand All @@ -60,7 +129,12 @@ export async function installGraalPy(
core.info(`Downloading GraalPy from "${downloadUrl}" ...`);

try {
const graalpyPath = await tc.downloadTool(downloadUrl, undefined, AUTH);
// ⭐ Wrapped in improved retry
const graalpyPath = await retry(
() => tc.downloadTool(downloadUrl, undefined, AUTH),
4,
2000
);

core.info('Extracting downloaded archive...');
if (IS_WINDOWS) {
Expand All @@ -69,8 +143,6 @@ 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
const archiveName = fs.readdirSync(downloadDir)[0];

const toolDir = path.join(downloadDir, archiveName);
Expand All @@ -91,13 +163,12 @@ export async function installGraalPy(
return {installDir, resolvedGraalPyVersion};
} 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`
`Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded`
);
} else {
core.info(err.message);
Expand All @@ -119,14 +190,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<IGraalPyManifestRelease[]> =
await http.getJson(url, headers);
const response: ifm.TypedResponse<IGraalPyManifestRelease[]> = 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}'`
Expand All @@ -137,13 +212,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<IGraalPyManifestRelease[]> =
await http.getJson(url, headers);
const response: ifm.TypedResponse<IGraalPyManifestRelease[]> = 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}'`
Expand Down Expand Up @@ -281,9 +360,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];
}
Loading