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

2fdf20c · Blake Kostner · 2024-09-25 11:20

7 files +129 -105

Files changed

modified .github/workflows/action.yml
+2 −1
@@ -7,7 +7,7 @@ on:
7 7 - main
8 8 pull_request:
9 9 branches:
10 - "*"
10 + - '*'
11 11 workflow_dispatch: {}
12 12
13 13 env:
@@ -62,6 +62,7 @@ jobs:
62 62 id: setup-beam
63 63 uses: ./
64 64 with:
65 + install-rebar: false
65 66 version-file: test/.tool-versions
66 67 version-type: strict
67 68
modified .github/workflows/ubuntu.yml
+2 −6
@@ -7,7 +7,7 @@ on:
7 7 - main
8 8 pull_request:
9 9 branches:
10 - "*"
10 + - '*'
11 11 workflow_dispatch: {}
12 12
13 13 jobs:
@@ -53,10 +53,6 @@ jobs:
53 53 elixir-version: '1.16'
54 54 rebar3-version: '3'
55 55 os: 'ubuntu-22.04'
56 - otp-version: '24'
57 elixir-version: '1.16'
58 rebar3-version: '3'
59 os: 'ubuntu-22.04'
60 56 - otp-version: '25.0'
61 57 elixir-version: 'v1.13.4-otp-25'
62 58 rebar3-version: '3.18.0'
@@ -121,7 +117,7 @@ jobs:
121 117 otp-version: false
122 118 os: 'ubuntu-latest'
123 119 disable_problem_matchers: true
124 - gleam-version: '0.22.0' # Version with Gleam's old archive naming convention
120 + - gleam-version: '0.22.0' # Version with Gleam's old archive naming convention
125 121 otp-version: '24'
126 122 os: 'ubuntu-latest'
127 123 disable_problem_matchers: true
modified dist/index.js
+49 −46
@@ -9106,12 +9106,13 @@ exports["default"] = _default;
9106 9106 const core = __nccwpck_require__(2186)
9107 9107 const { exec } = __nccwpck_require__(1514)
9108 9108 const tc = __nccwpck_require__(7784)
9109 const http = __nccwpck_require__(6255)
9110 9109 const path = __nccwpck_require__(1017)
9111 9110 const semver = __nccwpck_require__(1383)
9112 9111 const fs = __nccwpck_require__(7147)
9113 9112 const os = __nccwpck_require__(2037)
9114 9113
9114 +const MAX_HTTP_RETRIES = 3
9115 +
9115 9116 main().catch((err) => {
9116 9117 core.setFailed(err.message)
9117 9118 })
@@ -9361,8 +9362,7 @@ async function getOTPVersions(osVersion) {
9361 9362 hexMirrors: hexMirrorsInput(),
9362 9363 actionTitle: `fetch ${originListing}`,
9363 9364 action: async (hexMirror) => {
9364 const l = await get(`${hexMirror}${originListing}`, [null])
9365 return l
9365 + return get(`${hexMirror}${originListing}`, [])
9366 9366 },
9367 9367 })
9368 9368 } else if (process.platform === 'win32') {
@@ -9389,7 +9389,7 @@ async function getOTPVersions(osVersion) {
9389 9389 })
9390 9390 } else if (process.platform === 'win32') {
9391 9391 otpVersionsListings.forEach((otpVersionsListing) => {
9392 jsonParseAsList(otpVersionsListing)
9392 + otpVersionsListing
9393 9393 .map((x) => x.assets)
9394 9394 .flat()
9395 9395 .filter((x) => x.name.match(/^otp_win64_.*.exe$/))
@@ -9413,8 +9413,7 @@ async function getElixirVersions() {
9413 9413 hexMirrors: hexMirrorsInput(),
9414 9414 actionTitle: `fetch ${originListing}`,
9415 9415 action: async (hexMirror) => {
9416 const l = await get(`${hexMirror}${originListing}`, [null])
9417 return l
9416 + return get(`${hexMirror}${originListing}`, [])
9418 9417 },
9419 9418 })
9420 9419 const otpVersionsForElixirMap = {}
@@ -9447,7 +9446,7 @@ async function getGleamVersions() {
9447 9446 )
9448 9447 const gleamVersionsListing = {}
9449 9448 resultJSONs.forEach((resultJSON) => {
9450 jsonParseAsList(resultJSON)
9449 + resultJSON
9451 9450 .map((x) => x.tag_name)
9452 9451 .forEach((ver) => {
9453 9452 const gleamMatch = ver.match(/^v?([^ ]+)/)
@@ -9466,7 +9465,7 @@ async function getRebar3Versions() {
9466 9465 )
9467 9466 const rebar3VersionsListing = {}
9468 9467 resultJSONs.forEach((resultJSON) => {
9469 jsonParseAsList(resultJSON)
9468 + resultJSON
9470 9469 .map((x) => x.tag_name)
9471 9470 .forEach((ver) => {
9472 9471 rebar3VersionsListing[ver] = ver
@@ -9660,38 +9659,56 @@ function getRunnerOSVersion() {
9660 9659 return containerFromEnvImageOS
9661 9660 }
9662 9661
9663 async function get(url0, pageIdxs) {
9664 async function getPage(pageIdx) {
9665 const url = new URL(url0)
9666 const headers = {}
9667 const GithubToken = getInput('github-token', false)
9668 if (GithubToken && url.host === 'api.github.com') {
9669 headers.authorization = `Bearer ${GithubToken}`
9670 }
9662 +async function getUrlResponse(url, headers, attempt = 1) {
9663 + try {
9664 + const response = await fetch(url, {
9665 + headers,
9666 + signal: AbortSignal.timeout(10000),
9667 + })
9668 + const contentType = response.headers.get('content-type') || ''
9671 9669
9672 if (pageIdx !== null) {
9673 url.searchParams.append('page', pageIdx)
9670 + if (!response.ok) {
9671 + throw new Error(response.statusText)
9674 9672 }
9675 9673
9676 const httpClient = new http.HttpClient('setup-beam', [], {
9677 allowRetries: true,
9678 maxRetries: 3,
9679 })
9680 const response = await httpClient.get(url, headers)
9681 if (response.statusCode >= 400 && response.statusCode <= 599) {
9682 throw new Error(
9683 `Got ${response.statusCode} from ${url}. Exiting with error`,
9684 )
9674 + if (contentType.indexOf('application/json') !== -1) {
9675 + return response.json()
9676 + } else {
9677 + return response.text()
9685 9678 }
9679 + } catch (err) {
9680 + core.debug(`Error fetching from ${url}: ${err}`)
9686 9681
9687 return response.readBody()
9682 + if (attempt <= MAX_HTTP_RETRIES) {
9683 + const delay = attempt * 2 * 1000
9684 + core.debug(`Error during fetch. Retrying in ${delay}ms`)
9685 + await new Promise((resolve) => setTimeout(resolve, delay))
9686 + return getUrlResponse(url, headers, attempt + 1)
9687 + } else {
9688 + throw err
9689 + }
9688 9690 }
9691 +}
9689 9692
9690 if (pageIdxs[0] === null) {
9691 return getPage(null)
9693 +async function get(url0, pageIdxs) {
9694 + const url = new URL(url0)
9695 + const headers = {}
9696 + const GithubToken = getInput('github-token', false)
9697 + if (GithubToken && url.host === 'api.github.com') {
9698 + headers.authorization = `Bearer ${GithubToken}`
9692 9699 }
9693 9700
9694 return Promise.all(pageIdxs.map(getPage))
9701 + if (pageIdxs.length === 0) {
9702 + return getUrlResponse(url, headers)
9703 + } else {
9704 + return Promise.all(
9705 + pageIdxs.map((page) => {
9706 + const urlWithPage = new URL(url)
9707 + urlWithPage.searchParams.append('page', page)
9708 + return getUrlResponse(urlWithPage, headers)
9709 + }),
9710 + )
9711 + }
9695 9712 }
9696 9713
9697 9714 function maybePrependWithV(v) {
@@ -9786,21 +9803,6 @@ function parseVersionFile(versionFilePath0) {
9786 9803 return appVersions
9787 9804 }
9788 9805
9789 function jsonParseAsList(maybeJson) {
9790 try {
9791 const obj = JSON.parse(maybeJson)
9792 if (!Array.isArray(obj)) {
9793 throw new Error('expected a list!')
9794 }
9795
9796 return obj
9797 } catch (exc) {
9798 throw new Error(
9799 `Got an exception when trying to parse non-JSON list ${maybeJson}: ${exc}`,
9800 )
9801 }
9802 }
9803
9804 9806 function debugLog(groupName, message) {
9805 9807 const group = `Debugging for ${groupName}`
9806 9808 core.debug(
@@ -10158,6 +10160,7 @@ function debugLoggingEnabled() {
10158 10160 }
10159 10161
10160 10162 module.exports = {
10163 + get,
10161 10164 getOTPVersion,
10162 10165 getElixirVersion,
10163 10166 getGleamVersion,
modified package-lock.json
+0 −1
@@ -9,7 +9,6 @@
9 9 "dependencies": {
10 10 "@actions/core": "1.10.0",
11 11 "@actions/exec": "1.1.1",
12 "@actions/http-client": "2.1.0",
13 12 "@actions/tool-cache": "2.0.1",
14 13 "semver": "7.6.2"
15 14 },
modified package.json
+0 −1
@@ -18,7 +18,6 @@
18 18 "dependencies": {
19 19 "@actions/core": "1.10.0",
20 20 "@actions/exec": "1.1.1",
21 "@actions/http-client": "2.1.0",
22 21 "@actions/tool-cache": "2.0.1",
23 22 "semver": "7.6.2"
24 23 },
modified src/setup-beam.js
+49 −46
@@ -1,12 +1,13 @@
1 1 const core = require('@actions/core')
2 2 const { exec } = require('@actions/exec')
3 3 const tc = require('@actions/tool-cache')
4 const http = require('@actions/http-client')
5 4 const path = require('path')
6 5 const semver = require('semver')
7 6 const fs = require('fs')
8 7 const os = require('os')
9 8
9 +const MAX_HTTP_RETRIES = 3
10 +
10 11 main().catch((err) => {
11 12 core.setFailed(err.message)
12 13 })
@@ -261,8 +262,7 @@ async function getOTPVersions(osVersion) {
261 262 hexMirrors: hexMirrorsInput(),
262 263 actionTitle: `fetch ${originListing}`,
263 264 action: async (hexMirror) => {
264 const l = await get(`${hexMirror}${originListing}`, [null])
265 return l
265 + return get(`${hexMirror}${originListing}`, [])
266 266 },
267 267 })
268 268 } else if (process.platform === 'win32') {
@@ -289,7 +289,7 @@ async function getOTPVersions(osVersion) {
289 289 })
290 290 } else if (process.platform === 'win32') {
291 291 otpVersionsListings.forEach((otpVersionsListing) => {
292 jsonParseAsList(otpVersionsListing)
292 + otpVersionsListing
293 293 .map((x) => x.assets)
294 294 .flat()
295 295 .filter((x) => x.name.match(/^otp_win64_.*.exe$/))
@@ -313,8 +313,7 @@ async function getElixirVersions() {
313 313 hexMirrors: hexMirrorsInput(),
314 314 actionTitle: `fetch ${originListing}`,
315 315 action: async (hexMirror) => {
316 const l = await get(`${hexMirror}${originListing}`, [null])
317 return l
316 + return get(`${hexMirror}${originListing}`, [])
318 317 },
319 318 })
320 319 const otpVersionsForElixirMap = {}
@@ -347,7 +346,7 @@ async function getGleamVersions() {
347 346 )
348 347 const gleamVersionsListing = {}
349 348 resultJSONs.forEach((resultJSON) => {
350 jsonParseAsList(resultJSON)
349 + resultJSON
351 350 .map((x) => x.tag_name)
352 351 .forEach((ver) => {
353 352 const gleamMatch = ver.match(/^v?([^ ]+)/)
@@ -366,7 +365,7 @@ async function getRebar3Versions() {
366 365 )
367 366 const rebar3VersionsListing = {}
368 367 resultJSONs.forEach((resultJSON) => {
369 jsonParseAsList(resultJSON)
368 + resultJSON
370 369 .map((x) => x.tag_name)
371 370 .forEach((ver) => {
372 371 rebar3VersionsListing[ver] = ver
@@ -560,38 +559,56 @@ function getRunnerOSVersion() {
560 559 return containerFromEnvImageOS
561 560 }
562 561
563 async function get(url0, pageIdxs) {
564 async function getPage(pageIdx) {
565 const url = new URL(url0)
566 const headers = {}
567 const GithubToken = getInput('github-token', false)
568 if (GithubToken && url.host === 'api.github.com') {
569 headers.authorization = `Bearer ${GithubToken}`
570 }
562 +async function getUrlResponse(url, headers, attempt = 1) {
563 + try {
564 + const response = await fetch(url, {
565 + headers,
566 + signal: AbortSignal.timeout(10000),
567 + })
568 + const contentType = response.headers.get('content-type') || ''
571 569
572 if (pageIdx !== null) {
573 url.searchParams.append('page', pageIdx)
570 + if (!response.ok) {
571 + throw new Error(response.statusText)
574 572 }
575 573
576 const httpClient = new http.HttpClient('setup-beam', [], {
577 allowRetries: true,
578 maxRetries: 3,
579 })
580 const response = await httpClient.get(url, headers)
581 if (response.statusCode >= 400 && response.statusCode <= 599) {
582 throw new Error(
583 `Got ${response.statusCode} from ${url}. Exiting with error`,
584 )
574 + if (contentType.indexOf('application/json') !== -1) {
575 + return response.json()
576 + } else {
577 + return response.text()
585 578 }
579 + } catch (err) {
580 + core.debug(`Error fetching from ${url}: ${err}`)
586 581
587 return response.readBody()
582 + if (attempt <= MAX_HTTP_RETRIES) {
583 + const delay = attempt * 2 * 1000
584 + core.debug(`Error during fetch. Retrying in ${delay}ms`)
585 + await new Promise((resolve) => setTimeout(resolve, delay))
586 + return getUrlResponse(url, headers, attempt + 1)
587 + } else {
588 + throw err
589 + }
588 590 }
591 +}
589 592
590 if (pageIdxs[0] === null) {
591 return getPage(null)
593 +async function get(url0, pageIdxs) {
594 + const url = new URL(url0)
595 + const headers = {}
596 + const GithubToken = getInput('github-token', false)
597 + if (GithubToken && url.host === 'api.github.com') {
598 + headers.authorization = `Bearer ${GithubToken}`
592 599 }
593 600
594 return Promise.all(pageIdxs.map(getPage))
601 + if (pageIdxs.length === 0) {
602 + return getUrlResponse(url, headers)
603 + } else {
604 + return Promise.all(
605 + pageIdxs.map((page) => {
606 + const urlWithPage = new URL(url)
607 + urlWithPage.searchParams.append('page', page)
608 + return getUrlResponse(urlWithPage, headers)
609 + }),
610 + )
611 + }
595 612 }
596 613
597 614 function maybePrependWithV(v) {
@@ -686,21 +703,6 @@ function parseVersionFile(versionFilePath0) {
686 703 return appVersions
687 704 }
688 705
689 function jsonParseAsList(maybeJson) {
690 try {
691 const obj = JSON.parse(maybeJson)
692 if (!Array.isArray(obj)) {
693 throw new Error('expected a list!')
694 }
695
696 return obj
697 } catch (exc) {
698 throw new Error(
699 `Got an exception when trying to parse non-JSON list ${maybeJson}: ${exc}`,
700 )
701 }
702 }
703
704 706 function debugLog(groupName, message) {
705 707 const group = `Debugging for ${groupName}`
706 708 core.debug(
@@ -1058,6 +1060,7 @@ function debugLoggingEnabled() {
1058 1060 }
1059 1061
1060 1062 module.exports = {
1063 + get,
1061 1064 getOTPVersion,
1062 1065 getElixirVersion,
1063 1066 getGleamVersion,
modified test/setup-beam.test.js
+27 −4
@@ -7,6 +7,7 @@ simulateInput('github-token', process.env.GITHUB_TOKEN)
7 7 simulateInput('hexpm-mirrors', 'https://builds.hex.pm', { multiline: true })
8 8
9 9 const assert = require('assert')
10 +const http = require('http')
10 11 const fs = require('fs')
11 12 const os = require('os')
12 13 const path = require('path')
@@ -71,8 +72,8 @@ async function all() {
71 72 await testRebar3Versions()
72 73
73 74 await testGetVersionFromSpec()
74
75 75 await testParseVersionFile()
76 + await testGetRetry()
76 77
77 78 await testElixirMixCompileError()
78 79 await testElixirMixCompileWarning()
@@ -870,10 +871,10 @@ async function testParseVersionFile() {
870 871 const gleamVersion = unsimulateInput('gleam-version')
871 872 const rebar3Version = unsimulateInput('rebar3-version')
872 873
873 const erlang = '25.1.1'
874 const elixir = '1.14.1'
874 + const erlang = '27'
875 + const elixir = '1.17.0'
875 876 const gleam = '0.23.0'
876 const rebar3 = '3.16.0'
877 + const rebar3 = '3.24.0'
877 878 const toolVersions = `# a comment
878 879 erlang ref:v${erlang}# comment, no space, and ref:v
879 880 elixir ref:${elixir} # comment, with space and ref:
@@ -913,6 +914,28 @@ gleam ${gleam} \nrebar ${rebar3}`
913 914 simulateInput('rebar3-version', rebar3Version)
914 915 }
915 916
917 +async function testGetRetry() {
918 + let attempt = 0
919 + const server = http.createServer((req, res) => {
920 + attempt++
921 + if (attempt == 2) {
922 + res.write('correct!')
923 + res.end()
924 + }
925 + })
926 +
927 + try {
928 + server.listen(0)
929 + const port = server.address().port
930 +
931 + const response = await setupBeam.get(`http://localhost:${port}`, [])
932 + assert.equal(response, 'correct!')
933 + assert.equal(attempt, 2)
934 + } finally {
935 + server.close()
936 + }
937 +}
938 +
916 939 async function testElixirMixCompileError() {
917 940 const [matcher] = problemMatcher.find(
918 941 ({ owner }) => owner === 'elixir-mixCompileError',

Parents: d1c0257