diff --git a/.github/workflows/hexpm-mirrors.yml b/.github/workflows/hexpm-mirrors.yml new file mode 100644 index 0000000..b87d1da --- /dev/null +++ b/.github/workflows/hexpm-mirrors.yml @@ -0,0 +1,58 @@ +--- +name: hexpm-mirrors + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test-failing-first-mirror: + name: Test option hexpm-mirrors with a dummy first mirror + runs-on: ${{matrix.os}} + strategy: + fail-fast: false + matrix: + os: ['ubuntu-22.04', 'windows-2022'] + otp-version: ['24'] + elixir-version: ['v1.14', ''] + install-rebar: [true, false] + install-hex: [true, false] + steps: + - uses: actions/checkout@v3 + - name: Use erlef/setup-beam + id: setup-beam + uses: ./ + with: + otp-version: ${{matrix.otp-version}} + elixir-version: ${{matrix.elixir-version}} + install-rebar: ${{matrix.install-rebar}} + install-hex: ${{matrix.install-hex}} + hexpm-mirrors: | + https://mirror.invalid + https://cdn.jsdelivr.net/hex + https://builds.hex.pm + + test-invalid-mirror: + name: Test errors with mirror + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Use erlef/setup-beam + id: failing-setup-beam + uses: ./ + continue-on-error: true + with: + otp-version: '24.3.4.6' + elixir-version: 'v1.13.4' + install-rebar: true + install-hex: true + hexpm-mirrors: https://mirror.invalid + - name: Report unexpected success + if: ${{ steps.failing-setup-beam.outcome == 'success' }} + run: | + echo "Action is expected to fail" + exit 1 diff --git a/README.md b/README.md index f682e3a..5885d7c 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,47 @@ jobs: **Note**: the `otp-version: false` input is only applicable when installing Gleam. +## Alternative hex.pm mirrors + +It is possible to use alternative hex.pm mirror(s), in their declared order, with +option `hexpm-mirrors`. By default, the action will use `builds.hex.pm`. +To use other alternative mirrors, add one per line, as shown below. + +```yaml +# create this in .github/workflows/ci.yml +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + # Use `cdn.jsdelivr.net/hex` as an alternative to `builds.hex.pm` + hexpm-mirrors: https://cdn.jsdelivr.net/hex +``` + +Alternatively, you may try `cdn.jsdelivr.net/hex` if `builds.hex.pm` fails: + +```yaml +# create this in .github/workflows/ci.yml +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: erlef/setup-beam@v1 + with: + otp-version: '25' + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex +``` + ## Environment variables Base installation folders (useful for e.g. fetching headers for NIFs) are available in the following diff --git a/__tests__/setup-beam.test.js b/__tests__/setup-beam.test.js index 7d9c613..2496823 100644 --- a/__tests__/setup-beam.test.js +++ b/__tests__/setup-beam.test.js @@ -102,48 +102,49 @@ async function testOTPVersions() { let expected let spec let osVersion + const hexMirrors = ['https://repo.hex.pm', 'https://cdn.jsdelivr.net/hex'] if (process.platform === 'linux') { spec = '19.3.x' osVersion = 'ubuntu-16.04' expected = 'OTP-19.3.6.13' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '^19.3.6' osVersion = 'ubuntu-16.04' expected = 'OTP-19.3.6.13' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '^19.3' osVersion = 'ubuntu-18.04' expected = 'OTP-19.3.6.13' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '20' osVersion = 'ubuntu-20.04' expected = 'OTP-20.3.8.26' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '20.x' osVersion = 'ubuntu-20.04' expected = 'OTP-20.3.8.26' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '20.0' osVersion = 'ubuntu-20.04' expected = 'OTP-20.0.5' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '20.0.x' osVersion = 'ubuntu-20.04' expected = 'OTP-20.0.5' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) } @@ -151,19 +152,19 @@ async function testOTPVersions() { spec = '24.0.1' osVersion = 'windows-latest' expected = '24.0.1' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '23.2.x' osVersion = 'windows-2016' expected = '23.2.7' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '23.0' osVersion = 'windows-2019' expected = '23.0.4' - got = await setupBeam.getOTPVersion(spec, osVersion) + got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) assert.deepStrictEqual(got, expected) } } @@ -173,30 +174,31 @@ async function testElixirVersions() { let expected let spec let otpVersion + const hexMirrors = ['https://repo.hex.pm'] spec = '1.1.x' otpVersion = 'OTP-17' expected = 'v1.1.1-otp-17' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '1.10.4' otpVersion = 'OTP-23' expected = 'v1.10.4-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) spec = '1.12.1' otpVersion = 'OTP-24.0.2' expected = 'v1.12.1-otp-24' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) simulateInput('version-type', 'strict') spec = '1.14.0' otpVersion = 'master' expected = 'v1.14.0' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) simulateInput('version-type', 'loose') @@ -204,7 +206,7 @@ async function testElixirVersions() { spec = 'v1.11.0-rc.0' otpVersion = 'OTP-23' expected = 'v1.11.0-rc.0-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) simulateInput('version-type', 'loose') @@ -212,7 +214,7 @@ async function testElixirVersions() { spec = 'v1.11.0' otpVersion = '22.3.4.2' expected = 'v1.11.0-otp-22' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) simulateInput('version-type', 'loose') @@ -220,7 +222,7 @@ async function testElixirVersions() { spec = 'main' otpVersion = '23.1' expected = 'main-otp-23' - got = await setupBeam.getElixirVersion(spec, otpVersion) + got = await setupBeam.getElixirVersion(spec, otpVersion, hexMirrors) assert.deepStrictEqual(got, expected) simulateInput('version-type', 'loose') } diff --git a/action.yml b/action.yml index ceb813a..d44d7ad 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,10 @@ inputs: version-file: description: a versions file (e.g. as used by `asdf`), which defines inputs default: '' + hexpm-mirrors: + description: mirror(s) for hex.pm, one per line + default: | + https://builds.hex.pm outputs: elixir-version: description: Exact version of Elixir that was installed diff --git a/dist/index.js b/dist/index.js index 8f3823e..8fd3f68 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7120,6 +7120,7 @@ try { /***/ 2127: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { +const core = __nccwpck_require__(2186) const { exec } = __nccwpck_require__(1514) const path = __nccwpck_require__(1017) @@ -7128,11 +7129,28 @@ const path = __nccwpck_require__(1017) * * @param {string} osVersion * @param {string} otpVersion + * @param {string[]} hexMirrors */ -async function installOTP(osVersion, otpVersion) { +async function installOTP(osVersion, otpVersion, hexMirrors) { const OS = process.platform if (OS === 'linux') { - await exec(__nccwpck_require__.ab + "install-otp.sh", [osVersion, otpVersion]) + if (hexMirrors.length === 0) { + throw new Error( + `Could not install Erlang/OTP ${otpVersion} from any hex.pm mirror`, + ) + } + const [hexMirror, ...hexMirrorsT] = hexMirrors + try { + await exec(__nccwpck_require__.ab + "install-otp.sh", [ + osVersion, + otpVersion, + hexMirror, + ]) + return + } catch (err) { + core.info(`install-otp.sh failed for mirror ${hexMirror}`) + } + await installOTP(osVersion, otpVersion, hexMirrorsT) } else if (OS === 'win32') { const script = __nccwpck_require__.ab + "install-otp.ps1" await exec(`pwsh.exe ${script} -VSN:${otpVersion}`) @@ -7143,15 +7161,34 @@ async function installOTP(osVersion, otpVersion) { * Install Elixir. * * @param {string} elixirVersion + * @param {string[]} hexMirrors */ -async function installElixir(elixirVersion) { - const OS = process.platform - if (OS === 'linux') { - await exec(__nccwpck_require__.ab + "install-elixir.sh", [elixirVersion]) - } else if (OS === 'win32') { - const script = __nccwpck_require__.ab + "install-elixir.ps1" - await exec(`pwsh.exe ${script} -VSN:${elixirVersion}`) +async function installElixir(elixirVersion, hexMirrors) { + if (hexMirrors.length === 0) { + throw new Error( + `Could not install Elixir ${elixirVersion} from any hex.pm mirror`, + ) } + const [hexMirror, ...hexMirrorsT] = hexMirrors + const OS = process.platform + let script + try { + if (OS === 'linux') { + script = __nccwpck_require__.ab + "install-elixir.sh" + await exec(__nccwpck_require__.ab + "install-elixir.sh", [elixirVersion, hexMirror]) + return + } + if (OS === 'win32') { + script = __nccwpck_require__.ab + "install-elixir.ps1" + await exec( + `pwsh.exe ${script} -VSN:${elixirVersion} -HEX_MIRROR:${hexMirror}`, + ) + return + } + } catch (err) { + core.info(`${script} failed for mirror ${hexMirror}`) + } + await installElixir(elixirVersion, hexMirrorsT) } /** @@ -7238,16 +7275,23 @@ async function main() { const elixirSpec = getInput('elixir-version', false, 'elixir', versions) const gleamSpec = getInput('gleam-version', false, 'gleam', versions) const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions) + const hexMirrors = core.getMultilineInput('hexpm-mirrors', { + required: false, + }) if (otpSpec !== 'false') { - await installOTP(otpSpec, osVersion) - const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec) + await installOTP(otpSpec, osVersion, hexMirrors) + const elixirInstalled = await maybeInstallElixir( + elixirSpec, + otpSpec, + hexMirrors, + ) if (elixirInstalled === true) { const shouldMixRebar = getInput('install-rebar', false) - await mix(shouldMixRebar, 'rebar') + await mix(shouldMixRebar, 'rebar', hexMirrors) const shouldMixHex = getInput('install-hex', false) - await mix(shouldMixHex, 'hex') + await mix(shouldMixHex, 'hex', hexMirrors) } } else if (!gleamSpec) { throw new Error('otp-version=false is only available when installing Gleam') @@ -7257,12 +7301,12 @@ async function main() { await maybeInstallRebar3(rebar3Spec) } -async function installOTP(otpSpec, osVersion) { - const otpVersion = await getOTPVersion(otpSpec, osVersion) +async function installOTP(otpSpec, osVersion, hexMirrors) { + const otpVersion = await getOTPVersion(otpSpec, osVersion, hexMirrors) console.log( `##[group]Installing Erlang/OTP ${otpVersion} - built on ${osVersion}`, ) - await installer.installOTP(osVersion, otpVersion) + await installer.installOTP(osVersion, otpVersion, hexMirrors) core.setOutput('otp-version', otpVersion) core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/otp/bin`) console.log('##[endgroup]') @@ -7270,13 +7314,17 @@ async function installOTP(otpSpec, osVersion) { return otpVersion } -async function maybeInstallElixir(elixirSpec, otpVersion) { +async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { let installed = false if (elixirSpec) { - const elixirVersion = await getElixirVersion(elixirSpec, otpVersion) + const elixirVersion = await getElixirVersion( + elixirSpec, + otpSpec, + hexMirrors, + ) console.log(`##[group]Installing Elixir ${elixirVersion}`) - await installer.installElixir(elixirVersion) + await installer.installElixir(elixirVersion, hexMirrors) core.setOutput('elixir-version', elixirVersion) const disableProblemMatchers = getInput('disable_problem_matchers', false) if (disableProblemMatchers === 'false') { @@ -7295,12 +7343,28 @@ async function maybeInstallElixir(elixirSpec, otpVersion) { return installed } -async function mix(shouldMix, what) { +async function mixWithMirrors(cmd, args, hexMirrors) { + if (hexMirrors.length === 0) { + throw new Error('mix failed with every mirror') + } + const [hexMirror, ...hexMirrorsT] = hexMirrors + process.env.HEX_MIRROR = hexMirror + try { + return await exec(cmd, args) + } catch (err) { + core.info( + `mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`, + ) + } + return mixWithMirrors(cmd, args, hexMirrorsT) +} + +async function mix(shouldMix, what, hexMirrors) { if (shouldMix === 'true') { const cmd = 'mix' const args = [`local.${what}`, '--force'] console.log(`##[group]Running ${cmd} ${args}`) - await exec(cmd, args) + await mixWithMirrors(cmd, args, hexMirrors) console.log('##[endgroup]') } } @@ -7344,8 +7408,8 @@ async function maybeInstallRebar3(rebar3Spec) { return installed } -async function getOTPVersion(otpSpec0, osVersion) { - const otpVersions = await getOTPVersions(osVersion) +async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { + const otpVersions = await getOTPVersions(osVersion, hexMirrors) let otpSpec = otpSpec0 // might be a branch (?) const otpVersion = getVersionFromSpec( otpSpec, @@ -7364,10 +7428,10 @@ async function getOTPVersion(otpSpec0, osVersion) { return otpVersions.get(otpVersion) // from the reference, for download } -async function getElixirVersion(exSpec0, otpVersion0) { +async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) { const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2] const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1] - const elixirVersions = await getElixirVersions() + const elixirVersions = await getElixirVersions(hexMirrors) const semverVersions = Array.from(elixirVersions.keys()).sort() const exSpec = exSpec0.replace(/-otp-.*$/, '') const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true) @@ -7427,19 +7491,19 @@ async function getRebar3Version(r3Spec) { return rebar3Version } -async function getOTPVersions(osVersion) { - let originListing - let pageIdxs +async function getOTPVersions(osVersion, hexMirrors) { + let otpVersionsListings if (process.platform === 'linux') { - originListing = `https://builds.hex.pm/builds/otp/${osVersion}/builds.txt` - pageIdxs = [null] + otpVersionsListings = await getWithMirrors( + `/builds/otp/${osVersion}/builds.txt`, + hexMirrors, + ) } else if (process.platform === 'win32') { - originListing = + const originListing = 'https://api.github.com/repos/erlang/otp/releases?per_page=100' - pageIdxs = [1, 2, 3] + otpVersionsListings = await get(originListing, [1, 2, 3]) } - const otpVersionsListings = await get(originListing, pageIdxs) const otpVersions = new Map() if (process.platform === 'linux') { @@ -7469,10 +7533,10 @@ async function getOTPVersions(osVersion) { return otpVersions } -async function getElixirVersions() { - const elixirVersionsListings = await get( - 'https://builds.hex.pm/builds/elixir/builds.txt', - [null], +async function getElixirVersions(hexMirrors) { + const elixirVersionsListings = await getWithMirrors( + '/builds/elixir/builds.txt', + hexMirrors, ) const otpVersionsForElixirMap = new Map() @@ -7656,6 +7720,19 @@ async function get(url0, pageIdxs) { return Promise.all(pageIdxs.map(getPage)) } +async function getWithMirrors(resourcePath, hexMirrors) { + if (hexMirrors.length === 0) { + throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`) + } + const [hexMirror, ...hexMirrorsT] = hexMirrors + try { + return await get(`${hexMirror}${resourcePath}`, [null]) + } catch (err) { + core.info(`get failed for URL ${hexMirror}${resourcePath}`) + } + return getWithMirrors(resourcePath, hexMirrorsT) +} + function maybePrependWithV(v) { if (isVersion(v)) { return `v${v.replace('v', '')}` diff --git a/dist/install-elixir.ps1 b/dist/install-elixir.ps1 index fdd6605..8a4ded2 100644 --- a/dist/install-elixir.ps1 +++ b/dist/install-elixir.ps1 @@ -1,4 +1,4 @@ -param([Parameter(Mandatory=$true)][string]${VSN}) +param([Parameter(Mandatory=$true)][string]${VSN}, [Parameter(Mandatory=$true)][string]${HEX_MIRROR}) $ErrorActionPreference="Stop" @@ -9,7 +9,7 @@ $FILE_OUTPUT="elixir.zip" $DIR_FOR_BIN=".setup-beam/elixir" $ProgressPreference="SilentlyContinue" -Invoke-WebRequest "https://builds.hex.pm/builds/elixir/${FILE_INPUT}" -OutFile "${FILE_OUTPUT}" +Invoke-WebRequest "${HEX_MIRROR}/builds/elixir/${FILE_INPUT}" -OutFile "${FILE_OUTPUT}" $ProgressPreference="Continue" New-Item "${DIR_FOR_BIN}" -ItemType Directory | Out-Null $ProgressPreference="SilentlyContinue" diff --git a/dist/install-elixir.sh b/dist/install-elixir.sh index 547c933..571f186 100755 --- a/dist/install-elixir.sh +++ b/dist/install-elixir.sh @@ -5,11 +5,12 @@ set -eo pipefail cd "${RUNNER_TEMP}" VSN=${1} +HEX_MIRROR=${2} FILE_INPUT="${VSN}.zip" FILE_OUTPUT=elixir.zip DIR_FOR_BIN=.setup-beam/elixir -wget -q -O "${FILE_OUTPUT}" "https://builds.hex.pm/builds/elixir/${FILE_INPUT}" +wget -q -O "${FILE_OUTPUT}" "${HEX_MIRROR}/builds/elixir/${FILE_INPUT}" mkdir -p "${DIR_FOR_BIN}" unzip -q -o -d "${DIR_FOR_BIN}" "${FILE_OUTPUT}" echo "Installed Elixir version follows" diff --git a/dist/install-otp.sh b/dist/install-otp.sh index 931289b..016d4ac 100755 --- a/dist/install-otp.sh +++ b/dist/install-otp.sh @@ -6,11 +6,12 @@ cd "${RUNNER_TEMP}" OS=${1} VSN=${2} +HEX_MIRROR=${3} FILE_INPUT="${VSN}.tar.gz" FILE_OUTPUT=otp.tar.gz DIR_FOR_BIN=.setup-beam/otp -wget -q -O "${FILE_OUTPUT}" "https://builds.hex.pm/builds/otp/${OS}/${FILE_INPUT}" +wget -q -O "${FILE_OUTPUT}" "${HEX_MIRROR}/builds/otp/${OS}/${FILE_INPUT}" mkdir -p "${DIR_FOR_BIN}" tar zxf "${FILE_OUTPUT}" -C "${DIR_FOR_BIN}" --strip-components=1 "${DIR_FOR_BIN}/Install" -minimal "$(pwd)/${DIR_FOR_BIN}" diff --git a/src/install-elixir.ps1 b/src/install-elixir.ps1 index fdd6605..8a4ded2 100644 --- a/src/install-elixir.ps1 +++ b/src/install-elixir.ps1 @@ -1,4 +1,4 @@ -param([Parameter(Mandatory=$true)][string]${VSN}) +param([Parameter(Mandatory=$true)][string]${VSN}, [Parameter(Mandatory=$true)][string]${HEX_MIRROR}) $ErrorActionPreference="Stop" @@ -9,7 +9,7 @@ $FILE_OUTPUT="elixir.zip" $DIR_FOR_BIN=".setup-beam/elixir" $ProgressPreference="SilentlyContinue" -Invoke-WebRequest "https://builds.hex.pm/builds/elixir/${FILE_INPUT}" -OutFile "${FILE_OUTPUT}" +Invoke-WebRequest "${HEX_MIRROR}/builds/elixir/${FILE_INPUT}" -OutFile "${FILE_OUTPUT}" $ProgressPreference="Continue" New-Item "${DIR_FOR_BIN}" -ItemType Directory | Out-Null $ProgressPreference="SilentlyContinue" diff --git a/src/install-elixir.sh b/src/install-elixir.sh index 547c933..571f186 100755 --- a/src/install-elixir.sh +++ b/src/install-elixir.sh @@ -5,11 +5,12 @@ set -eo pipefail cd "${RUNNER_TEMP}" VSN=${1} +HEX_MIRROR=${2} FILE_INPUT="${VSN}.zip" FILE_OUTPUT=elixir.zip DIR_FOR_BIN=.setup-beam/elixir -wget -q -O "${FILE_OUTPUT}" "https://builds.hex.pm/builds/elixir/${FILE_INPUT}" +wget -q -O "${FILE_OUTPUT}" "${HEX_MIRROR}/builds/elixir/${FILE_INPUT}" mkdir -p "${DIR_FOR_BIN}" unzip -q -o -d "${DIR_FOR_BIN}" "${FILE_OUTPUT}" echo "Installed Elixir version follows" diff --git a/src/install-otp.sh b/src/install-otp.sh index 931289b..016d4ac 100755 --- a/src/install-otp.sh +++ b/src/install-otp.sh @@ -6,11 +6,12 @@ cd "${RUNNER_TEMP}" OS=${1} VSN=${2} +HEX_MIRROR=${3} FILE_INPUT="${VSN}.tar.gz" FILE_OUTPUT=otp.tar.gz DIR_FOR_BIN=.setup-beam/otp -wget -q -O "${FILE_OUTPUT}" "https://builds.hex.pm/builds/otp/${OS}/${FILE_INPUT}" +wget -q -O "${FILE_OUTPUT}" "${HEX_MIRROR}/builds/otp/${OS}/${FILE_INPUT}" mkdir -p "${DIR_FOR_BIN}" tar zxf "${FILE_OUTPUT}" -C "${DIR_FOR_BIN}" --strip-components=1 "${DIR_FOR_BIN}/Install" -minimal "$(pwd)/${DIR_FOR_BIN}" diff --git a/src/installer.js b/src/installer.js index b19a3b4..9b04e27 100644 --- a/src/installer.js +++ b/src/installer.js @@ -1,3 +1,4 @@ +const core = require('@actions/core') const { exec } = require('@actions/exec') const path = require('path') @@ -6,11 +7,28 @@ const path = require('path') * * @param {string} osVersion * @param {string} otpVersion + * @param {string[]} hexMirrors */ -async function installOTP(osVersion, otpVersion) { +async function installOTP(osVersion, otpVersion, hexMirrors) { const OS = process.platform if (OS === 'linux') { - await exec(path.join(__dirname, 'install-otp.sh'), [osVersion, otpVersion]) + if (hexMirrors.length === 0) { + throw new Error( + `Could not install Erlang/OTP ${otpVersion} from any hex.pm mirror`, + ) + } + const [hexMirror, ...hexMirrorsT] = hexMirrors + try { + await exec(path.join(__dirname, 'install-otp.sh'), [ + osVersion, + otpVersion, + hexMirror, + ]) + return + } catch (err) { + core.info(`install-otp.sh failed for mirror ${hexMirror}`) + } + await installOTP(osVersion, otpVersion, hexMirrorsT) } else if (OS === 'win32') { const script = path.join(__dirname, 'install-otp.ps1') await exec(`pwsh.exe ${script} -VSN:${otpVersion}`) @@ -21,15 +39,34 @@ async function installOTP(osVersion, otpVersion) { * Install Elixir. * * @param {string} elixirVersion + * @param {string[]} hexMirrors */ -async function installElixir(elixirVersion) { - const OS = process.platform - if (OS === 'linux') { - await exec(path.join(__dirname, 'install-elixir.sh'), [elixirVersion]) - } else if (OS === 'win32') { - const script = path.join(__dirname, 'install-elixir.ps1') - await exec(`pwsh.exe ${script} -VSN:${elixirVersion}`) +async function installElixir(elixirVersion, hexMirrors) { + if (hexMirrors.length === 0) { + throw new Error( + `Could not install Elixir ${elixirVersion} from any hex.pm mirror`, + ) } + const [hexMirror, ...hexMirrorsT] = hexMirrors + const OS = process.platform + let script + try { + if (OS === 'linux') { + script = path.join(__dirname, 'install-elixir.sh') + await exec(script, [elixirVersion, hexMirror]) + return + } + if (OS === 'win32') { + script = path.join(__dirname, 'install-elixir.ps1') + await exec( + `pwsh.exe ${script} -VSN:${elixirVersion} -HEX_MIRROR:${hexMirror}`, + ) + return + } + } catch (err) { + core.info(`${script} failed for mirror ${hexMirror}`) + } + await installElixir(elixirVersion, hexMirrorsT) } /** diff --git a/src/setup-beam.js b/src/setup-beam.js index e8b4ea6..9e73f1a 100644 --- a/src/setup-beam.js +++ b/src/setup-beam.js @@ -30,16 +30,23 @@ async function main() { const elixirSpec = getInput('elixir-version', false, 'elixir', versions) const gleamSpec = getInput('gleam-version', false, 'gleam', versions) const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions) + const hexMirrors = core.getMultilineInput('hexpm-mirrors', { + required: false, + }) if (otpSpec !== 'false') { - await installOTP(otpSpec, osVersion) - const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec) + await installOTP(otpSpec, osVersion, hexMirrors) + const elixirInstalled = await maybeInstallElixir( + elixirSpec, + otpSpec, + hexMirrors, + ) if (elixirInstalled === true) { const shouldMixRebar = getInput('install-rebar', false) - await mix(shouldMixRebar, 'rebar') + await mix(shouldMixRebar, 'rebar', hexMirrors) const shouldMixHex = getInput('install-hex', false) - await mix(shouldMixHex, 'hex') + await mix(shouldMixHex, 'hex', hexMirrors) } } else if (!gleamSpec) { throw new Error('otp-version=false is only available when installing Gleam') @@ -49,12 +56,12 @@ async function main() { await maybeInstallRebar3(rebar3Spec) } -async function installOTP(otpSpec, osVersion) { - const otpVersion = await getOTPVersion(otpSpec, osVersion) +async function installOTP(otpSpec, osVersion, hexMirrors) { + const otpVersion = await getOTPVersion(otpSpec, osVersion, hexMirrors) console.log( `##[group]Installing Erlang/OTP ${otpVersion} - built on ${osVersion}`, ) - await installer.installOTP(osVersion, otpVersion) + await installer.installOTP(osVersion, otpVersion, hexMirrors) core.setOutput('otp-version', otpVersion) core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/otp/bin`) console.log('##[endgroup]') @@ -62,13 +69,17 @@ async function installOTP(otpSpec, osVersion) { return otpVersion } -async function maybeInstallElixir(elixirSpec, otpVersion) { +async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { let installed = false if (elixirSpec) { - const elixirVersion = await getElixirVersion(elixirSpec, otpVersion) + const elixirVersion = await getElixirVersion( + elixirSpec, + otpSpec, + hexMirrors, + ) console.log(`##[group]Installing Elixir ${elixirVersion}`) - await installer.installElixir(elixirVersion) + await installer.installElixir(elixirVersion, hexMirrors) core.setOutput('elixir-version', elixirVersion) const disableProblemMatchers = getInput('disable_problem_matchers', false) if (disableProblemMatchers === 'false') { @@ -87,12 +98,28 @@ async function maybeInstallElixir(elixirSpec, otpVersion) { return installed } -async function mix(shouldMix, what) { +async function mixWithMirrors(cmd, args, hexMirrors) { + if (hexMirrors.length === 0) { + throw new Error('mix failed with every mirror') + } + const [hexMirror, ...hexMirrorsT] = hexMirrors + process.env.HEX_MIRROR = hexMirror + try { + return await exec(cmd, args) + } catch (err) { + core.info( + `mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`, + ) + } + return mixWithMirrors(cmd, args, hexMirrorsT) +} + +async function mix(shouldMix, what, hexMirrors) { if (shouldMix === 'true') { const cmd = 'mix' const args = [`local.${what}`, '--force'] console.log(`##[group]Running ${cmd} ${args}`) - await exec(cmd, args) + await mixWithMirrors(cmd, args, hexMirrors) console.log('##[endgroup]') } } @@ -136,8 +163,8 @@ async function maybeInstallRebar3(rebar3Spec) { return installed } -async function getOTPVersion(otpSpec0, osVersion) { - const otpVersions = await getOTPVersions(osVersion) +async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { + const otpVersions = await getOTPVersions(osVersion, hexMirrors) let otpSpec = otpSpec0 // might be a branch (?) const otpVersion = getVersionFromSpec( otpSpec, @@ -156,10 +183,10 @@ async function getOTPVersion(otpSpec0, osVersion) { return otpVersions.get(otpVersion) // from the reference, for download } -async function getElixirVersion(exSpec0, otpVersion0) { +async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) { const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2] const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1] - const elixirVersions = await getElixirVersions() + const elixirVersions = await getElixirVersions(hexMirrors) const semverVersions = Array.from(elixirVersions.keys()).sort() const exSpec = exSpec0.replace(/-otp-.*$/, '') const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true) @@ -219,19 +246,19 @@ async function getRebar3Version(r3Spec) { return rebar3Version } -async function getOTPVersions(osVersion) { - let originListing - let pageIdxs +async function getOTPVersions(osVersion, hexMirrors) { + let otpVersionsListings if (process.platform === 'linux') { - originListing = `https://builds.hex.pm/builds/otp/${osVersion}/builds.txt` - pageIdxs = [null] + otpVersionsListings = await getWithMirrors( + `/builds/otp/${osVersion}/builds.txt`, + hexMirrors, + ) } else if (process.platform === 'win32') { - originListing = + const originListing = 'https://api.github.com/repos/erlang/otp/releases?per_page=100' - pageIdxs = [1, 2, 3] + otpVersionsListings = await get(originListing, [1, 2, 3]) } - const otpVersionsListings = await get(originListing, pageIdxs) const otpVersions = new Map() if (process.platform === 'linux') { @@ -261,10 +288,10 @@ async function getOTPVersions(osVersion) { return otpVersions } -async function getElixirVersions() { - const elixirVersionsListings = await get( - 'https://builds.hex.pm/builds/elixir/builds.txt', - [null], +async function getElixirVersions(hexMirrors) { + const elixirVersionsListings = await getWithMirrors( + '/builds/elixir/builds.txt', + hexMirrors, ) const otpVersionsForElixirMap = new Map() @@ -448,6 +475,19 @@ async function get(url0, pageIdxs) { return Promise.all(pageIdxs.map(getPage)) } +async function getWithMirrors(resourcePath, hexMirrors) { + if (hexMirrors.length === 0) { + throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`) + } + const [hexMirror, ...hexMirrorsT] = hexMirrors + try { + return await get(`${hexMirror}${resourcePath}`, [null]) + } catch (err) { + core.info(`get failed for URL ${hexMirror}${resourcePath}`) + } + return getWithMirrors(resourcePath, hexMirrorsT) +} + function maybePrependWithV(v) { if (isVersion(v)) { return `v${v.replace('v', '')}`