From 2fdf20c985f54ba844e3cb28d00996176e2f71f7 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 25 Sep 2024 04:20:41 -0700 Subject: [PATCH] feat: setup custom retry with builtin node fetch (#304) --- .github/workflows/action.yml | 3 +- .github/workflows/ubuntu.yml | 8 +-- dist/index.js | 101 ++++++++++++++++++----------------- package-lock.json | 1 - package.json | 1 - src/setup-beam.js | 101 ++++++++++++++++++----------------- test/setup-beam.test.js | 31 +++++++++-- 7 files changed, 135 insertions(+), 111 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 949cbf7..5bd12ea 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -7,7 +7,7 @@ on: - main pull_request: branches: - - "*" + - '*' workflow_dispatch: {} env: @@ -62,6 +62,7 @@ jobs: id: setup-beam uses: ./ with: + install-rebar: false version-file: test/.tool-versions version-type: strict diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 48126da..e25dc40 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -7,7 +7,7 @@ on: - main pull_request: branches: - - "*" + - '*' workflow_dispatch: {} jobs: @@ -53,10 +53,6 @@ jobs: elixir-version: '1.16' rebar3-version: '3' os: 'ubuntu-22.04' - - otp-version: '24' - elixir-version: '1.16' - rebar3-version: '3' - os: 'ubuntu-22.04' - otp-version: '25.0' elixir-version: 'v1.13.4-otp-25' rebar3-version: '3.18.0' @@ -121,7 +117,7 @@ jobs: otp-version: false os: 'ubuntu-latest' disable_problem_matchers: true - - gleam-version: '0.22.0' # Version with Gleam's old archive naming convention + - gleam-version: '0.22.0' # Version with Gleam's old archive naming convention otp-version: '24' os: 'ubuntu-latest' disable_problem_matchers: true diff --git a/dist/index.js b/dist/index.js index 80e709b..ce2ab74 100644 --- a/dist/index.js +++ b/dist/index.js @@ -9106,12 +9106,13 @@ exports["default"] = _default; const core = __nccwpck_require__(2186) const { exec } = __nccwpck_require__(1514) const tc = __nccwpck_require__(7784) -const http = __nccwpck_require__(6255) const path = __nccwpck_require__(1017) const semver = __nccwpck_require__(1383) const fs = __nccwpck_require__(7147) const os = __nccwpck_require__(2037) +const MAX_HTTP_RETRIES = 3 + main().catch((err) => { core.setFailed(err.message) }) @@ -9361,8 +9362,7 @@ async function getOTPVersions(osVersion) { hexMirrors: hexMirrorsInput(), actionTitle: `fetch ${originListing}`, action: async (hexMirror) => { - const l = await get(`${hexMirror}${originListing}`, [null]) - return l + return get(`${hexMirror}${originListing}`, []) }, }) } else if (process.platform === 'win32') { @@ -9389,7 +9389,7 @@ async function getOTPVersions(osVersion) { }) } else if (process.platform === 'win32') { otpVersionsListings.forEach((otpVersionsListing) => { - jsonParseAsList(otpVersionsListing) + otpVersionsListing .map((x) => x.assets) .flat() .filter((x) => x.name.match(/^otp_win64_.*.exe$/)) @@ -9413,8 +9413,7 @@ async function getElixirVersions() { hexMirrors: hexMirrorsInput(), actionTitle: `fetch ${originListing}`, action: async (hexMirror) => { - const l = await get(`${hexMirror}${originListing}`, [null]) - return l + return get(`${hexMirror}${originListing}`, []) }, }) const otpVersionsForElixirMap = {} @@ -9447,7 +9446,7 @@ async function getGleamVersions() { ) const gleamVersionsListing = {} resultJSONs.forEach((resultJSON) => { - jsonParseAsList(resultJSON) + resultJSON .map((x) => x.tag_name) .forEach((ver) => { const gleamMatch = ver.match(/^v?([^ ]+)/) @@ -9466,7 +9465,7 @@ async function getRebar3Versions() { ) const rebar3VersionsListing = {} resultJSONs.forEach((resultJSON) => { - jsonParseAsList(resultJSON) + resultJSON .map((x) => x.tag_name) .forEach((ver) => { rebar3VersionsListing[ver] = ver @@ -9660,38 +9659,56 @@ function getRunnerOSVersion() { return containerFromEnvImageOS } -async function get(url0, pageIdxs) { - async function getPage(pageIdx) { - const url = new URL(url0) - const headers = {} - const GithubToken = getInput('github-token', false) - if (GithubToken && url.host === 'api.github.com') { - headers.authorization = `Bearer ${GithubToken}` - } - - if (pageIdx !== null) { - url.searchParams.append('page', pageIdx) - } - - const httpClient = new http.HttpClient('setup-beam', [], { - allowRetries: true, - maxRetries: 3, +async function getUrlResponse(url, headers, attempt = 1) { + try { + const response = await fetch(url, { + headers, + signal: AbortSignal.timeout(10000), }) - const response = await httpClient.get(url, headers) - if (response.statusCode >= 400 && response.statusCode <= 599) { - throw new Error( - `Got ${response.statusCode} from ${url}. Exiting with error`, - ) + const contentType = response.headers.get('content-type') || '' + + if (!response.ok) { + throw new Error(response.statusText) } - return response.readBody() + if (contentType.indexOf('application/json') !== -1) { + return response.json() + } else { + return response.text() + } + } catch (err) { + core.debug(`Error fetching from ${url}: ${err}`) + + if (attempt <= MAX_HTTP_RETRIES) { + const delay = attempt * 2 * 1000 + core.debug(`Error during fetch. Retrying in ${delay}ms`) + await new Promise((resolve) => setTimeout(resolve, delay)) + return getUrlResponse(url, headers, attempt + 1) + } else { + throw err + } + } +} + +async function get(url0, pageIdxs) { + const url = new URL(url0) + const headers = {} + const GithubToken = getInput('github-token', false) + if (GithubToken && url.host === 'api.github.com') { + headers.authorization = `Bearer ${GithubToken}` } - if (pageIdxs[0] === null) { - return getPage(null) + if (pageIdxs.length === 0) { + return getUrlResponse(url, headers) + } else { + return Promise.all( + pageIdxs.map((page) => { + const urlWithPage = new URL(url) + urlWithPage.searchParams.append('page', page) + return getUrlResponse(urlWithPage, headers) + }), + ) } - - return Promise.all(pageIdxs.map(getPage)) } function maybePrependWithV(v) { @@ -9786,21 +9803,6 @@ function parseVersionFile(versionFilePath0) { return appVersions } -function jsonParseAsList(maybeJson) { - try { - const obj = JSON.parse(maybeJson) - if (!Array.isArray(obj)) { - throw new Error('expected a list!') - } - - return obj - } catch (exc) { - throw new Error( - `Got an exception when trying to parse non-JSON list ${maybeJson}: ${exc}`, - ) - } -} - function debugLog(groupName, message) { const group = `Debugging for ${groupName}` core.debug( @@ -10158,6 +10160,7 @@ function debugLoggingEnabled() { } module.exports = { + get, getOTPVersion, getElixirVersion, getGleamVersion, diff --git a/package-lock.json b/package-lock.json index 6228057..5ed1441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "dependencies": { "@actions/core": "1.10.0", "@actions/exec": "1.1.1", - "@actions/http-client": "2.1.0", "@actions/tool-cache": "2.0.1", "semver": "7.6.2" }, diff --git a/package.json b/package.json index 6686828..7f0a507 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "dependencies": { "@actions/core": "1.10.0", "@actions/exec": "1.1.1", - "@actions/http-client": "2.1.0", "@actions/tool-cache": "2.0.1", "semver": "7.6.2" }, diff --git a/src/setup-beam.js b/src/setup-beam.js index a06eac2..bfd8ca5 100644 --- a/src/setup-beam.js +++ b/src/setup-beam.js @@ -1,12 +1,13 @@ const core = require('@actions/core') const { exec } = require('@actions/exec') const tc = require('@actions/tool-cache') -const http = require('@actions/http-client') const path = require('path') const semver = require('semver') const fs = require('fs') const os = require('os') +const MAX_HTTP_RETRIES = 3 + main().catch((err) => { core.setFailed(err.message) }) @@ -261,8 +262,7 @@ async function getOTPVersions(osVersion) { hexMirrors: hexMirrorsInput(), actionTitle: `fetch ${originListing}`, action: async (hexMirror) => { - const l = await get(`${hexMirror}${originListing}`, [null]) - return l + return get(`${hexMirror}${originListing}`, []) }, }) } else if (process.platform === 'win32') { @@ -289,7 +289,7 @@ async function getOTPVersions(osVersion) { }) } else if (process.platform === 'win32') { otpVersionsListings.forEach((otpVersionsListing) => { - jsonParseAsList(otpVersionsListing) + otpVersionsListing .map((x) => x.assets) .flat() .filter((x) => x.name.match(/^otp_win64_.*.exe$/)) @@ -313,8 +313,7 @@ async function getElixirVersions() { hexMirrors: hexMirrorsInput(), actionTitle: `fetch ${originListing}`, action: async (hexMirror) => { - const l = await get(`${hexMirror}${originListing}`, [null]) - return l + return get(`${hexMirror}${originListing}`, []) }, }) const otpVersionsForElixirMap = {} @@ -347,7 +346,7 @@ async function getGleamVersions() { ) const gleamVersionsListing = {} resultJSONs.forEach((resultJSON) => { - jsonParseAsList(resultJSON) + resultJSON .map((x) => x.tag_name) .forEach((ver) => { const gleamMatch = ver.match(/^v?([^ ]+)/) @@ -366,7 +365,7 @@ async function getRebar3Versions() { ) const rebar3VersionsListing = {} resultJSONs.forEach((resultJSON) => { - jsonParseAsList(resultJSON) + resultJSON .map((x) => x.tag_name) .forEach((ver) => { rebar3VersionsListing[ver] = ver @@ -560,38 +559,56 @@ function getRunnerOSVersion() { return containerFromEnvImageOS } -async function get(url0, pageIdxs) { - async function getPage(pageIdx) { - const url = new URL(url0) - const headers = {} - const GithubToken = getInput('github-token', false) - if (GithubToken && url.host === 'api.github.com') { - headers.authorization = `Bearer ${GithubToken}` - } - - if (pageIdx !== null) { - url.searchParams.append('page', pageIdx) - } - - const httpClient = new http.HttpClient('setup-beam', [], { - allowRetries: true, - maxRetries: 3, +async function getUrlResponse(url, headers, attempt = 1) { + try { + const response = await fetch(url, { + headers, + signal: AbortSignal.timeout(10000), }) - const response = await httpClient.get(url, headers) - if (response.statusCode >= 400 && response.statusCode <= 599) { - throw new Error( - `Got ${response.statusCode} from ${url}. Exiting with error`, - ) + const contentType = response.headers.get('content-type') || '' + + if (!response.ok) { + throw new Error(response.statusText) } - return response.readBody() + if (contentType.indexOf('application/json') !== -1) { + return response.json() + } else { + return response.text() + } + } catch (err) { + core.debug(`Error fetching from ${url}: ${err}`) + + if (attempt <= MAX_HTTP_RETRIES) { + const delay = attempt * 2 * 1000 + core.debug(`Error during fetch. Retrying in ${delay}ms`) + await new Promise((resolve) => setTimeout(resolve, delay)) + return getUrlResponse(url, headers, attempt + 1) + } else { + throw err + } + } +} + +async function get(url0, pageIdxs) { + const url = new URL(url0) + const headers = {} + const GithubToken = getInput('github-token', false) + if (GithubToken && url.host === 'api.github.com') { + headers.authorization = `Bearer ${GithubToken}` } - if (pageIdxs[0] === null) { - return getPage(null) + if (pageIdxs.length === 0) { + return getUrlResponse(url, headers) + } else { + return Promise.all( + pageIdxs.map((page) => { + const urlWithPage = new URL(url) + urlWithPage.searchParams.append('page', page) + return getUrlResponse(urlWithPage, headers) + }), + ) } - - return Promise.all(pageIdxs.map(getPage)) } function maybePrependWithV(v) { @@ -686,21 +703,6 @@ function parseVersionFile(versionFilePath0) { return appVersions } -function jsonParseAsList(maybeJson) { - try { - const obj = JSON.parse(maybeJson) - if (!Array.isArray(obj)) { - throw new Error('expected a list!') - } - - return obj - } catch (exc) { - throw new Error( - `Got an exception when trying to parse non-JSON list ${maybeJson}: ${exc}`, - ) - } -} - function debugLog(groupName, message) { const group = `Debugging for ${groupName}` core.debug( @@ -1058,6 +1060,7 @@ function debugLoggingEnabled() { } module.exports = { + get, getOTPVersion, getElixirVersion, getGleamVersion, diff --git a/test/setup-beam.test.js b/test/setup-beam.test.js index c9b3b7b..0f77e66 100644 --- a/test/setup-beam.test.js +++ b/test/setup-beam.test.js @@ -7,6 +7,7 @@ simulateInput('github-token', process.env.GITHUB_TOKEN) simulateInput('hexpm-mirrors', 'https://builds.hex.pm', { multiline: true }) const assert = require('assert') +const http = require('http') const fs = require('fs') const os = require('os') const path = require('path') @@ -71,8 +72,8 @@ async function all() { await testRebar3Versions() await testGetVersionFromSpec() - await testParseVersionFile() + await testGetRetry() await testElixirMixCompileError() await testElixirMixCompileWarning() @@ -870,10 +871,10 @@ async function testParseVersionFile() { const gleamVersion = unsimulateInput('gleam-version') const rebar3Version = unsimulateInput('rebar3-version') - const erlang = '25.1.1' - const elixir = '1.14.1' + const erlang = '27' + const elixir = '1.17.0' const gleam = '0.23.0' - const rebar3 = '3.16.0' + const rebar3 = '3.24.0' const toolVersions = `# a comment erlang ref:v${erlang}# comment, no space, and ref:v elixir ref:${elixir} # comment, with space and ref: @@ -913,6 +914,28 @@ gleam ${gleam} \nrebar ${rebar3}` simulateInput('rebar3-version', rebar3Version) } +async function testGetRetry() { + let attempt = 0 + const server = http.createServer((req, res) => { + attempt++ + if (attempt == 2) { + res.write('correct!') + res.end() + } + }) + + try { + server.listen(0) + const port = server.address().port + + const response = await setupBeam.get(`http://localhost:${port}`, []) + assert.equal(response, 'correct!') + assert.equal(attempt, 2) + } finally { + server.close() + } +} + async function testElixirMixCompileError() { const [matcher] = problemMatcher.find( ({ owner }) => owner === 'elixir-mixCompileError',