feat: setup custom retry with builtin node fetch (#304)

This commit is contained in:
Blake Kostner
2024-09-25 04:20:41 -07:00
committed by GitHub
parent d1c02572fa
commit 2fdf20c985
7 changed files with 135 additions and 111 deletions
+2 -1
View File
@@ -7,7 +7,7 @@ on:
- main - main
pull_request: pull_request:
branches: branches:
- "*" - '*'
workflow_dispatch: {} workflow_dispatch: {}
env: env:
@@ -62,6 +62,7 @@ jobs:
id: setup-beam id: setup-beam
uses: ./ uses: ./
with: with:
install-rebar: false
version-file: test/.tool-versions version-file: test/.tool-versions
version-type: strict version-type: strict
+1 -5
View File
@@ -7,7 +7,7 @@ on:
- main - main
pull_request: pull_request:
branches: branches:
- "*" - '*'
workflow_dispatch: {} workflow_dispatch: {}
jobs: jobs:
@@ -53,10 +53,6 @@ jobs:
elixir-version: '1.16' elixir-version: '1.16'
rebar3-version: '3' rebar3-version: '3'
os: 'ubuntu-22.04' os: 'ubuntu-22.04'
- otp-version: '24'
elixir-version: '1.16'
rebar3-version: '3'
os: 'ubuntu-22.04'
- otp-version: '25.0' - otp-version: '25.0'
elixir-version: 'v1.13.4-otp-25' elixir-version: 'v1.13.4-otp-25'
rebar3-version: '3.18.0' rebar3-version: '3.18.0'
+48 -45
View File
@@ -9106,12 +9106,13 @@ exports["default"] = _default;
const core = __nccwpck_require__(2186) const core = __nccwpck_require__(2186)
const { exec } = __nccwpck_require__(1514) const { exec } = __nccwpck_require__(1514)
const tc = __nccwpck_require__(7784) const tc = __nccwpck_require__(7784)
const http = __nccwpck_require__(6255)
const path = __nccwpck_require__(1017) const path = __nccwpck_require__(1017)
const semver = __nccwpck_require__(1383) const semver = __nccwpck_require__(1383)
const fs = __nccwpck_require__(7147) const fs = __nccwpck_require__(7147)
const os = __nccwpck_require__(2037) const os = __nccwpck_require__(2037)
const MAX_HTTP_RETRIES = 3
main().catch((err) => { main().catch((err) => {
core.setFailed(err.message) core.setFailed(err.message)
}) })
@@ -9361,8 +9362,7 @@ async function getOTPVersions(osVersion) {
hexMirrors: hexMirrorsInput(), hexMirrors: hexMirrorsInput(),
actionTitle: `fetch ${originListing}`, actionTitle: `fetch ${originListing}`,
action: async (hexMirror) => { action: async (hexMirror) => {
const l = await get(`${hexMirror}${originListing}`, [null]) return get(`${hexMirror}${originListing}`, [])
return l
}, },
}) })
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
@@ -9389,7 +9389,7 @@ async function getOTPVersions(osVersion) {
}) })
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
otpVersionsListings.forEach((otpVersionsListing) => { otpVersionsListings.forEach((otpVersionsListing) => {
jsonParseAsList(otpVersionsListing) otpVersionsListing
.map((x) => x.assets) .map((x) => x.assets)
.flat() .flat()
.filter((x) => x.name.match(/^otp_win64_.*.exe$/)) .filter((x) => x.name.match(/^otp_win64_.*.exe$/))
@@ -9413,8 +9413,7 @@ async function getElixirVersions() {
hexMirrors: hexMirrorsInput(), hexMirrors: hexMirrorsInput(),
actionTitle: `fetch ${originListing}`, actionTitle: `fetch ${originListing}`,
action: async (hexMirror) => { action: async (hexMirror) => {
const l = await get(`${hexMirror}${originListing}`, [null]) return get(`${hexMirror}${originListing}`, [])
return l
}, },
}) })
const otpVersionsForElixirMap = {} const otpVersionsForElixirMap = {}
@@ -9447,7 +9446,7 @@ async function getGleamVersions() {
) )
const gleamVersionsListing = {} const gleamVersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) resultJSON
.map((x) => x.tag_name) .map((x) => x.tag_name)
.forEach((ver) => { .forEach((ver) => {
const gleamMatch = ver.match(/^v?([^ ]+)/) const gleamMatch = ver.match(/^v?([^ ]+)/)
@@ -9466,7 +9465,7 @@ async function getRebar3Versions() {
) )
const rebar3VersionsListing = {} const rebar3VersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) resultJSON
.map((x) => x.tag_name) .map((x) => x.tag_name)
.forEach((ver) => { .forEach((ver) => {
rebar3VersionsListing[ver] = ver rebar3VersionsListing[ver] = ver
@@ -9660,8 +9659,38 @@ function getRunnerOSVersion() {
return containerFromEnvImageOS return containerFromEnvImageOS
} }
async function getUrlResponse(url, headers, attempt = 1) {
try {
const response = await fetch(url, {
headers,
signal: AbortSignal.timeout(10000),
})
const contentType = response.headers.get('content-type') || ''
if (!response.ok) {
throw new Error(response.statusText)
}
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) { async function get(url0, pageIdxs) {
async function getPage(pageIdx) {
const url = new URL(url0) const url = new URL(url0)
const headers = {} const headers = {}
const GithubToken = getInput('github-token', false) const GithubToken = getInput('github-token', false)
@@ -9669,29 +9698,17 @@ async function get(url0, pageIdxs) {
headers.authorization = `Bearer ${GithubToken}` headers.authorization = `Bearer ${GithubToken}`
} }
if (pageIdx !== null) { if (pageIdxs.length === 0) {
url.searchParams.append('page', pageIdx) return getUrlResponse(url, headers)
} } else {
return Promise.all(
const httpClient = new http.HttpClient('setup-beam', [], { pageIdxs.map((page) => {
allowRetries: true, const urlWithPage = new URL(url)
maxRetries: 3, urlWithPage.searchParams.append('page', page)
}) return getUrlResponse(urlWithPage, headers)
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`,
) )
} }
return response.readBody()
}
if (pageIdxs[0] === null) {
return getPage(null)
}
return Promise.all(pageIdxs.map(getPage))
} }
function maybePrependWithV(v) { function maybePrependWithV(v) {
@@ -9786,21 +9803,6 @@ function parseVersionFile(versionFilePath0) {
return appVersions 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) { function debugLog(groupName, message) {
const group = `Debugging for ${groupName}` const group = `Debugging for ${groupName}`
core.debug( core.debug(
@@ -10158,6 +10160,7 @@ function debugLoggingEnabled() {
} }
module.exports = { module.exports = {
get,
getOTPVersion, getOTPVersion,
getElixirVersion, getElixirVersion,
getGleamVersion, getGleamVersion,
-1
View File
@@ -9,7 +9,6 @@
"dependencies": { "dependencies": {
"@actions/core": "1.10.0", "@actions/core": "1.10.0",
"@actions/exec": "1.1.1", "@actions/exec": "1.1.1",
"@actions/http-client": "2.1.0",
"@actions/tool-cache": "2.0.1", "@actions/tool-cache": "2.0.1",
"semver": "7.6.2" "semver": "7.6.2"
}, },
-1
View File
@@ -18,7 +18,6 @@
"dependencies": { "dependencies": {
"@actions/core": "1.10.0", "@actions/core": "1.10.0",
"@actions/exec": "1.1.1", "@actions/exec": "1.1.1",
"@actions/http-client": "2.1.0",
"@actions/tool-cache": "2.0.1", "@actions/tool-cache": "2.0.1",
"semver": "7.6.2" "semver": "7.6.2"
}, },
+48 -45
View File
@@ -1,12 +1,13 @@
const core = require('@actions/core') const core = require('@actions/core')
const { exec } = require('@actions/exec') const { exec } = require('@actions/exec')
const tc = require('@actions/tool-cache') const tc = require('@actions/tool-cache')
const http = require('@actions/http-client')
const path = require('path') const path = require('path')
const semver = require('semver') const semver = require('semver')
const fs = require('fs') const fs = require('fs')
const os = require('os') const os = require('os')
const MAX_HTTP_RETRIES = 3
main().catch((err) => { main().catch((err) => {
core.setFailed(err.message) core.setFailed(err.message)
}) })
@@ -261,8 +262,7 @@ async function getOTPVersions(osVersion) {
hexMirrors: hexMirrorsInput(), hexMirrors: hexMirrorsInput(),
actionTitle: `fetch ${originListing}`, actionTitle: `fetch ${originListing}`,
action: async (hexMirror) => { action: async (hexMirror) => {
const l = await get(`${hexMirror}${originListing}`, [null]) return get(`${hexMirror}${originListing}`, [])
return l
}, },
}) })
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
@@ -289,7 +289,7 @@ async function getOTPVersions(osVersion) {
}) })
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
otpVersionsListings.forEach((otpVersionsListing) => { otpVersionsListings.forEach((otpVersionsListing) => {
jsonParseAsList(otpVersionsListing) otpVersionsListing
.map((x) => x.assets) .map((x) => x.assets)
.flat() .flat()
.filter((x) => x.name.match(/^otp_win64_.*.exe$/)) .filter((x) => x.name.match(/^otp_win64_.*.exe$/))
@@ -313,8 +313,7 @@ async function getElixirVersions() {
hexMirrors: hexMirrorsInput(), hexMirrors: hexMirrorsInput(),
actionTitle: `fetch ${originListing}`, actionTitle: `fetch ${originListing}`,
action: async (hexMirror) => { action: async (hexMirror) => {
const l = await get(`${hexMirror}${originListing}`, [null]) return get(`${hexMirror}${originListing}`, [])
return l
}, },
}) })
const otpVersionsForElixirMap = {} const otpVersionsForElixirMap = {}
@@ -347,7 +346,7 @@ async function getGleamVersions() {
) )
const gleamVersionsListing = {} const gleamVersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) resultJSON
.map((x) => x.tag_name) .map((x) => x.tag_name)
.forEach((ver) => { .forEach((ver) => {
const gleamMatch = ver.match(/^v?([^ ]+)/) const gleamMatch = ver.match(/^v?([^ ]+)/)
@@ -366,7 +365,7 @@ async function getRebar3Versions() {
) )
const rebar3VersionsListing = {} const rebar3VersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) resultJSON
.map((x) => x.tag_name) .map((x) => x.tag_name)
.forEach((ver) => { .forEach((ver) => {
rebar3VersionsListing[ver] = ver rebar3VersionsListing[ver] = ver
@@ -560,8 +559,38 @@ function getRunnerOSVersion() {
return containerFromEnvImageOS return containerFromEnvImageOS
} }
async function getUrlResponse(url, headers, attempt = 1) {
try {
const response = await fetch(url, {
headers,
signal: AbortSignal.timeout(10000),
})
const contentType = response.headers.get('content-type') || ''
if (!response.ok) {
throw new Error(response.statusText)
}
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) { async function get(url0, pageIdxs) {
async function getPage(pageIdx) {
const url = new URL(url0) const url = new URL(url0)
const headers = {} const headers = {}
const GithubToken = getInput('github-token', false) const GithubToken = getInput('github-token', false)
@@ -569,29 +598,17 @@ async function get(url0, pageIdxs) {
headers.authorization = `Bearer ${GithubToken}` headers.authorization = `Bearer ${GithubToken}`
} }
if (pageIdx !== null) { if (pageIdxs.length === 0) {
url.searchParams.append('page', pageIdx) return getUrlResponse(url, headers)
} } else {
return Promise.all(
const httpClient = new http.HttpClient('setup-beam', [], { pageIdxs.map((page) => {
allowRetries: true, const urlWithPage = new URL(url)
maxRetries: 3, urlWithPage.searchParams.append('page', page)
}) return getUrlResponse(urlWithPage, headers)
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`,
) )
} }
return response.readBody()
}
if (pageIdxs[0] === null) {
return getPage(null)
}
return Promise.all(pageIdxs.map(getPage))
} }
function maybePrependWithV(v) { function maybePrependWithV(v) {
@@ -686,21 +703,6 @@ function parseVersionFile(versionFilePath0) {
return appVersions 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) { function debugLog(groupName, message) {
const group = `Debugging for ${groupName}` const group = `Debugging for ${groupName}`
core.debug( core.debug(
@@ -1058,6 +1060,7 @@ function debugLoggingEnabled() {
} }
module.exports = { module.exports = {
get,
getOTPVersion, getOTPVersion,
getElixirVersion, getElixirVersion,
getGleamVersion, getGleamVersion,
+27 -4
View File
@@ -7,6 +7,7 @@ simulateInput('github-token', process.env.GITHUB_TOKEN)
simulateInput('hexpm-mirrors', 'https://builds.hex.pm', { multiline: true }) simulateInput('hexpm-mirrors', 'https://builds.hex.pm', { multiline: true })
const assert = require('assert') const assert = require('assert')
const http = require('http')
const fs = require('fs') const fs = require('fs')
const os = require('os') const os = require('os')
const path = require('path') const path = require('path')
@@ -71,8 +72,8 @@ async function all() {
await testRebar3Versions() await testRebar3Versions()
await testGetVersionFromSpec() await testGetVersionFromSpec()
await testParseVersionFile() await testParseVersionFile()
await testGetRetry()
await testElixirMixCompileError() await testElixirMixCompileError()
await testElixirMixCompileWarning() await testElixirMixCompileWarning()
@@ -870,10 +871,10 @@ async function testParseVersionFile() {
const gleamVersion = unsimulateInput('gleam-version') const gleamVersion = unsimulateInput('gleam-version')
const rebar3Version = unsimulateInput('rebar3-version') const rebar3Version = unsimulateInput('rebar3-version')
const erlang = '25.1.1' const erlang = '27'
const elixir = '1.14.1' const elixir = '1.17.0'
const gleam = '0.23.0' const gleam = '0.23.0'
const rebar3 = '3.16.0' const rebar3 = '3.24.0'
const toolVersions = `# a comment const toolVersions = `# a comment
erlang ref:v${erlang}# comment, no space, and ref:v erlang ref:v${erlang}# comment, no space, and ref:v
elixir ref:${elixir} # comment, with space and ref: elixir ref:${elixir} # comment, with space and ref:
@@ -913,6 +914,28 @@ gleam ${gleam} \nrebar ${rebar3}`
simulateInput('rebar3-version', rebar3Version) 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() { async function testElixirMixCompileError() {
const [matcher] = problemMatcher.find( const [matcher] = problemMatcher.find(
({ owner }) => owner === 'elixir-mixCompileError', ({ owner }) => owner === 'elixir-mixCompileError',