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:
node: true
es2021: true
es2022: true
extends:
- airbnb
- eslint:recommended
parserOptions:
ecmaVersion: 12
ecmaVersion: 2022
rules:
indent: ["warn", 2]
max-len: ["warn", 100]
no-undef: "error"
no-unused-vars: ["error", {"varsIgnorePattern": "^_"}]
indent: [warn, 2]
max-len: [warn, 100]
no-use-before-define: 0
operator-linebreak: 0
require-jsdoc: 0
semi: 0
settings:
+6 -13
View File
@@ -6,7 +6,8 @@ on:
branches:
- main
pull_request:
types: [opened, synchronize]
branches:
- main
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -19,16 +20,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- run: npm install -g npm
- 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
node-version: '16'
- run: npm run build-dist
- name: Check if build left artifacts
run: git diff --exit-code --ignore-space-at-eol
@@ -39,7 +32,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '16'
- run: npm ci
- run: npm test
- name: .tool-versions test
@@ -56,6 +49,6 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
node-version: '16'
- run: npm install --production
- run: npm test
+3 -4
View File
@@ -6,7 +6,8 @@ on:
branches:
- main
pull_request:
types: [opened, synchronize]
branches:
- main
jobs:
integration_test:
@@ -111,12 +112,10 @@ jobs:
otp-version: '22.3.4.1'
os: 'ubuntu-20.04'
version-type: 'strict'
- elixir-version: '1.10.3'
otp-version: '22.3.4.1'
os: 'ubuntu-20.04'
- elixir-version: 'main'
otp-version: '23.1'
os: 'ubuntu-20.04'
version-type: 'strict'
- elixir-version: 'main'
otp-version: '25'
os: 'ubuntu-20.04'
@@ -12,9 +12,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '14'
- run:
./.github/workflows/update_3rd_party_licenses.sh
env:
+2 -1
View File
@@ -6,7 +6,8 @@ on:
branches:
- main
pull_request:
types: [opened, synchronize]
branches:
- main
jobs:
integration_test:
+173 -90
View File
@@ -6221,15 +6221,18 @@ class Range {
const hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE]
range = range.replace(hr, hyphenReplace(this.options.includePrerelease))
debug('hyphen replace', range)
// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace)
debug('comparator trim', range)
// `~ 1.2.3` => `~1.2.3`
range = range.replace(re[t.TILDETRIM], tildeTrimReplace)
debug('tilde trim', range)
// `^ 1.2.3` => `^1.2.3`
range = range.replace(re[t.CARETTRIM], caretTrimReplace)
debug('caret trim', range)
// At this point, the range is completely trimmed and
// ready to be split into comparators.
@@ -7531,6 +7534,10 @@ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
// Max safe segment length for coercion.
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 = [
'major',
'premajor',
@@ -7544,6 +7551,7 @@ const RELEASE_TYPES = [
module.exports = {
MAX_LENGTH,
MAX_SAFE_COMPONENT_LENGTH,
MAX_SAFE_BUILD_LENGTH,
MAX_SAFE_INTEGER,
RELEASE_TYPES,
SEMVER_SPEC_VERSION,
@@ -7625,7 +7633,7 @@ module.exports = parseOptions
/***/ 9523:
/***/ ((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)
exports = module.exports = {}
@@ -7636,16 +7644,31 @@ const src = exports.src = []
const t = exports.t = {}
let R = 0
const createToken = (name, value, isGlobal) => {
// Replace all greedy whitespace to prevent regex dos issues. These regex are
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 safe = value
.split('\\s*').join('\\s{0,1}')
.split('\\s+').join('\\s')
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 safe = makeSafeRegex(value)
const index = R++
debug(name, index, value)
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.
createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*')
createToken('NUMERICIDENTIFIERLOOSE', '[0-9]+')
createToken('NUMERICIDENTIFIERLOOSE', '\\d+')
// ## Non-numeric Identifier
// Zero or more digits, followed by a letter or hyphen, and then zero or
// more letters, digits, or hyphens.
createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*')
createToken('NONNUMERICIDENTIFIER', `\\d*[a-zA-Z-]${LETTERDASHNUMBER}*`)
// ## Main Version
// Three dot-separated numeric identifiers.
@@ -7702,7 +7725,7 @@ createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE]
// ## Build Metadata Identifier
// Any combination of digits, letters, or hyphens.
createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+')
createToken('BUILDIDENTIFIER', `${LETTERDASHNUMBER}+`)
// ## Build Metadata
// Plus sign, followed by one or more period-separated build metadata
@@ -10052,15 +10075,16 @@ async function main() {
if (otpSpec !== 'false') {
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', hexMirrors)
const shouldMixHex = getInput('install-hex', false)
await mix(shouldMixHex, 'hex', hexMirrors)
}
@@ -10084,7 +10108,6 @@ async function installOTP(otpSpec, osVersion, hexMirrors) {
async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
let installed = false
if (elixirSpec) {
const elixirVersion = await getElixirVersion(
elixirSpec,
@@ -10094,6 +10117,7 @@ async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
core.startGroup(`Installing Elixir ${elixirVersion}`)
await installer.installElixir(elixirVersion, hexMirrors)
core.setOutput('elixir-version', elixirVersion)
const disableProblemMatchers = getInput('disable_problem_matchers', false)
if (disableProblemMatchers === 'false') {
const elixirMatchers = __nccwpck_require__.ab + "elixir-matchers.json"
@@ -10111,6 +10135,7 @@ 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 {
@@ -10120,6 +10145,7 @@ async function mixWithMirrors(cmd, args, hexMirrors) {
`mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`,
)
}
return mixWithMirrors(cmd, args, hexMirrorsT)
}
@@ -10135,7 +10161,6 @@ async function mix(shouldMix, what, hexMirrors) {
async function maybeInstallGleam(gleamSpec) {
let installed = false
if (gleamSpec) {
const gleamVersion = await getGleamVersion(gleamSpec)
core.startGroup(`Installing Gleam ${gleamVersion}`)
@@ -10153,7 +10178,6 @@ async function maybeInstallGleam(gleamSpec) {
async function maybeInstallRebar3(rebar3Spec) {
let installed = false
let rebar3Version
if (rebar3Spec) {
if (rebar3Spec === 'nightly') {
rebar3Version = 'nightly'
@@ -10174,14 +10198,9 @@ async function maybeInstallRebar3(rebar3Spec) {
async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
const otpVersions = await getOTPVersions(osVersion, hexMirrors)
let otpSpec = otpSpec0 // might be a branch (?)
const otpVersion = getVersionFromSpec(
otpSpec,
Array.from(otpVersions.keys()).sort(),
)
if (isVersion(otpSpec0)) {
otpSpec = `OTP-${otpSpec0}` // ... it's a version!
}
const spec = otpSpec0.replace(/^OTP-/, '')
const versions = otpVersions
const otpVersion = getVersionFromSpec(spec, versions)
if (otpVersion === null) {
throw new Error(
`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) {
const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2]
const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1]
const elixirVersions = await getElixirVersions(hexMirrors)
const semverVersions = Array.from(elixirVersions.keys()).sort()
const exSpec = exSpec0.replace(/-otp-.*$/, '')
const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true)
let elixirVersionForDownload = elixirVersionFromSpec
if (isVersion(otpVersionMajor)) {
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
}
const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions(
hexMirrors,
)
const spec = exSpec0.replace(/-otp-.*$/, '')
const versions = elixirVersions
const elixirVersionFromSpec = getVersionFromSpec(spec, versions)
if (elixirVersionFromSpec === null) {
throw new Error(
`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 (
(elixirVersionComp && elixirVersionComp.includes(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)
}
async function getGleamVersion(gleamSpec0) {
const gleamSpec = gleamSpec0.match(/^v?(.+)$/)
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) {
throw new Error(
`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) {
const rebar3Versions = await getRebar3Versions()
const rebar3Version = getVersionFromSpec(r3Spec, rebar3Versions)
const spec = r3Spec
const versions = rebar3Versions
const rebar3Version = getVersionFromSpec(spec, versions)
if (rebar3Version === null) {
throw new Error(
`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])
}
const otpVersions = new Map()
debugLog(`OTP versions listings from ${originListing}`, otpVersionsListings)
const otpVersions = {}
if (process.platform === 'linux') {
otpVersionsListings
.trim()
@@ -10280,8 +10307,9 @@ async function getOTPVersions(osVersion, hexMirrors) {
.match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
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') {
otpVersionsListings.forEach((otpVersionsListing) => {
@@ -10293,15 +10321,12 @@ async function getOTPVersions(osVersion, hexMirrors) {
const otpMatch = x.name.match(/^otp_win64_(.*).exe$/)
const otpVersion = otpMatch[1]
debugLog('OTP line and parsing', [otpMatch, otpVersion])
otpVersions.set(otpVersion, otpVersion)
otpVersions[otpVersion] = otpVersion
})
})
}
debugLog(
`OTP versions from ${originListing}`,
JSON.stringify([...otpVersions.entries()]),
)
debugLog(`OTP versions from ${originListing}`, JSON.stringify(otpVersions))
return otpVersions
}
@@ -10311,7 +10336,8 @@ async function getElixirVersions(hexMirrors) {
'/builds/elixir/builds.txt',
hexMirrors,
)
const otpVersionsForElixirMap = new Map()
const otpVersionsForElixirMap = {}
const elixirVersions = {}
elixirVersionsListings
.trim()
@@ -10319,17 +10345,18 @@ async function getElixirVersions(hexMirrors) {
.forEach((line) => {
const elixirMatch =
line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/)
const elixirVersion = maybePrependWithV(elixirMatch[1])
const elixirVersion = elixirMatch[1]
const otpVersion = elixirMatch[2]
const otpVersions = otpVersionsForElixirMap.get(elixirVersion) || []
const otpVersions = otpVersionsForElixirMap[elixirVersion] || []
if (otpVersion) {
// -otp- present (special case)
otpVersions.push(otpVersion)
}
otpVersionsForElixirMap.set(elixirVersion, otpVersions)
otpVersionsForElixirMap[elixirVersion] = otpVersions
elixirVersions[elixirVersion] = elixirVersion
})
return otpVersionsForElixirMap
return [otpVersionsForElixirMap, elixirVersions]
}
async function getGleamVersions() {
@@ -10337,13 +10364,17 @@ async function getGleamVersions() {
'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100',
[1, 2, 3],
)
const gleamVersionsListing = []
const gleamVersionsListing = {}
resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON)
.map((x) => x.tag_name)
.sort()
.forEach((v) => gleamVersionsListing.push(v))
.forEach((ver) => {
const gleamMatch = ver.match(/^v?([^ ]+)/)
const gleamVersion = gleamMatch[1]
gleamVersionsListing[gleamVersion] = gleamVersion
})
})
return gleamVersionsListing
}
@@ -10352,13 +10383,15 @@ async function getRebar3Versions() {
'https://api.github.com/repos/erlang/rebar3/releases?per_page=100',
[1, 2, 3],
)
const rebar3VersionsListing = []
const rebar3VersionsListing = {}
resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON)
.map((x) => x.tag_name)
.sort()
.forEach((v) => rebar3VersionsListing.push(v))
.forEach((ver) => {
rebar3VersionsListing[ver] = ver
})
})
return rebar3VersionsListing
}
@@ -10366,48 +10399,68 @@ function isStrictVersion() {
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
if (spec.match(/rc/) || isStrictVersion()) {
version = spec
if (isStrictVersion() || isRC(spec0)) {
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) {
// We keep a map of semver => "spec" in order to use semver ranges to find appropriate versions
const versionsMap = versions.sort(sortVersions).reduce((acc, v) => {
if (!v.match(/rc/)) {
// release candidates are opt-in
acc[maybeCoerced(v)] = v
versions = altVersions[rangeMax]
for (let i = versions.length - 1; i >= 0; i -= 1) {
const thisVersion = versions[i]
const thisVersionAbc = versionAbc(thisVersion)
const thisVersionAbcRange = semver.validRange(thisVersionAbc)
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
if (maybePrependWithV0 && v != null) {
v = maybePrependWithV(v)
}
if (!versions.includes(v)) {
v = null
}
return v
return version || null
}
function maybeCoerced(v) {
let ret
let ret = null
try {
if (!isRC(v)) {
ret = semver.coerce(v).version
} else {
ret = maybeRemoveVPrefix(v)
}
} catch {
// some stuff can't be coerced, like 'main'
ret = v
@@ -10420,11 +10473,11 @@ function sortVersions(left, right) {
let ret = 0
const newL = verAsComparableStr(left)
const newR = verAsComparableStr(right)
function verAsComparableStr(ver) {
const matchGroups = 5
const verSpec = /([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?/
const matchGroups = 6
const verSpec = xyzAbcVersion('', '')
const matches = ver.match(verSpec).splice(1, matchGroups)
return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '')
}
@@ -10437,6 +10490,10 @@ function sortVersions(left, right) {
return ret
}
function isRC(ver) {
return ver.match(xyzAbcVersion('^', '(?:-rc\\.?\\d+)'))
}
function getRunnerOSVersion() {
const ImageOSToContainer = {
ubuntu18: 'ubuntu-18.04',
@@ -10446,7 +10503,6 @@ function getRunnerOSVersion() {
win22: 'windows-2022',
}
const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS]
if (!containerFromEnvImageOS) {
throw new Error(
"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 headers = {}
const GithubToken = getInput('github-token', false)
if (GithubToken && url.host === 'api.github.com') {
headers.authorization = `Bearer ${GithubToken}`
}
@@ -10480,9 +10535,7 @@ async function get(url0, pageIdxs) {
allowRetries: true,
maxRetries: 3,
})
const response = await httpClient.get(url, headers)
if (response.statusCode >= 400 && response.statusCode <= 599) {
throw new Error(
`Got ${response.statusCode} from ${url}. Exiting with error`,
@@ -10495,6 +10548,7 @@ async function get(url0, pageIdxs) {
if (pageIdxs[0] === null) {
return getPage(null)
}
return Promise.all(pageIdxs.map(getPage))
}
@@ -10502,12 +10556,14 @@ 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)
}
@@ -10515,11 +10571,35 @@ function maybePrependWithV(v) {
if (isVersion(v)) {
return `v${v.replace('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) {
return /^v?\d+/.test(v)
return v.match(xyzAbcVersion('^', '$')) !== null
}
function getInput(inputName, required, alternativeName, alternatives) {
@@ -10537,6 +10617,7 @@ alongside ${alternativeName}=${alternativeValue} \
} else if (!input) {
input = alternativeValue
}
return input
}
@@ -10550,6 +10631,7 @@ function parseVersionFile(versionFilePath0) {
`The specified version file, ${versionFilePath0}, does not exist`,
)
}
core.startGroup(`Parsing version file at ${versionFilePath0}`)
const appVersions = new Map()
const versions = fs.readFileSync(versionFilePath, 'utf8')
@@ -10583,6 +10665,7 @@ function jsonParseAsList(maybeJson) {
if (!Array.isArray(obj)) {
throw new Error('expected a list!')
}
return obj
} catch (exc) {
throw new Error(
+968 -3411
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",
"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": {
"@actions/core": "1.10.0",
"@actions/exec": "1.1.1",
"@actions/http-client": "2.1.0",
"@actions/tool-cache": "^2.0.1",
"semver": "7.5.2"
"@actions/tool-cache": "2.0.1",
"semver": "7.5.3"
},
"devDependencies": {
"@vercel/ncc": "0.34.0",
"eslint": "8.25.0",
"eslint-config-airbnb": "19.0.4",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.10",
"husky": "8.0.1",
"markdownlint": "0.26.2",
"prettier": "2.7.1",
"shellcheck": "1.1.0",
"@vercel/ncc": "0.36.1",
"eslint": "8.43.0",
"markdownlint-cli": "0.35.0",
"prettier": "2.8.8",
"shellcheck": "2.2.0",
"yaml-lint": "1.7.0",
"yarn": "1.22.19"
},
+141 -81
View File
@@ -35,15 +35,16 @@ async function main() {
if (otpSpec !== 'false') {
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', hexMirrors)
const shouldMixHex = getInput('install-hex', false)
await mix(shouldMixHex, 'hex', hexMirrors)
}
@@ -67,7 +68,6 @@ async function installOTP(otpSpec, osVersion, hexMirrors) {
async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
let installed = false
if (elixirSpec) {
const elixirVersion = await getElixirVersion(
elixirSpec,
@@ -77,6 +77,7 @@ async function maybeInstallElixir(elixirSpec, otpSpec, hexMirrors) {
core.startGroup(`Installing Elixir ${elixirVersion}`)
await installer.installElixir(elixirVersion, hexMirrors)
core.setOutput('elixir-version', elixirVersion)
const disableProblemMatchers = getInput('disable_problem_matchers', false)
if (disableProblemMatchers === 'false') {
const elixirMatchers = path.join(
@@ -99,6 +100,7 @@ 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 {
@@ -108,6 +110,7 @@ async function mixWithMirrors(cmd, args, hexMirrors) {
`mix failed with mirror ${process.env.HEX_MIRROR} with message ${err.message})`,
)
}
return mixWithMirrors(cmd, args, hexMirrorsT)
}
@@ -123,7 +126,6 @@ async function mix(shouldMix, what, hexMirrors) {
async function maybeInstallGleam(gleamSpec) {
let installed = false
if (gleamSpec) {
const gleamVersion = await getGleamVersion(gleamSpec)
core.startGroup(`Installing Gleam ${gleamVersion}`)
@@ -141,7 +143,6 @@ async function maybeInstallGleam(gleamSpec) {
async function maybeInstallRebar3(rebar3Spec) {
let installed = false
let rebar3Version
if (rebar3Spec) {
if (rebar3Spec === 'nightly') {
rebar3Version = 'nightly'
@@ -162,14 +163,9 @@ async function maybeInstallRebar3(rebar3Spec) {
async function getOTPVersion(otpSpec0, osVersion, hexMirrors) {
const otpVersions = await getOTPVersions(osVersion, hexMirrors)
let otpSpec = otpSpec0 // might be a branch (?)
const otpVersion = getVersionFromSpec(
otpSpec,
Array.from(otpVersions.keys()).sort(),
)
if (isVersion(otpSpec0)) {
otpSpec = `OTP-${otpSpec0}` // ... it's a version!
}
const spec = otpSpec0.replace(/^OTP-/, '')
const versions = otpVersions
const otpVersion = getVersionFromSpec(spec, versions)
if (otpVersion === null) {
throw new Error(
`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) {
const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2]
const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1]
const elixirVersions = await getElixirVersions(hexMirrors)
const semverVersions = Array.from(elixirVersions.keys()).sort()
const exSpec = exSpec0.replace(/-otp-.*$/, '')
const elixirVersionFromSpec = getVersionFromSpec(exSpec, semverVersions, true)
let elixirVersionForDownload = elixirVersionFromSpec
if (isVersion(otpVersionMajor)) {
elixirVersionForDownload = `${elixirVersionFromSpec}-otp-${otpVersionMajor}`
}
const [otpVersionsForElixirMap, elixirVersions] = await getElixirVersions(
hexMirrors,
)
const spec = exSpec0.replace(/-otp-.*$/, '')
const versions = elixirVersions
const elixirVersionFromSpec = getVersionFromSpec(spec, versions)
if (elixirVersionFromSpec === null) {
throw new Error(
`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 (
(elixirVersionComp && elixirVersionComp.includes(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)
}
async function getGleamVersion(gleamSpec0) {
const gleamSpec = gleamSpec0.match(/^v?(.+)$/)
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) {
throw new Error(
`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) {
const rebar3Versions = await getRebar3Versions()
const rebar3Version = getVersionFromSpec(r3Spec, rebar3Versions)
const spec = r3Spec
const versions = rebar3Versions
const rebar3Version = getVersionFromSpec(spec, versions)
if (rebar3Version === null) {
throw new Error(
`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])
}
const otpVersions = new Map()
debugLog(`OTP versions listings from ${originListing}`, otpVersionsListings)
const otpVersions = {}
if (process.platform === 'linux') {
otpVersionsListings
.trim()
@@ -268,8 +272,9 @@ async function getOTPVersions(osVersion, hexMirrors) {
.match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
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') {
otpVersionsListings.forEach((otpVersionsListing) => {
@@ -281,15 +286,12 @@ async function getOTPVersions(osVersion, hexMirrors) {
const otpMatch = x.name.match(/^otp_win64_(.*).exe$/)
const otpVersion = otpMatch[1]
debugLog('OTP line and parsing', [otpMatch, otpVersion])
otpVersions.set(otpVersion, otpVersion)
otpVersions[otpVersion] = otpVersion
})
})
}
debugLog(
`OTP versions from ${originListing}`,
JSON.stringify([...otpVersions.entries()]),
)
debugLog(`OTP versions from ${originListing}`, JSON.stringify(otpVersions))
return otpVersions
}
@@ -299,7 +301,8 @@ async function getElixirVersions(hexMirrors) {
'/builds/elixir/builds.txt',
hexMirrors,
)
const otpVersionsForElixirMap = new Map()
const otpVersionsForElixirMap = {}
const elixirVersions = {}
elixirVersionsListings
.trim()
@@ -307,17 +310,18 @@ async function getElixirVersions(hexMirrors) {
.forEach((line) => {
const elixirMatch =
line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/)
const elixirVersion = maybePrependWithV(elixirMatch[1])
const elixirVersion = elixirMatch[1]
const otpVersion = elixirMatch[2]
const otpVersions = otpVersionsForElixirMap.get(elixirVersion) || []
const otpVersions = otpVersionsForElixirMap[elixirVersion] || []
if (otpVersion) {
// -otp- present (special case)
otpVersions.push(otpVersion)
}
otpVersionsForElixirMap.set(elixirVersion, otpVersions)
otpVersionsForElixirMap[elixirVersion] = otpVersions
elixirVersions[elixirVersion] = elixirVersion
})
return otpVersionsForElixirMap
return [otpVersionsForElixirMap, elixirVersions]
}
async function getGleamVersions() {
@@ -325,13 +329,17 @@ async function getGleamVersions() {
'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100',
[1, 2, 3],
)
const gleamVersionsListing = []
const gleamVersionsListing = {}
resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON)
.map((x) => x.tag_name)
.sort()
.forEach((v) => gleamVersionsListing.push(v))
.forEach((ver) => {
const gleamMatch = ver.match(/^v?([^ ]+)/)
const gleamVersion = gleamMatch[1]
gleamVersionsListing[gleamVersion] = gleamVersion
})
})
return gleamVersionsListing
}
@@ -340,13 +348,15 @@ async function getRebar3Versions() {
'https://api.github.com/repos/erlang/rebar3/releases?per_page=100',
[1, 2, 3],
)
const rebar3VersionsListing = []
const rebar3VersionsListing = {}
resultJSONs.forEach((resultJSON) => {
jsonParseAsList(resultJSON)
.map((x) => x.tag_name)
.sort()
.forEach((v) => rebar3VersionsListing.push(v))
.forEach((ver) => {
rebar3VersionsListing[ver] = ver
})
})
return rebar3VersionsListing
}
@@ -354,48 +364,68 @@ function isStrictVersion() {
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
if (spec.match(/rc/) || isStrictVersion()) {
version = spec
if (isStrictVersion() || isRC(spec0)) {
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) {
// We keep a map of semver => "spec" in order to use semver ranges to find appropriate versions
const versionsMap = versions.sort(sortVersions).reduce((acc, v) => {
if (!v.match(/rc/)) {
// release candidates are opt-in
acc[maybeCoerced(v)] = v
versions = altVersions[rangeMax]
for (let i = versions.length - 1; i >= 0; i -= 1) {
const thisVersion = versions[i]
const thisVersionAbc = versionAbc(thisVersion)
const thisVersionAbcRange = semver.validRange(thisVersionAbc)
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
if (maybePrependWithV0 && v != null) {
v = maybePrependWithV(v)
}
if (!versions.includes(v)) {
v = null
}
return v
return version || null
}
function maybeCoerced(v) {
let ret
let ret = null
try {
if (!isRC(v)) {
ret = semver.coerce(v).version
} else {
ret = maybeRemoveVPrefix(v)
}
} catch {
// some stuff can't be coerced, like 'main'
ret = v
@@ -408,11 +438,11 @@ function sortVersions(left, right) {
let ret = 0
const newL = verAsComparableStr(left)
const newR = verAsComparableStr(right)
function verAsComparableStr(ver) {
const matchGroups = 5
const verSpec = /([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?/
const matchGroups = 6
const verSpec = xyzAbcVersion('', '')
const matches = ver.match(verSpec).splice(1, matchGroups)
return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '')
}
@@ -425,6 +455,10 @@ function sortVersions(left, right) {
return ret
}
function isRC(ver) {
return ver.match(xyzAbcVersion('^', '(?:-rc\\.?\\d+)'))
}
function getRunnerOSVersion() {
const ImageOSToContainer = {
ubuntu18: 'ubuntu-18.04',
@@ -434,7 +468,6 @@ function getRunnerOSVersion() {
win22: 'windows-2022',
}
const containerFromEnvImageOS = ImageOSToContainer[process.env.ImageOS]
if (!containerFromEnvImageOS) {
throw new Error(
"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 headers = {}
const GithubToken = getInput('github-token', false)
if (GithubToken && url.host === 'api.github.com') {
headers.authorization = `Bearer ${GithubToken}`
}
@@ -468,9 +500,7 @@ async function get(url0, pageIdxs) {
allowRetries: true,
maxRetries: 3,
})
const response = await httpClient.get(url, headers)
if (response.statusCode >= 400 && response.statusCode <= 599) {
throw new Error(
`Got ${response.statusCode} from ${url}. Exiting with error`,
@@ -483,6 +513,7 @@ async function get(url0, pageIdxs) {
if (pageIdxs[0] === null) {
return getPage(null)
}
return Promise.all(pageIdxs.map(getPage))
}
@@ -490,12 +521,14 @@ 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)
}
@@ -503,11 +536,35 @@ function maybePrependWithV(v) {
if (isVersion(v)) {
return `v${v.replace('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) {
return /^v?\d+/.test(v)
return v.match(xyzAbcVersion('^', '$')) !== null
}
function getInput(inputName, required, alternativeName, alternatives) {
@@ -525,6 +582,7 @@ alongside ${alternativeName}=${alternativeValue} \
} else if (!input) {
input = alternativeValue
}
return input
}
@@ -538,6 +596,7 @@ function parseVersionFile(versionFilePath0) {
`The specified version file, ${versionFilePath0}, does not exist`,
)
}
core.startGroup(`Parsing version file at ${versionFilePath0}`)
const appVersions = new Map()
const versions = fs.readFileSync(versionFilePath, 'utf8')
@@ -571,6 +630,7 @@ function jsonParseAsList(maybeJson) {
if (!Array.isArray(obj)) {
throw new Error('expected a list!')
}
return obj
} catch (exc) {
throw new Error(
+52 -2
View File
@@ -104,9 +104,18 @@ async function testOTPVersions() {
let expected
let spec
let osVersion
let before
const hexMirrors = ['https://repo.hex.pm', 'https://cdn.jsdelivr.net/hex']
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'
osVersion = 'ubuntu-16.04'
expected = 'OTP-19.3.6.13'
@@ -131,6 +140,12 @@ async function testOTPVersions() {
got = await setupBeam.getOTPVersion(spec, osVersion, hexMirrors)
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'
osVersion = 'ubuntu-20.04'
expected = 'OTP-20.3.8.26'
@@ -291,7 +306,7 @@ async function testGetVersionFromSpec() {
let expected
let spec
let before
const versions = [
const versions0 = [
'3.2.30.5',
'3.2.3.5',
'1',
@@ -317,10 +332,20 @@ async function testGetVersionFromSpec() {
'22.3.4.9.1',
'22.3.4.12.1',
'22.3.4.10.1',
'22.3.4.2',
'main',
'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'
expected = '1.1.0'
@@ -417,6 +442,31 @@ async function testGetVersionFromSpec() {
assert.deepStrictEqual(got, expected)
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')
spec = '22.3.4.3'
expected = null