Rework our version matching algorithm (#217)

* Update some dependencies' versions (also, get rid of husky)

* Introduce branch dependency execution

We want the most important things to fail first and prevent the
other ones from executing

e.g. if your dist/ is not updated, there's no point seeing the tests fail

* Rid ourselves of bloat

* Introduce expectations for upcoming version

* Tweak whitespace

* Tweak our calls to getVersionFromSpec (increase code consistency)

* Remove non-used variable

* Start coercing on input, stop sorting/maybe prepending 'v' on input

We'll deal with all this in a centralized place to make
it easier to reason on the code

* Revamp our getVersionFromSpec

1. stop accepting maybePrependWithV0, since this is only used in specialized
   cases
2. sort versions internally (don't expect it to come sorted)
3. compare rc/strict and .includes in a single go (don't separate these)
4. don't expect already coerced version arrays, coerce them when required
   (the maybeCoerced version had changed already, which means we're smarter
   at putting stuff inside the version array)
5. raise an exception if a semver-invalid version is not flagged as strict

* Reduce our expectations, but still be appropriate to our mission

* Update our tests for our new expectations (start the TDD bit)

* Execute npm run build-dist and keep results

* Increase inter-job dependency

* Don't expect markdownlint to be installed remotely

* Revert "Increase inter-job dependency"

This reverts commit 500b0fe7e9eec1c4f258b4d16b40719ee1d0a1ad.

* Revert "Introduce branch dependency execution"

This reverts commit fbe1317a800d7722f4e8b668c073218263d0ab17.

* Have all the CI workflows run under the same conditions

* Fix as per CI results

* Re-establish the possibility to match versions like 22.3.4.2.1.0

We cheat on the semver matching:
1. we keep an ordered list of versions that respect e.g. 22.3.4
2. if we match 22.3.4 when fetching a version, we get the last value
   from the list of versions that respect it, e.g. 22.3.4.2.1.0
   (since this is ordered, it should work as previously expected)

This increases code complexity for getVersionFromSpec,
but hopefully not by much...

We also remove throw-based test cases (non-versions will fail later,
with null), and add some tests to prove our need code.

* Trim out linting practices

But still keep them useful
This commit is contained in:
Paulo F. Oliveira
2023-07-21 20:56:02 +01:00
committed by GitHub
parent efc884ac2f
commit f6a73a7bf1
10 changed files with 1369 additions and 3642 deletions
+5 -8
View File
@@ -1,19 +1,16 @@
--- ---
env: env:
node: true node: true
es2021: true es2022: true
extends: extends:
- airbnb - eslint:recommended
parserOptions: parserOptions:
ecmaVersion: 12 ecmaVersion: 2022
rules: rules:
indent: ["warn", 2] indent: [warn, 2]
max-len: ["warn", 100] max-len: [warn, 100]
no-undef: "error"
no-unused-vars: ["error", {"varsIgnorePattern": "^_"}]
no-use-before-define: 0 no-use-before-define: 0
operator-linebreak: 0 operator-linebreak: 0
require-jsdoc: 0
semi: 0 semi: 0
settings: settings:
+6 -13
View File
@@ -6,7 +6,8 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
types: [opened, synchronize] branches:
- main
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -19,16 +20,8 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '14' node-version: '16'
- run: npm install -g npm - run: npm run build-dist
- run: npm install
- run: npm run build
- run: npm run format
- run: npm install -g markdownlint-cli
- run: npm run markdownlint
- run: npm run shellcheck
- run: npm run yamllint
- run: npm run jslint
- name: Check if build left artifacts - name: Check if build left artifacts
run: git diff --exit-code --ignore-space-at-eol run: git diff --exit-code --ignore-space-at-eol
@@ -39,7 +32,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '14' node-version: '16'
- run: npm ci - run: npm ci
- run: npm test - run: npm test
- name: .tool-versions test - name: .tool-versions test
@@ -56,6 +49,6 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '14' node-version: '16'
- run: npm install --production - run: npm install --production
- run: npm test - run: npm test
+3 -4
View File
@@ -6,7 +6,8 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
types: [opened, synchronize] branches:
- main
jobs: jobs:
integration_test: integration_test:
@@ -111,12 +112,10 @@ jobs:
otp-version: '22.3.4.1' otp-version: '22.3.4.1'
os: 'ubuntu-20.04' os: 'ubuntu-20.04'
version-type: 'strict' version-type: 'strict'
- elixir-version: '1.10.3'
otp-version: '22.3.4.1'
os: 'ubuntu-20.04'
- elixir-version: 'main' - elixir-version: 'main'
otp-version: '23.1' otp-version: '23.1'
os: 'ubuntu-20.04' os: 'ubuntu-20.04'
version-type: 'strict'
- elixir-version: 'main' - elixir-version: 'main'
otp-version: '25' otp-version: '25'
os: 'ubuntu-20.04' os: 'ubuntu-20.04'
@@ -12,9 +12,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- run: - run:
./.github/workflows/update_3rd_party_licenses.sh ./.github/workflows/update_3rd_party_licenses.sh
env: env:
+2 -1
View File
@@ -6,7 +6,8 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
types: [opened, synchronize] branches:
- main
jobs: jobs:
integration_test: integration_test:
+179 -96
View File
@@ -6221,15 +6221,18 @@ class Range {
const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE]
range = range.replace(hr, hyphenReplace(this.options.includePrerelease)) range = range.replace(hr, hyphenReplace(this.options.includePrerelease))
debug('hyphen replace', range) debug('hyphen replace', range)
// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace)
debug('comparator trim', range) debug('comparator trim', range)
// `~ 1.2.3` => `~1.2.3` // `~ 1.2.3` => `~1.2.3`
range = range.replace(re[t.TILDETRIM], tildeTrimReplace) range = range.replace(re[t.TILDETRIM], tildeTrimReplace)
debug('tilde trim', range)
// `^ 1.2.3` => `^1.2.3` // `^ 1.2.3` => `^1.2.3`
range = range.replace(re[t.CARETTRIM], caretTrimReplace) range = range.replace(re[t.CARETTRIM], caretTrimReplace)
debug('caret trim', range)
// At this point, the range is completely trimmed and // At this point, the range is completely trimmed and
// ready to be split into comparators. // ready to be split into comparators.
@@ -7531,6 +7534,10 @@ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
// Max safe segment length for coercion. // Max safe segment length for coercion.
const MAX_SAFE_COMPONENT_LENGTH = 16 const MAX_SAFE_COMPONENT_LENGTH = 16
// Max safe length for a build identifier. The max length minus 6 characters for
// the shortest version with a build 0.0.0+BUILD.
const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6
const RELEASE_TYPES = [ const RELEASE_TYPES = [
'major', 'major',
'premajor', 'premajor',
@@ -7544,6 +7551,7 @@ const RELEASE_TYPES = [
module.exports = { module.exports = {
MAX_LENGTH, MAX_LENGTH,
MAX_SAFE_COMPONENT_LENGTH, MAX_SAFE_COMPONENT_LENGTH,
MAX_SAFE_BUILD_LENGTH,
MAX_SAFE_INTEGER, MAX_SAFE_INTEGER,
RELEASE_TYPES, RELEASE_TYPES,
SEMVER_SPEC_VERSION, SEMVER_SPEC_VERSION,
@@ -7625,7 +7633,7 @@ module.exports = parseOptions
/***/ 9523: /***/ 9523:
/***/ ((module, exports, __nccwpck_require__) => { /***/ ((module, exports, __nccwpck_require__) => {
const { MAX_SAFE_COMPONENT_LENGTH } = __nccwpck_require__(2293) const { MAX_SAFE_COMPONENT_LENGTH, MAX_SAFE_BUILD_LENGTH } = __nccwpck_require__(2293)
const debug = __nccwpck_require__(427) const debug = __nccwpck_require__(427)
exports = module.exports = {} exports = module.exports = {}
@@ -7636,16 +7644,31 @@ const src = exports.src = []
const t = exports.t = {} const t = exports.t = {}
let R = 0 let R = 0
const LETTERDASHNUMBER = '[a-zA-Z0-9-]'
// Replace some greedy regex tokens to prevent regex dos issues. These regex are
// used internally via the safeRe object since all inputs in this library get
// normalized first to trim and collapse all extra whitespace. The original
// regexes are exported for userland consumption and lower level usage. A
// future breaking change could export the safer regex only with a note that
// all input should have extra whitespace removed.
const safeRegexReplacements = [
['\\s', 1],
['\\d', MAX_SAFE_COMPONENT_LENGTH],
[LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],
]
const makeSafeRegex = (value) => {
for (const [token, max] of safeRegexReplacements) {
value = value
.split(`${token}*`).join(`${token}{0,${max}}`)
.split(`${token}+`).join(`${token}{1,${max}}`)
}
return value
}
const createToken = (name, value, isGlobal) => { const createToken = (name, value, isGlobal) => {
// Replace all greedy whitespace to prevent regex dos issues. These regex are const safe = makeSafeRegex(value)
// used internally via the safeRe object since all inputs in this library get
// normalized first to trim and collapse all extra whitespace. The original
// regexes are exported for userland consumption and lower level usage. A
// future breaking change could export the safer regex only with a note that
// all input should have extra whitespace removed.
const safe = value
.split('\\s*').join('\\s{0,1}')
.split('\\s+').join('\\s')
const index = R++ const index = R++
debug(name, index, value) debug(name, index, value)
t[name] = index t[name] = index
@@ -7661,13 +7684,13 @@ const createToken = (name, value, isGlobal) => {
// A single `0`, or a non-zero digit followed by zero or more digits. // A single `0`, or a non-zero digit followed by zero or more digits.
createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*') createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*')
createToken('NUMERICIDENTIFIERLOOSE', '[0-9]+') createToken('NUMERICIDENTIFIERLOOSE', '\\d+')
// ## Non-numeric Identifier // ## Non-numeric Identifier
// Zero or more digits, followed by a letter or hyphen, and then zero or // Zero or more digits, followed by a letter or hyphen, and then zero or
// more letters, digits, or hyphens. // more letters, digits, or hyphens.
createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*') createToken('NONNUMERICIDENTIFIER', `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`)
// ## Main Version // ## Main Version
// Three dot-separated numeric identifiers. // Three dot-separated numeric identifiers.
@@ -7702,7 +7725,7 @@ createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE]
// ## Build Metadata Identifier // ## Build Metadata Identifier
// Any combination of digits, letters, or hyphens. // Any combination of digits, letters, or hyphens.
createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+') createToken('BUILDIDENTIFIER', `${LETTERDASHNUMBER}+`)
// ## Build Metadata // ## Build Metadata
// Plus sign, followed by one or more period-separated build metadata // Plus sign, followed by one or more period-separated build metadata
@@ -10052,15 +10075,16 @@ async function main() {
if (otpSpec !== 'false') { if (otpSpec !== 'false') {
await installOTP(otpSpec, osVersion, hexMirrors) await installOTP(otpSpec, osVersion, hexMirrors)
const elixirInstalled = await maybeInstallElixir( const elixirInstalled = await maybeInstallElixir(
elixirSpec, elixirSpec,
otpSpec, otpSpec,
hexMirrors, hexMirrors,
) )
if (elixirInstalled === true) { if (elixirInstalled === true) {
const shouldMixRebar = getInput('install-rebar', false) const shouldMixRebar = getInput('install-rebar', false)
await mix(shouldMixRebar, 'rebar', hexMirrors) await mix(shouldMixRebar, 'rebar', hexMirrors)
const shouldMixHex = getInput('install-hex', false) const shouldMixHex = getInput('install-hex', false)
await mix(shouldMixHex, 'hex', hexMirrors) await mix(shouldMixHex, 'hex', hexMirrors)
} }
@@ -10084,7 +10108,6 @@ async function installOTP(otpSpec, osVersion, hexMirrors) {
async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
let installed = false let installed = false
if (elixirSpec) { if (elixirSpec) {
const elixirVersion = await getElixirVersion( const elixirVersion = await getElixirVersion(
elixirSpec, elixirSpec,
@@ -10094,6 +10117,7 @@ async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
core.startGroup(`Installing Elixir ${elixirVersion}`) core.startGroup(`Installing Elixir ${elixirVersion}`)
await installer.installElixir(elixirVersion, hexMirrors) await installer.installElixir(elixirVersion, hexMirrors)
core.setOutput('elixir-version', elixirVersion) core.setOutput('elixir-version', elixirVersion)
const disableProblemMatchers = getInput('disable_problem_matchers', false) const disableProblemMatchers = getInput('disable_problem_matchers', false)
if (disableProblemMatchers === 'false') { if (disableProblemMatchers === 'false') {
const elixirMatchers = __nccwpck_require__.ab + "elixir-matchers.json" const elixirMatchers = __nccwpck_require__.ab + "elixir-matchers.json"
@@ -10111,6 +10135,7 @@ async function mixWithMirrors(cmd, args, hexMirrors) {
if (hexMirrors.length === 0) { if (hexMirrors.length === 0) {
throw new Error('mix failed with every mirror') throw new Error('mix failed with every mirror')
} }
const [hexMirror, ...hexMirrorsT] = hexMirrors const [hexMirror, ...hexMirrorsT] = hexMirrors
process.env.HEX_MIRROR = hexMirror process.env.HEX_MIRROR = hexMirror
try { try {
@@ -10120,6 +10145,7 @@ async function mixWithMirrors(cmd, args, hexMirrors) {
`mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`, `mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`,
) )
} }
return mixWithMirrors(cmd, args, hexMirrorsT) return mixWithMirrors(cmd, args, hexMirrorsT)
} }
@@ -10135,7 +10161,6 @@ async function mix(shouldMix, what, hexMirrors) {
async function maybeInstallGleam(gleamSpec) { async function maybeInstallGleam(gleamSpec) {
let installed = false let installed = false
if (gleamSpec) { if (gleamSpec) {
const gleamVersion = await getGleamVersion(gleamSpec) const gleamVersion = await getGleamVersion(gleamSpec)
core.startGroup(`Installing Gleam ${gleamVersion}`) core.startGroup(`Installing Gleam ${gleamVersion}`)
@@ -10153,7 +10178,6 @@ async function maybeInstallGleam(gleamSpec) {
async function maybeInstallRebar3(rebar3Spec) { async function maybeInstallRebar3(rebar3Spec) {
let installed = false let installed = false
let rebar3Version let rebar3Version
if (rebar3Spec) { if (rebar3Spec) {
if (rebar3Spec === 'nightly') { if (rebar3Spec === 'nightly') {
rebar3Version = 'nightly' rebar3Version = 'nightly'
@@ -10174,14 +10198,9 @@ async function maybeInstallRebar3(rebar3Spec) {
async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
const otpVersions = await getOTPVersions(osVersion, hexMirrors) const otpVersions = await getOTPVersions(osVersion, hexMirrors)
let otpSpec = otpSpec0 // might be a branch (?) const spec = otpSpec0.replace(/^OTP-/, '')
const otpVersion = getVersionFromSpec( const versions = otpVersions
otpSpec, const otpVersion = getVersionFromSpec(spec, versions)
Array.from(otpVersions.keys()).sort(),
)
if (isVersion(otpSpec0)) {
otpSpec = `OTP-${otpSpec0}` // ... it's a version!
}
if (otpVersion === null) { if (otpVersion === null) {
throw new Error( throw new Error(
`Requested Erlang/OTP version (${otpSpec0}) not found in version list ` + `Requested Erlang/OTP version (${otpSpec0}) not found in version list ` +
@@ -10189,20 +10208,18 @@ async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
) )
} }
return otpVersions.get(otpVersion) // from the reference, for download return otpVersion // from the reference, for download
} }
async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) { async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2] const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2]
const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1] const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1]
const elixirVersions = await getElixirVersions(hexMirrors) const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions(
const semverVersions = Array.from(elixirVersions.keys()).sort() hexMirrors,
const exSpec = exSpec0.replace(/-otp-.*$/, '') )
const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true) const spec = exSpec0.replace(/-otp-.*$/, '')
let elixirVersionForDownload = elixirVersionFromSpec const versions = elixirVersions
if (isVersion(otpVersionMajor)) { const elixirVersionFromSpec = getVersionFromSpec(spec, versions)
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
}
if (elixirVersionFromSpec === null) { if (elixirVersionFromSpec === null) {
throw new Error( throw new Error(
`Requested Elixir version (${exSpec0}) not found in version list ` + `Requested Elixir version (${exSpec0}) not found in version list ` +
@@ -10210,7 +10227,7 @@ async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
) )
} }
const elixirVersionComp = elixirVersions.get(elixirVersionFromSpec) const elixirVersionComp = otpVersionsForElixirMap[elixirVersionFromSpec]
if ( if (
(elixirVersionComp && elixirVersionComp.includes(otpVersionMajor)) || (elixirVersionComp && elixirVersionComp.includes(otpVersionMajor)) ||
!isVersion(otpVersionMajor) !isVersion(otpVersionMajor)
@@ -10227,13 +10244,20 @@ async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
) )
} }
let elixirVersionForDownload = elixirVersionFromSpec
if (isVersion(otpVersionMajor)) {
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
}
return maybePrependWithV(elixirVersionForDownload) return maybePrependWithV(elixirVersionForDownload)
} }
async function getGleamVersion(gleamSpec0) { async function getGleamVersion(gleamSpec0) {
const gleamSpec = gleamSpec0.match(/^v?(.+)$/)
const gleamVersions = await getGleamVersions() const gleamVersions = await getGleamVersions()
const gleamVersion = getVersionFromSpec(gleamSpec[1], gleamVersions, true) const spec = gleamSpec0
const versions = gleamVersions
const gleamVersion = getVersionFromSpec(spec, versions)
if (gleamVersion === null) { if (gleamVersion === null) {
throw new Error( throw new Error(
`Requested Gleam version (${gleamSpec0}) not found in version list ` + `Requested Gleam version (${gleamSpec0}) not found in version list ` +
@@ -10241,12 +10265,14 @@ async function getGleamVersion(gleamSpec0) {
) )
} }
return maybePrependWithV(gleamVersion, gleamVersion) return maybePrependWithV(gleamVersion)
} }
async function getRebar3Version(r3Spec) { async function getRebar3Version(r3Spec) {
const rebar3Versions = await getRebar3Versions() const rebar3Versions = await getRebar3Versions()
const rebar3Version = getVersionFromSpec(r3Spec, rebar3Versions) const spec = r3Spec
const versions = rebar3Versions
const rebar3Version = getVersionFromSpec(spec, versions)
if (rebar3Version === null) { if (rebar3Version === null) {
throw new Error( throw new Error(
`Requested rebar3 version (${r3Spec}) not found in version list ` + `Requested rebar3 version (${r3Spec}) not found in version list ` +
@@ -10269,8 +10295,9 @@ async function getOTPVersions(osVersion, hexMirrors) {
otpVersionsListings = await get(originListing, [1, 2, 3]) otpVersionsListings = await get(originListing, [1, 2, 3])
} }
const otpVersions = new Map() debugLog(`OTP versions listings from ${originListing}`, otpVersionsListings)
const otpVersions = {}
if (process.platform === 'linux') { if (process.platform === 'linux') {
otpVersionsListings otpVersionsListings
.trim() .trim()
@@ -10280,8 +10307,9 @@ async function getOTPVersions(osVersion, hexMirrors) {
.match(/^([^ ]+)?( .+)/)[1] .match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/) .match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2] const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
debugLog('OTP line and parsing', [line, otpVersion, otpMatch]) debugLog('OTP line and parsing', [line, otpVersion, otpMatch])
otpVersions.set(otpVersion, otpMatch[0]) // we keep the original for later reference otpVersions[otpVersion] = otpVersionOrig // we keep the original for later reference
}) })
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
otpVersionsListings.forEach((otpVersionsListing) => { otpVersionsListings.forEach((otpVersionsListing) => {
@@ -10293,15 +10321,12 @@ async function getOTPVersions(osVersion, hexMirrors) {
const otpMatch = x.name.match(/^otp_win64_(.*).exe$/) const otpMatch = x.name.match(/^otp_win64_(.*).exe$/)
const otpVersion = otpMatch[1] const otpVersion = otpMatch[1]
debugLog('OTP line and parsing', [otpMatch, otpVersion]) debugLog('OTP line and parsing', [otpMatch, otpVersion])
otpVersions.set(otpVersion, otpVersion) otpVersions[otpVersion] = otpVersion
}) })
}) })
} }
debugLog( debugLog(`OTP versions from ${originListing}`, JSON.stringify(otpVersions))
`OTP versions from ${originListing}`,
JSON.stringify([...otpVersions.entries()]),
)
return otpVersions return otpVersions
} }
@@ -10311,7 +10336,8 @@ async function getElixirVersions(hexMirrors) {
'/builds/elixir/builds.txt', '/builds/elixir/builds.txt',
hexMirrors, hexMirrors,
) )
const otpVersionsForElixirMap = new Map() const otpVersionsForElixirMap = {}
const elixirVersions = {}
elixirVersionsListings elixirVersionsListings
.trim() .trim()
@@ -10319,17 +10345,18 @@ async function getElixirVersions(hexMirrors) {
.forEach((line) => { .forEach((line) => {
const elixirMatch = const elixirMatch =
line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/) line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/)
const elixirVersion = maybePrependWithV(elixirMatch[1]) const elixirVersion = elixirMatch[1]
const otpVersion = elixirMatch[2] const otpVersion = elixirMatch[2]
const otpVersions = otpVersionsForElixirMap.get(elixirVersion) || [] const otpVersions = otpVersionsForElixirMap[elixirVersion] || []
if (otpVersion) { if (otpVersion) {
// -otp- present (special case) // -otp- present (special case)
otpVersions.push(otpVersion) otpVersions.push(otpVersion)
} }
otpVersionsForElixirMap.set(elixirVersion, otpVersions) otpVersionsForElixirMap[elixirVersion] = otpVersions
elixirVersions[elixirVersion] = elixirVersion
}) })
return otpVersionsForElixirMap return [otpVersionsForElixirMap, elixirVersions]
} }
async function getGleamVersions() { async function getGleamVersions() {
@@ -10337,13 +10364,17 @@ async function getGleamVersions() {
'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100', 'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100',
[1, 2, 3], [1, 2, 3],
) )
const gleamVersionsListing = [] const gleamVersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) jsonParseAsList(resultJSON)
.map((x) => x.tag_name) .map((x) => x.tag_name)
.sort() .forEach((ver) => {
.forEach((v) => gleamVersionsListing.push(v)) const gleamMatch = ver.match(/^v?([^ ]+)/)
const gleamVersion = gleamMatch[1]
gleamVersionsListing[gleamVersion] = gleamVersion
})
}) })
return gleamVersionsListing return gleamVersionsListing
} }
@@ -10352,13 +10383,15 @@ async function getRebar3Versions() {
'https://api.github.com/repos/erlang/rebar3/releases?per_page=100', 'https://api.github.com/repos/erlang/rebar3/releases?per_page=100',
[1, 2, 3], [1, 2, 3],
) )
const rebar3VersionsListing = [] const rebar3VersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) jsonParseAsList(resultJSON)
.map((x) => x.tag_name) .map((x) => x.tag_name)
.sort() .forEach((ver) => {
.forEach((v) => rebar3VersionsListing.push(v)) rebar3VersionsListing[ver] = ver
})
}) })
return rebar3VersionsListing return rebar3VersionsListing
} }
@@ -10366,48 +10399,68 @@ function isStrictVersion() {
return getInput('version-type', false) === 'strict' return getInput('version-type', false) === 'strict'
} }
function getVersionFromSpec(spec, versions, maybePrependWithV0) { function getVersionFromSpec(spec0, versions0) {
const spec = maybeRemoveVPrefix(spec0)
const altVersions = {}
Object.entries(versions0).forEach(([version, altVersion]) => {
let coerced
if (isStrictVersion() || isRC(version)) {
// If `version-type: strict` or version is RC, we just try to remove a potential initial v
coerced = maybeRemoveVPrefix(version)
} else {
// Otherwise, we place the version into a version bucket
coerced = maybeCoerced(version)
}
const alt = (altVersions[coerced] || []).concat(altVersion)
alt.sort(sortVersions)
altVersions[coerced] = alt
})
let versions = Object.keys(altVersions)
const rangeForMax = semver.validRange(spec0) || maybeCoerced(spec)
const rangeMax = semver.maxSatisfying(versions, rangeForMax)
let version = null let version = null
if (spec.match(/rc/) || isStrictVersion()) { if (isStrictVersion() || isRC(spec0)) {
version = spec if (versions0[spec]) {
} // If `version-type: strict` or version is RC, we obtain it directly
version = versions0[spec]
}
} else if (rangeMax !== null) {
// Otherwise, we compare alt. versions' semver ranges to this version, from highest to lowest
const thatVersion = spec
const thatVersionAbc = versionAbc(thatVersion)
const thatVersionAbcRange = semver.validRange(thatVersionAbc)
if (version === null) { versions = altVersions[rangeMax]
// We keep a map of semver => "spec" in order to use semver ranges to find appropriate versions for (let i = versions.length - 1; i >= 0; i -= 1) {
const versionsMap = versions.sort(sortVersions).reduce((acc, v) => { const thisVersion = versions[i]
if (!v.match(/rc/)) { const thisVersionAbc = versionAbc(thisVersion)
// release candidates are opt-in const thisVersionAbcRange = semver.validRange(thisVersionAbc)
acc[maybeCoerced(v)] = v
if (
thatVersionAbcRange &&
semver.intersects(thatVersionAbcRange, thisVersionAbcRange)
) {
version = thisVersion
break
} }
return acc
}, {})
const rangeForMax = semver.validRange(spec)
if (rangeForMax) {
version =
versionsMap[semver.maxSatisfying(Object.keys(versionsMap), rangeForMax)]
} else {
version = versionsMap[maybeCoerced(spec)]
} }
} }
let v = version === null || version === undefined ? null : version return version || null
if (maybePrependWithV0 && v != null) {
v = maybePrependWithV(v)
}
if (!versions.includes(v)) {
v = null
}
return v
} }
function maybeCoerced(v) { function maybeCoerced(v) {
let ret let ret = null
try { try {
ret = semver.coerce(v).version if (!isRC(v)) {
ret = semver.coerce(v).version
} else {
ret = maybeRemoveVPrefix(v)
}
} catch { } catch {
// some stuff can't be coerced, like 'main' // some stuff can't be coerced, like 'main'
ret = v ret = v
@@ -10420,11 +10473,11 @@ function sortVersions(left, right) {
let ret = 0 let ret = 0
const newL = verAsComparableStr(left) const newL = verAsComparableStr(left)
const newR = verAsComparableStr(right) const newR = verAsComparableStr(right)
function verAsComparableStr(ver) { function verAsComparableStr(ver) {
const matchGroups = 5 const matchGroups = 6
const verSpec = /([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?/ const verSpec = xyzAbcVersion('', '')
const matches = ver.match(verSpec).splice(1, matchGroups) const matches = ver.match(verSpec).splice(1, matchGroups)
return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '') return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '')
} }
@@ -10437,6 +10490,10 @@ function sortVersions(left, right) {
return ret return ret
} }
function isRC(ver) {
return ver.match(xyzAbcVersion('^', '(?:-rc\\.?\\d+)'))
}
function getRunnerOSVersion() { function getRunnerOSVersion() {
const ImageOSToContainer = { const ImageOSToContainer = {
ubuntu18: 'ubuntu-18.04', ubuntu18: 'ubuntu-18.04',
@@ -10446,7 +10503,6 @@ function getRunnerOSVersion() {
win22: 'windows-2022', win22: 'windows-2022',
} }
const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS] const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS]
if (!containerFromEnvImageOS) { if (!containerFromEnvImageOS) {
throw new Error( throw new Error(
"Tried to map a target OS from env. variable 'ImageOS' (got " + "Tried to map a target OS from env. variable 'ImageOS' (got " +
@@ -10467,7 +10523,6 @@ async function get(url0, pageIdxs) {
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)
if (GithubToken && url.host === 'api.github.com') { if (GithubToken && url.host === 'api.github.com') {
headers.authorization = `Bearer ${GithubToken}` headers.authorization = `Bearer ${GithubToken}`
} }
@@ -10480,9 +10535,7 @@ async function get(url0, pageIdxs) {
allowRetries: true, allowRetries: true,
maxRetries: 3, maxRetries: 3,
}) })
const response = await httpClient.get(url, headers) const response = await httpClient.get(url, headers)
if (response.statusCode >= 400 && response.statusCode <= 599) { if (response.statusCode >= 400 && response.statusCode <= 599) {
throw new Error( throw new Error(
`Got ${response.statusCode} from ${url}. Exiting with error`, `Got ${response.statusCode} from ${url}. Exiting with error`,
@@ -10495,6 +10548,7 @@ async function get(url0, pageIdxs) {
if (pageIdxs[0] === null) { if (pageIdxs[0] === null) {
return getPage(null) return getPage(null)
} }
return Promise.all(pageIdxs.map(getPage)) return Promise.all(pageIdxs.map(getPage))
} }
@@ -10502,12 +10556,14 @@ async function getWithMirrors(resourcePath, hexMirrors) {
if (hexMirrors.length === 0) { if (hexMirrors.length === 0) {
throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`) throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`)
} }
const [hexMirror, ...hexMirrorsT] = hexMirrors const [hexMirror, ...hexMirrorsT] = hexMirrors
try { try {
return await get(`${hexMirror}${resourcePath}`, [null]) return await get(`${hexMirror}${resourcePath}`, [null])
} catch (err) { } catch (err) {
core.info(`get failed for URL ${hexMirror}${resourcePath}`) core.info(`get failed for URL ${hexMirror}${resourcePath}`)
} }
return getWithMirrors(resourcePath, hexMirrorsT) return getWithMirrors(resourcePath, hexMirrorsT)
} }
@@ -10515,11 +10571,35 @@ function maybePrependWithV(v) {
if (isVersion(v)) { if (isVersion(v)) {
return `v${v.replace('v', '')}` return `v${v.replace('v', '')}`
} }
return v return v
} }
function maybeRemoveVPrefix(ver) {
let ret = ver
if (isVersion(ver)) {
ret = ver.replace('v', '')
}
return ret
}
function xyzAbcVersion(pref, suf) {
// This accounts for stuff like 6.0.2.0.1.0, as proposed by Erlang's
// https://www.erlang.org/doc/system_principles/versions.html
const dd = '\\.?(\\d+)?'
return new RegExp(
`${pref}v?(\\d+)${dd}${dd}${dd}${dd}${dd}(?:-rc\\.?\\d+)?(?:-otp-\\d+)?${suf}`,
)
}
function versionAbc(ver) {
// For a version like 6.0.2.0.1.0, return 0.1.0
return ver.match(/\d+(?:\.[^.]+)?(?:\.[^.]+)?(?:\.)?(.*)/)[1]
}
function isVersion(v) { function isVersion(v) {
return /^v?\d+/.test(v) return v.match(xyzAbcVersion('^', '$')) !== null
} }
function getInput(inputName, required, alternativeName, alternatives) { function getInput(inputName, required, alternativeName, alternatives) {
@@ -10537,6 +10617,7 @@ alongside ${alternativeName}=${alternativeValue} \
} else if (!input) { } else if (!input) {
input = alternativeValue input = alternativeValue
} }
return input return input
} }
@@ -10550,6 +10631,7 @@ function parseVersionFile(versionFilePath0) {
`The specified version file, ${versionFilePath0}, does not exist`, `The specified version file, ${versionFilePath0}, does not exist`,
) )
} }
core.startGroup(`Parsing version file at ${versionFilePath0}`) core.startGroup(`Parsing version file at ${versionFilePath0}`)
const appVersions = new Map() const appVersions = new Map()
const versions = fs.readFileSync(versionFilePath, 'utf8') const versions = fs.readFileSync(versionFilePath, 'utf8')
@@ -10583,6 +10665,7 @@ function jsonParseAsList(maybeJson) {
if (!Array.isArray(obj)) { if (!Array.isArray(obj)) {
throw new Error('expected a list!') throw new Error('expected a list!')
} }
return obj return obj
} catch (exc) { } catch (exc) {
throw new Error( throw new Error(
+972 -3415
View File
File diff suppressed because it is too large Load Diff
+7 -17
View File
@@ -13,29 +13,19 @@
"yamllint": "yamllint .github/workflows/**.yml && yamllint .*.yml && yamllint *.yml", "yamllint": "yamllint .github/workflows/**.yml && yamllint .*.yml && yamllint *.yml",
"build-dist": "npm install && npm run build && npm run format && npm run markdownlint && npm run shellcheck && npm run yamllint && npm run jslint" "build-dist": "npm install && npm run build && npm run format && npm run markdownlint && npm run shellcheck && npm run yamllint && npm run jslint"
}, },
"husky": {
"hooks": {
"pre-commit": "npm run format -- --check && npm run build && git add dist/"
}
},
"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/http-client": "2.1.0",
"@actions/tool-cache": "^2.0.1", "@actions/tool-cache": "2.0.1",
"semver": "7.5.2" "semver": "7.5.3"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "0.34.0", "@vercel/ncc": "0.36.1",
"eslint": "8.25.0", "eslint": "8.43.0",
"eslint-config-airbnb": "19.0.4", "markdownlint-cli": "0.35.0",
"eslint-plugin-import": "2.26.0", "prettier": "2.8.8",
"eslint-plugin-jsx-a11y": "6.6.1", "shellcheck": "2.2.0",
"eslint-plugin-react": "7.31.10",
"husky": "8.0.1",
"markdownlint": "0.26.2",
"prettier": "2.7.1",
"shellcheck": "1.1.0",
"yaml-lint": "1.7.0", "yaml-lint": "1.7.0",
"yarn": "1.22.19" "yarn": "1.22.19"
}, },
+143 -83
View File
@@ -35,15 +35,16 @@ async function main() {
if (otpSpec !== 'false') { if (otpSpec !== 'false') {
await installOTP(otpSpec, osVersion, hexMirrors) await installOTP(otpSpec, osVersion, hexMirrors)
const elixirInstalled = await maybeInstallElixir( const elixirInstalled = await maybeInstallElixir(
elixirSpec, elixirSpec,
otpSpec, otpSpec,
hexMirrors, hexMirrors,
) )
if (elixirInstalled === true) { if (elixirInstalled === true) {
const shouldMixRebar = getInput('install-rebar', false) const shouldMixRebar = getInput('install-rebar', false)
await mix(shouldMixRebar, 'rebar', hexMirrors) await mix(shouldMixRebar, 'rebar', hexMirrors)
const shouldMixHex = getInput('install-hex', false) const shouldMixHex = getInput('install-hex', false)
await mix(shouldMixHex, 'hex', hexMirrors) await mix(shouldMixHex, 'hex', hexMirrors)
} }
@@ -67,7 +68,6 @@ async function installOTP(otpSpec, osVersion, hexMirrors) {
async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) { async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
let installed = false let installed = false
if (elixirSpec) { if (elixirSpec) {
const elixirVersion = await getElixirVersion( const elixirVersion = await getElixirVersion(
elixirSpec, elixirSpec,
@@ -77,6 +77,7 @@ async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
core.startGroup(`Installing Elixir ${elixirVersion}`) core.startGroup(`Installing Elixir ${elixirVersion}`)
await installer.installElixir(elixirVersion, hexMirrors) await installer.installElixir(elixirVersion, hexMirrors)
core.setOutput('elixir-version', elixirVersion) core.setOutput('elixir-version', elixirVersion)
const disableProblemMatchers = getInput('disable_problem_matchers', false) const disableProblemMatchers = getInput('disable_problem_matchers', false)
if (disableProblemMatchers === 'false') { if (disableProblemMatchers === 'false') {
const elixirMatchers = path.join( const elixirMatchers = path.join(
@@ -99,6 +100,7 @@ async function mixWithMirrors(cmd, args, hexMirrors) {
if (hexMirrors.length === 0) { if (hexMirrors.length === 0) {
throw new Error('mix failed with every mirror') throw new Error('mix failed with every mirror')
} }
const [hexMirror, ...hexMirrorsT] = hexMirrors const [hexMirror, ...hexMirrorsT] = hexMirrors
process.env.HEX_MIRROR = hexMirror process.env.HEX_MIRROR = hexMirror
try { try {
@@ -108,6 +110,7 @@ async function mixWithMirrors(cmd, args, hexMirrors) {
`mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`, `mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`,
) )
} }
return mixWithMirrors(cmd, args, hexMirrorsT) return mixWithMirrors(cmd, args, hexMirrorsT)
} }
@@ -123,7 +126,6 @@ async function mix(shouldMix, what, hexMirrors) {
async function maybeInstallGleam(gleamSpec) { async function maybeInstallGleam(gleamSpec) {
let installed = false let installed = false
if (gleamSpec) { if (gleamSpec) {
const gleamVersion = await getGleamVersion(gleamSpec) const gleamVersion = await getGleamVersion(gleamSpec)
core.startGroup(`Installing Gleam ${gleamVersion}`) core.startGroup(`Installing Gleam ${gleamVersion}`)
@@ -141,7 +143,6 @@ async function maybeInstallGleam(gleamSpec) {
async function maybeInstallRebar3(rebar3Spec) { async function maybeInstallRebar3(rebar3Spec) {
let installed = false let installed = false
let rebar3Version let rebar3Version
if (rebar3Spec) { if (rebar3Spec) {
if (rebar3Spec === 'nightly') { if (rebar3Spec === 'nightly') {
rebar3Version = 'nightly' rebar3Version = 'nightly'
@@ -162,14 +163,9 @@ async function maybeInstallRebar3(rebar3Spec) {
async function getOTPVersion(otpSpec0, osVersion, hexMirrors) { async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
const otpVersions = await getOTPVersions(osVersion, hexMirrors) const otpVersions = await getOTPVersions(osVersion, hexMirrors)
let otpSpec = otpSpec0 // might be a branch (?) const spec = otpSpec0.replace(/^OTP-/, '')
const otpVersion = getVersionFromSpec( const versions = otpVersions
otpSpec, const otpVersion = getVersionFromSpec(spec, versions)
Array.from(otpVersions.keys()).sort(),
)
if (isVersion(otpSpec0)) {
otpSpec = `OTP-${otpSpec0}` // ... it's a version!
}
if (otpVersion === null) { if (otpVersion === null) {
throw new Error( throw new Error(
`Requested Erlang/OTP version (${otpSpec0}) not found in version list ` + `Requested Erlang/OTP version (${otpSpec0}) not found in version list ` +
@@ -177,20 +173,18 @@ async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
) )
} }
return otpVersions.get(otpVersion) // from the reference, for download return otpVersion // from the reference, for download
} }
async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) { async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2] const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2]
const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1] const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1]
const elixirVersions = await getElixirVersions(hexMirrors) const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions(
const semverVersions = Array.from(elixirVersions.keys()).sort() hexMirrors,
const exSpec = exSpec0.replace(/-otp-.*$/, '') )
const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true) const spec = exSpec0.replace(/-otp-.*$/, '')
let elixirVersionForDownload = elixirVersionFromSpec const versions = elixirVersions
if (isVersion(otpVersionMajor)) { const elixirVersionFromSpec = getVersionFromSpec(spec, versions)
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
}
if (elixirVersionFromSpec === null) { if (elixirVersionFromSpec === null) {
throw new Error( throw new Error(
`Requested Elixir version (${exSpec0}) not found in version list ` + `Requested Elixir version (${exSpec0}) not found in version list ` +
@@ -198,7 +192,7 @@ async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
) )
} }
const elixirVersionComp = elixirVersions.get(elixirVersionFromSpec) const elixirVersionComp = otpVersionsForElixirMap[elixirVersionFromSpec]
if ( if (
(elixirVersionComp && elixirVersionComp.includes(otpVersionMajor)) || (elixirVersionComp && elixirVersionComp.includes(otpVersionMajor)) ||
!isVersion(otpVersionMajor) !isVersion(otpVersionMajor)
@@ -215,13 +209,20 @@ async function getElixirVersion(exSpec0, otpVersion0, hexMirrors) {
) )
} }
let elixirVersionForDownload = elixirVersionFromSpec
if (isVersion(otpVersionMajor)) {
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
}
return maybePrependWithV(elixirVersionForDownload) return maybePrependWithV(elixirVersionForDownload)
} }
async function getGleamVersion(gleamSpec0) { async function getGleamVersion(gleamSpec0) {
const gleamSpec = gleamSpec0.match(/^v?(.+)$/)
const gleamVersions = await getGleamVersions() const gleamVersions = await getGleamVersions()
const gleamVersion = getVersionFromSpec(gleamSpec[1], gleamVersions, true) const spec = gleamSpec0
const versions = gleamVersions
const gleamVersion = getVersionFromSpec(spec, versions)
if (gleamVersion === null) { if (gleamVersion === null) {
throw new Error( throw new Error(
`Requested Gleam version (${gleamSpec0}) not found in version list ` + `Requested Gleam version (${gleamSpec0}) not found in version list ` +
@@ -229,12 +230,14 @@ async function getGleamVersion(gleamSpec0) {
) )
} }
return maybePrependWithV(gleamVersion, gleamVersion) return maybePrependWithV(gleamVersion)
} }
async function getRebar3Version(r3Spec) { async function getRebar3Version(r3Spec) {
const rebar3Versions = await getRebar3Versions() const rebar3Versions = await getRebar3Versions()
const rebar3Version = getVersionFromSpec(r3Spec, rebar3Versions) const spec = r3Spec
const versions = rebar3Versions
const rebar3Version = getVersionFromSpec(spec, versions)
if (rebar3Version === null) { if (rebar3Version === null) {
throw new Error( throw new Error(
`Requested rebar3 version (${r3Spec}) not found in version list ` + `Requested rebar3 version (${r3Spec}) not found in version list ` +
@@ -257,8 +260,9 @@ async function getOTPVersions(osVersion, hexMirrors) {
otpVersionsListings = await get(originListing, [1, 2, 3]) otpVersionsListings = await get(originListing, [1, 2, 3])
} }
const otpVersions = new Map() debugLog(`OTP versions listings from ${originListing}`, otpVersionsListings)
const otpVersions = {}
if (process.platform === 'linux') { if (process.platform === 'linux') {
otpVersionsListings otpVersionsListings
.trim() .trim()
@@ -268,8 +272,9 @@ async function getOTPVersions(osVersion, hexMirrors) {
.match(/^([^ ]+)?( .+)/)[1] .match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/) .match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2] const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
debugLog('OTP line and parsing', [line, otpVersion, otpMatch]) debugLog('OTP line and parsing', [line, otpVersion, otpMatch])
otpVersions.set(otpVersion, otpMatch[0]) // we keep the original for later reference otpVersions[otpVersion] = otpVersionOrig // we keep the original for later reference
}) })
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
otpVersionsListings.forEach((otpVersionsListing) => { otpVersionsListings.forEach((otpVersionsListing) => {
@@ -281,15 +286,12 @@ async function getOTPVersions(osVersion, hexMirrors) {
const otpMatch = x.name.match(/^otp_win64_(.*).exe$/) const otpMatch = x.name.match(/^otp_win64_(.*).exe$/)
const otpVersion = otpMatch[1] const otpVersion = otpMatch[1]
debugLog('OTP line and parsing', [otpMatch, otpVersion]) debugLog('OTP line and parsing', [otpMatch, otpVersion])
otpVersions.set(otpVersion, otpVersion) otpVersions[otpVersion] = otpVersion
}) })
}) })
} }
debugLog( debugLog(`OTP versions from ${originListing}`, JSON.stringify(otpVersions))
`OTP versions from ${originListing}`,
JSON.stringify([...otpVersions.entries()]),
)
return otpVersions return otpVersions
} }
@@ -299,7 +301,8 @@ async function getElixirVersions(hexMirrors) {
'/builds/elixir/builds.txt', '/builds/elixir/builds.txt',
hexMirrors, hexMirrors,
) )
const otpVersionsForElixirMap = new Map() const otpVersionsForElixirMap = {}
const elixirVersions = {}
elixirVersionsListings elixirVersionsListings
.trim() .trim()
@@ -307,17 +310,18 @@ async function getElixirVersions(hexMirrors) {
.forEach((line) => { .forEach((line) => {
const elixirMatch = const elixirMatch =
line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/) line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/)
const elixirVersion = maybePrependWithV(elixirMatch[1]) const elixirVersion = elixirMatch[1]
const otpVersion = elixirMatch[2] const otpVersion = elixirMatch[2]
const otpVersions = otpVersionsForElixirMap.get(elixirVersion) || [] const otpVersions = otpVersionsForElixirMap[elixirVersion] || []
if (otpVersion) { if (otpVersion) {
// -otp- present (special case) // -otp- present (special case)
otpVersions.push(otpVersion) otpVersions.push(otpVersion)
} }
otpVersionsForElixirMap.set(elixirVersion, otpVersions) otpVersionsForElixirMap[elixirVersion] = otpVersions
elixirVersions[elixirVersion] = elixirVersion
}) })
return otpVersionsForElixirMap return [otpVersionsForElixirMap, elixirVersions]
} }
async function getGleamVersions() { async function getGleamVersions() {
@@ -325,13 +329,17 @@ async function getGleamVersions() {
'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100', 'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100',
[1, 2, 3], [1, 2, 3],
) )
const gleamVersionsListing = [] const gleamVersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) jsonParseAsList(resultJSON)
.map((x) => x.tag_name) .map((x) => x.tag_name)
.sort() .forEach((ver) => {
.forEach((v) => gleamVersionsListing.push(v)) const gleamMatch = ver.match(/^v?([^ ]+)/)
const gleamVersion = gleamMatch[1]
gleamVersionsListing[gleamVersion] = gleamVersion
})
}) })
return gleamVersionsListing return gleamVersionsListing
} }
@@ -340,13 +348,15 @@ async function getRebar3Versions() {
'https://api.github.com/repos/erlang/rebar3/releases?per_page=100', 'https://api.github.com/repos/erlang/rebar3/releases?per_page=100',
[1, 2, 3], [1, 2, 3],
) )
const rebar3VersionsListing = [] const rebar3VersionsListing = {}
resultJSONs.forEach((resultJSON) => { resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON) jsonParseAsList(resultJSON)
.map((x) => x.tag_name) .map((x) => x.tag_name)
.sort() .forEach((ver) => {
.forEach((v) => rebar3VersionsListing.push(v)) rebar3VersionsListing[ver] = ver
})
}) })
return rebar3VersionsListing return rebar3VersionsListing
} }
@@ -354,48 +364,68 @@ function isStrictVersion() {
return getInput('version-type', false) === 'strict' return getInput('version-type', false) === 'strict'
} }
function getVersionFromSpec(spec, versions, maybePrependWithV0) { function getVersionFromSpec(spec0, versions0) {
const spec = maybeRemoveVPrefix(spec0)
const altVersions = {}
Object.entries(versions0).forEach(([version, altVersion]) => {
let coerced
if (isStrictVersion() || isRC(version)) {
// If `version-type: strict` or version is RC, we just try to remove a potential initial v
coerced = maybeRemoveVPrefix(version)
} else {
// Otherwise, we place the version into a version bucket
coerced = maybeCoerced(version)
}
const alt = (altVersions[coerced] || []).concat(altVersion)
alt.sort(sortVersions)
altVersions[coerced] = alt
})
let versions = Object.keys(altVersions)
const rangeForMax = semver.validRange(spec0) || maybeCoerced(spec)
const rangeMax = semver.maxSatisfying(versions, rangeForMax)
let version = null let version = null
if (spec.match(/rc/) || isStrictVersion()) { if (isStrictVersion() || isRC(spec0)) {
version = spec if (versions0[spec]) {
} // If `version-type: strict` or version is RC, we obtain it directly
version = versions0[spec]
}
} else if (rangeMax !== null) {
// Otherwise, we compare alt. versions' semver ranges to this version, from highest to lowest
const thatVersion = spec
const thatVersionAbc = versionAbc(thatVersion)
const thatVersionAbcRange = semver.validRange(thatVersionAbc)
if (version === null) { versions = altVersions[rangeMax]
// We keep a map of semver => "spec" in order to use semver ranges to find appropriate versions for (let i = versions.length - 1; i >= 0; i -= 1) {
const versionsMap = versions.sort(sortVersions).reduce((acc, v) => { const thisVersion = versions[i]
if (!v.match(/rc/)) { const thisVersionAbc = versionAbc(thisVersion)
// release candidates are opt-in const thisVersionAbcRange = semver.validRange(thisVersionAbc)
acc[maybeCoerced(v)] = v
if (
thatVersionAbcRange &&
semver.intersects(thatVersionAbcRange, thisVersionAbcRange)
) {
version = thisVersion
break
} }
return acc
}, {})
const rangeForMax = semver.validRange(spec)
if (rangeForMax) {
version =
versionsMap[semver.maxSatisfying(Object.keys(versionsMap), rangeForMax)]
} else {
version = versionsMap[maybeCoerced(spec)]
} }
} }
let v = version === null || version === undefined ? null : version return version || null
if (maybePrependWithV0 && v != null) {
v = maybePrependWithV(v)
}
if (!versions.includes(v)) {
v = null
}
return v
} }
function maybeCoerced(v) { function maybeCoerced(v) {
let ret let ret = null
try { try {
ret = semver.coerce(v).version if (!isRC(v)) {
ret = semver.coerce(v).version
} else {
ret = maybeRemoveVPrefix(v)
}
} catch { } catch {
// some stuff can't be coerced, like 'main' // some stuff can't be coerced, like 'main'
ret = v ret = v
@@ -408,11 +438,11 @@ function sortVersions(left, right) {
let ret = 0 let ret = 0
const newL = verAsComparableStr(left) const newL = verAsComparableStr(left)
const newR = verAsComparableStr(right) const newR = verAsComparableStr(right)
function verAsComparableStr(ver) { function verAsComparableStr(ver) {
const matchGroups = 5 const matchGroups = 6
const verSpec = /([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?/ const verSpec = xyzAbcVersion('', '')
const matches = ver.match(verSpec).splice(1, matchGroups) const matches = ver.match(verSpec).splice(1, matchGroups)
return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '') return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '')
} }
@@ -425,6 +455,10 @@ function sortVersions(left, right) {
return ret return ret
} }
function isRC(ver) {
return ver.match(xyzAbcVersion('^', '(?:-rc\\.?\\d+)'))
}
function getRunnerOSVersion() { function getRunnerOSVersion() {
const ImageOSToContainer = { const ImageOSToContainer = {
ubuntu18: 'ubuntu-18.04', ubuntu18: 'ubuntu-18.04',
@@ -434,7 +468,6 @@ function getRunnerOSVersion() {
win22: 'windows-2022', win22: 'windows-2022',
} }
const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS] const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS]
if (!containerFromEnvImageOS) { if (!containerFromEnvImageOS) {
throw new Error( throw new Error(
"Tried to map a target OS from env. variable 'ImageOS' (got " + "Tried to map a target OS from env. variable 'ImageOS' (got " +
@@ -455,7 +488,6 @@ async function get(url0, pageIdxs) {
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)
if (GithubToken && url.host === 'api.github.com') { if (GithubToken && url.host === 'api.github.com') {
headers.authorization = `Bearer ${GithubToken}` headers.authorization = `Bearer ${GithubToken}`
} }
@@ -468,9 +500,7 @@ async function get(url0, pageIdxs) {
allowRetries: true, allowRetries: true,
maxRetries: 3, maxRetries: 3,
}) })
const response = await httpClient.get(url, headers) const response = await httpClient.get(url, headers)
if (response.statusCode >= 400 && response.statusCode <= 599) { if (response.statusCode >= 400 && response.statusCode <= 599) {
throw new Error( throw new Error(
`Got ${response.statusCode} from ${url}. Exiting with error`, `Got ${response.statusCode} from ${url}. Exiting with error`,
@@ -483,6 +513,7 @@ async function get(url0, pageIdxs) {
if (pageIdxs[0] === null) { if (pageIdxs[0] === null) {
return getPage(null) return getPage(null)
} }
return Promise.all(pageIdxs.map(getPage)) return Promise.all(pageIdxs.map(getPage))
} }
@@ -490,12 +521,14 @@ async function getWithMirrors(resourcePath, hexMirrors) {
if (hexMirrors.length === 0) { if (hexMirrors.length === 0) {
throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`) throw new Error(`Could not fetch ${resourcePath} from any hex.pm mirror`)
} }
const [hexMirror, ...hexMirrorsT] = hexMirrors const [hexMirror, ...hexMirrorsT] = hexMirrors
try { try {
return await get(`${hexMirror}${resourcePath}`, [null]) return await get(`${hexMirror}${resourcePath}`, [null])
} catch (err) { } catch (err) {
core.info(`get failed for URL ${hexMirror}${resourcePath}`) core.info(`get failed for URL ${hexMirror}${resourcePath}`)
} }
return getWithMirrors(resourcePath, hexMirrorsT) return getWithMirrors(resourcePath, hexMirrorsT)
} }
@@ -503,11 +536,35 @@ function maybePrependWithV(v) {
if (isVersion(v)) { if (isVersion(v)) {
return `v${v.replace('v', '')}` return `v${v.replace('v', '')}`
} }
return v return v
} }
function maybeRemoveVPrefix(ver) {
let ret = ver
if (isVersion(ver)) {
ret = ver.replace('v', '')
}
return ret
}
function xyzAbcVersion(pref, suf) {
// This accounts for stuff like 6.0.2.0.1.0, as proposed by Erlang's
// https://www.erlang.org/doc/system_principles/versions.html
const dd = '\\.?(\\d+)?'
return new RegExp(
`${pref}v?(\\d+)${dd}${dd}${dd}${dd}${dd}(?:-rc\\.?\\d+)?(?:-otp-\\d+)?${suf}`,
)
}
function versionAbc(ver) {
// For a version like 6.0.2.0.1.0, return 0.1.0
return ver.match(/\d+(?:\.[^.]+)?(?:\.[^.]+)?(?:\.)?(.*)/)[1]
}
function isVersion(v) { function isVersion(v) {
return /^v?\d+/.test(v) return v.match(xyzAbcVersion('^', '$')) !== null
} }
function getInput(inputName, required, alternativeName, alternatives) { function getInput(inputName, required, alternativeName, alternatives) {
@@ -525,6 +582,7 @@ alongside ${alternativeName}=${alternativeValue} \
} else if (!input) { } else if (!input) {
input = alternativeValue input = alternativeValue
} }
return input return input
} }
@@ -538,6 +596,7 @@ function parseVersionFile(versionFilePath0) {
`The specified version file, ${versionFilePath0}, does not exist`, `The specified version file, ${versionFilePath0}, does not exist`,
) )
} }
core.startGroup(`Parsing version file at ${versionFilePath0}`) core.startGroup(`Parsing version file at ${versionFilePath0}`)
const appVersions = new Map() const appVersions = new Map()
const versions = fs.readFileSync(versionFilePath, 'utf8') const versions = fs.readFileSync(versionFilePath, 'utf8')
@@ -571,6 +630,7 @@ function jsonParseAsList(maybeJson) {
if (!Array.isArray(obj)) { if (!Array.isArray(obj)) {
throw new Error('expected a list!') throw new Error('expected a list!')
} }
return obj return obj
} catch (exc) { } catch (exc) {
throw new Error( throw new Error(
+52 -2
View File
@@ -104,9 +104,18 @@ async function testOTPVersions() {
let expected let expected
let spec let spec
let osVersion let osVersion
let before
const hexMirrors = ['https://repo.hex.pm', 'https://cdn.jsdelivr.net/hex'] const hexMirrors = ['https://repo.hex.pm', 'https://cdn.jsdelivr.net/hex']
if (process.platform === 'linux') { if (process.platform === 'linux') {
before = simulateInput('version-type', 'strict')
spec = '25.3.2.1'
osVersion = 'ubuntu-20.04'
expected = 'OTP-25.3.2.1'
got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors)
assert.deepStrictEqual(got, expected)
simulateInput('version-type', before)
spec = '19.3.x' spec = '19.3.x'
osVersion = 'ubuntu-16.04' osVersion = 'ubuntu-16.04'
expected = 'OTP-19.3.6.13' expected = 'OTP-19.3.6.13'
@@ -131,6 +140,12 @@ async function testOTPVersions() {
got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors) got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors)
assert.deepStrictEqual(got, expected) assert.deepStrictEqual(got, expected)
spec = '20.3.8.26'
osVersion = 'ubuntu-20.04'
expected = 'OTP-20.3.8.26'
got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors)
assert.deepStrictEqual(got, expected)
spec = '20.x' spec = '20.x'
osVersion = 'ubuntu-20.04' osVersion = 'ubuntu-20.04'
expected = 'OTP-20.3.8.26' expected = 'OTP-20.3.8.26'
@@ -291,7 +306,7 @@ async function testGetVersionFromSpec() {
let expected let expected
let spec let spec
let before let before
const versions = [ const versions0 = [
'3.2.30.5', '3.2.30.5',
'3.2.3.5', '3.2.3.5',
'1', '1',
@@ -317,10 +332,20 @@ async function testGetVersionFromSpec() {
'22.3.4.9.1', '22.3.4.9.1',
'22.3.4.12.1', '22.3.4.12.1',
'22.3.4.10.1', '22.3.4.10.1',
'22.3.4.2',
'main', 'main',
'v11.11.0-rc.0-otp-23', 'v11.11.0-rc.0-otp-23',
'22.3.4.2', '22.3.4.2.1.0',
'12.1.2.2',
'12.1.2.4',
'12.1.2.0',
'12.1.2.3',
'12.1.2.3.0',
] ]
const versions = {}
versions0.forEach((version) => {
versions[version] = version
})
spec = '1' spec = '1'
expected = '1.1.0' expected = '1.1.0'
@@ -417,6 +442,31 @@ async function testGetVersionFromSpec() {
assert.deepStrictEqual(got, expected) assert.deepStrictEqual(got, expected)
simulateInput('version-type', before) simulateInput('version-type', before)
spec = '22.3.4.2'
expected = '22.3.4.2.1.0'
got = setupBeam.getVersionFromSpec(spec, versions)
assert.deepStrictEqual(got, expected)
spec = '22.3.4.2.1'
expected = '22.3.4.2.1.0'
got = setupBeam.getVersionFromSpec(spec, versions)
assert.deepStrictEqual(got, expected)
spec = '22.3.4.2.1.0'
expected = '22.3.4.2.1.0'
got = setupBeam.getVersionFromSpec(spec, versions)
assert.deepStrictEqual(got, expected)
spec = '12.1.2.3'
expected = '12.1.2.3.0'
got = setupBeam.getVersionFromSpec(spec, versions)
assert.deepStrictEqual(got, expected)
spec = '22.3.4.2.1.0.1'
expected = null
got = setupBeam.getVersionFromSpec(spec, versions)
assert.deepStrictEqual(got, expected)
before = simulateInput('version-type', 'strict') before = simulateInput('version-type', 'strict')
spec = '22.3.4.3' spec = '22.3.4.3'
expected = null expected = null