Compare commits

...

9 Commits

Author SHA1 Message Date
GitHub Actions e6d7c94229 Automation: update setup-beam version output to fceaea9 2025-07-10 15:21:07 +00:00
Paulo F. Oliveira fceaea9f54 Fix: more versioning around nightly and maint/main (#359)
* Test Elixir main-otp-28 / Erlang maint-28

* Test Gleam nightly / OTP 28

* Tweak versioning

1. stop trying to "coerce" versions on input
2. handle `OTP-` in one more place
3. handle `nightly`

* Remove tests that should be failing previously

There is no strict 27 for macos-15, for example

* Remove similar tests

We'll test once per platform in similar conditions

* `await` for `assert.rejects` (not noticeable if alone inside `it`)

* Test maint- for strict and loose
2025-07-10 16:20:49 +01:00
GitHub Actions 2bb5b653d0 Automation: update setup-beam version output to 1d4efdd 2025-07-10 15:18:41 +00:00
dependabot[bot] 1d4efdd2e9 Bump globals from 16.2.0 to 16.3.0 (#361)
Bumps [globals](https://github.com/sindresorhus/globals) from 16.2.0 to 16.3.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.2.0...v16.3.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 16:18:18 +01:00
GitHub Actions 73f047e328 Automation: update setup-beam version output to 6dd8a1a 2025-07-10 09:38:56 +00:00
dependabot[bot] 6dd8a1a852 Bump @eslint/js from 9.30.0 to 9.30.1 (#360)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.30.0 to 9.30.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.30.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.30.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 10:38:37 +01:00
GitHub Actions fd9a2e183f Automation: update setup-beam version output to 65085e3 2025-07-10 09:14:46 +00:00
dependabot[bot] 65085e37bc Bump eslint from 9.30.0 to 9.30.1 (#362)
Bumps [eslint](https://github.com/eslint/eslint) from 9.30.0 to 9.30.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.30.0...v9.30.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.30.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 10:14:28 +01:00
GitHub Actions 80c1659516 Automation: update setup-beam version output to 8e7fdef 2025-07-03 18:12:11 +00:00
7 changed files with 138 additions and 109 deletions
-6
View File
@@ -43,12 +43,6 @@ jobs:
elixir-version: '1.14.3'
os: 'macos-latest'
version-type: 'strict'
- otp-version: '27'
os: 'macos-15'
version-type: 'strict'
- otp-version: '26'
os: 'macos-15'
version-type: 'strict'
- otp-version: '25'
os: 'macos-15'
- otp-version: '26'
-6
View File
@@ -53,12 +53,6 @@ jobs:
elixir-version: '1.14.3'
os: 'ubuntu-latest'
version-type: 'strict'
- otp-version: '27'
os: 'ubuntu-24.04'
version-type: 'strict'
- otp-version: '26'
os: 'ubuntu-24.04'
version-type: 'strict'
- otp-version: '25'
os: 'ubuntu-24.04'
- otp-version: '26'
+23 -21
View File
@@ -25933,7 +25933,7 @@ async function main() {
await maybeInstallRebar3(rebar3Spec)
// undefined is replaced by a function, post- main branch merge
const setupBeamVersion = '1fe9179'
const setupBeamVersion = 'fceaea9'
core.setOutput('setup-beam-version', setupBeamVersion)
}
@@ -26045,10 +26045,14 @@ async function maybeInstallRebar3(rebar3Spec) {
return installed
}
function maybeRemoveOTPPrefix(otpSpec) {
return otpSpec.replace(/^OTP-/, '')
}
async function getOTPVersion(otpSpec0, osVersion) {
const [otpVersions, originListing, hexMirrors] =
await getOTPVersions(osVersion)
let spec = otpSpec0.replace(/^OTP-/, '')
let spec = maybeRemoveOTPPrefix(otpSpec0)
const versions = otpVersions
const otpVersion = getVersionFromSpec(spec, versions)
@@ -26224,12 +26228,9 @@ async function getOTPVersions(osVersion) {
.trim()
.split('\n')
.forEach((line) => {
const otpMatch = line
.match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
debugLog('OTP line and parsing', [line, otpVersion, otpMatch])
const otpVersionOrig = line.match(/^([^ ]+)?( .+)/)[1]
const otpVersion = maybeRemoveOTPPrefix(otpVersionOrig)
debugLog('OTP line and parsing', [line, otpVersion, otpVersionOrig])
otpVersions[otpVersion] = otpVersionOrig // we keep the original for later reference
})
} else if (process.platform === 'win32') {
@@ -26243,10 +26244,10 @@ async function getOTPVersions(osVersion) {
.flat()
.filter((x) => x.name.match(file_regex))
.forEach((x) => {
const otpMatch = x.name.match(file_regex)
const otpVersion = otpMatch[1]
debugLog('OTP line and parsing', [otpMatch, otpVersion])
otpVersions[otpVersion] = otpVersion
const otpVersionOrig = x.name.match(file_regex)[1]
const otpVersion = otpVersionOrig
debugLog('OTP line and parsing', [x.name, otpVersion, otpVersionOrig])
otpVersions[otpVersion] = otpVersionOrig
})
})
} else if (process.platform === 'darwin') {
@@ -26255,10 +26256,9 @@ async function getOTPVersions(osVersion) {
columns: true,
})
.forEach((line) => {
const otpMatch = line.ref_name.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
debugLog('OTP line and parsing', [line, otpVersion, otpMatch])
const otpVersionOrig = line.ref_name
const otpVersion = maybeRemoveOTPPrefix(otpVersionOrig)
debugLog('OTP line and parsing', [line, otpVersion, otpVersionOrig])
otpVersions[otpVersion] = otpVersionOrig // we keep the original for later reference
})
}
@@ -26384,7 +26384,8 @@ function getVersionFromSpec(spec0, versions0) {
isKnownBranch(version) ||
isKnownVerBranch(version)
) {
// If `version-type: strict` or version is RC, we just try to remove a potential initial v
// If `version-type: strict`, version is an RC, or version is "a branch"
// we just try to remove a potential initial v
coerced = maybeRemoveVPrefix(version)
} else {
// Otherwise, we place the version into a version bucket
@@ -26401,18 +26402,19 @@ function getVersionFromSpec(spec0, versions0) {
const rangeMax = semver.maxSatisfying(versions, rangeForMax)
let version = null
if (
if (spec0 === 'latest') {
version = versions0[versions0.latest]
} else if (
isStrictVersion() ||
isRC(spec0) ||
isKnownBranch(spec0) ||
isKnownVerBranch(spec0)
isKnownVerBranch(spec0) ||
spec0 === 'nightly'
) {
if (versions0[spec]) {
// We obtain it directly
version = versions0[spec]
}
} else if (spec0 === 'latest') {
version = versions0[versions0.latest]
} else if (rangeMax !== null) {
// Otherwise, we compare alt. versions' semver ranges to this version, from highest to lowest
const thatVersion = spec
+14 -13
View File
@@ -15,11 +15,11 @@
"semver": "7.7.2"
},
"devDependencies": {
"@eslint/js": "9.30.0",
"@eslint/js": "9.30.1",
"@vercel/ncc": "0.38.3",
"eslint": "9.30.0",
"eslint": "9.30.1",
"eslint-plugin-yml": "1.18.0",
"globals": "16.2.0",
"globals": "16.3.0",
"markdownlint-cli": "0.45.0",
"prettier": "3.6.2",
"shellcheck": "3.1.0",
@@ -189,10 +189,11 @@
}
},
"node_modules/@eslint/js": {
"version": "9.30.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz",
"integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==",
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
"integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -963,9 +964,9 @@
}
},
"node_modules/eslint": {
"version": "9.30.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz",
"integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==",
"version": "9.30.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
"integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@@ -974,7 +975,7 @@
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.14.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.30.0",
"@eslint/js": "9.30.1",
"@eslint/plugin-kit": "^0.3.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
@@ -1406,9 +1407,9 @@
}
},
"node_modules/globals": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"version": "16.3.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
"license": "MIT",
"engines": {
+3 -3
View File
@@ -25,9 +25,9 @@
},
"devDependencies": {
"@vercel/ncc": "0.38.3",
"eslint": "9.30.0",
"@eslint/js": "9.30.0",
"globals": "16.2.0",
"eslint": "9.30.1",
"@eslint/js": "9.30.1",
"globals": "16.3.0",
"markdownlint-cli": "0.45.0",
"prettier": "3.6.2",
"shellcheck": "3.1.0",
+23 -21
View File
@@ -53,7 +53,7 @@ async function main() {
await maybeInstallRebar3(rebar3Spec)
// undefined is replaced by a function, post- main branch merge
const setupBeamVersion = '1fe9179'
const setupBeamVersion = 'fceaea9'
core.setOutput('setup-beam-version', setupBeamVersion)
}
@@ -170,10 +170,14 @@ async function maybeInstallRebar3(rebar3Spec) {
return installed
}
function maybeRemoveOTPPrefix(otpSpec) {
return otpSpec.replace(/^OTP-/, '')
}
async function getOTPVersion(otpSpec0, osVersion) {
const [otpVersions, originListing, hexMirrors] =
await getOTPVersions(osVersion)
let spec = otpSpec0.replace(/^OTP-/, '')
let spec = maybeRemoveOTPPrefix(otpSpec0)
const versions = otpVersions
const otpVersion = getVersionFromSpec(spec, versions)
@@ -349,12 +353,9 @@ async function getOTPVersions(osVersion) {
.trim()
.split('\n')
.forEach((line) => {
const otpMatch = line
.match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
debugLog('OTP line and parsing', [line, otpVersion, otpMatch])
const otpVersionOrig = line.match(/^([^ ]+)?( .+)/)[1]
const otpVersion = maybeRemoveOTPPrefix(otpVersionOrig)
debugLog('OTP line and parsing', [line, otpVersion, otpVersionOrig])
otpVersions[otpVersion] = otpVersionOrig // we keep the original for later reference
})
} else if (process.platform === 'win32') {
@@ -368,10 +369,10 @@ async function getOTPVersions(osVersion) {
.flat()
.filter((x) => x.name.match(file_regex))
.forEach((x) => {
const otpMatch = x.name.match(file_regex)
const otpVersion = otpMatch[1]
debugLog('OTP line and parsing', [otpMatch, otpVersion])
otpVersions[otpVersion] = otpVersion
const otpVersionOrig = x.name.match(file_regex)[1]
const otpVersion = otpVersionOrig
debugLog('OTP line and parsing', [x.name, otpVersion, otpVersionOrig])
otpVersions[otpVersion] = otpVersionOrig
})
})
} else if (process.platform === 'darwin') {
@@ -380,10 +381,9 @@ async function getOTPVersions(osVersion) {
columns: true,
})
.forEach((line) => {
const otpMatch = line.ref_name.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
const otpVersionOrig = otpMatch[0]
debugLog('OTP line and parsing', [line, otpVersion, otpMatch])
const otpVersionOrig = line.ref_name
const otpVersion = maybeRemoveOTPPrefix(otpVersionOrig)
debugLog('OTP line and parsing', [line, otpVersion, otpVersionOrig])
otpVersions[otpVersion] = otpVersionOrig // we keep the original for later reference
})
}
@@ -509,7 +509,8 @@ function getVersionFromSpec(spec0, versions0) {
isKnownBranch(version) ||
isKnownVerBranch(version)
) {
// If `version-type: strict` or version is RC, we just try to remove a potential initial v
// If `version-type: strict`, version is an RC, or version is "a branch"
// we just try to remove a potential initial v
coerced = maybeRemoveVPrefix(version)
} else {
// Otherwise, we place the version into a version bucket
@@ -526,18 +527,19 @@ function getVersionFromSpec(spec0, versions0) {
const rangeMax = semver.maxSatisfying(versions, rangeForMax)
let version = null
if (
if (spec0 === 'latest') {
version = versions0[versions0.latest]
} else if (
isStrictVersion() ||
isRC(spec0) ||
isKnownBranch(spec0) ||
isKnownVerBranch(spec0)
isKnownVerBranch(spec0) ||
spec0 === 'nightly'
) {
if (versions0[spec]) {
// We obtain it directly
version = versions0[spec]
}
} else if (spec0 === 'latest') {
version = versions0[versions0.latest]
} else if (rangeMax !== null) {
// Otherwise, we compare alt. versions' semver ranges to this version, from highest to lowest
const thatVersion = spec
+75 -39
View File
@@ -77,7 +77,7 @@ describe('OTP install', () => {
const otpOSVersion = 'ubuntu-08.04'
const otpVersion = 'OTP-23.2'
assert.rejects(
await assert.rejects(
async () => {
await setupBeam.install('otp', {
hexMirror: 'https://builds.hex.pm',
@@ -97,7 +97,7 @@ describe('OTP install', () => {
describe('Elixir install', () => {
it('fails for version 0.11 without OTP', async () => {
const exVersion = '0.11'
assert.rejects(
await assert.rejects(
async () => {
await setupBeam.install('elixir', {
hexMirror: 'https://builds.hex.pm',
@@ -114,7 +114,7 @@ describe('Elixir install', () => {
it('fails for version 1.0.0 on OTP 17 (without OTP)', async () => {
const exVersion = 'v1.0.0-otp-17'
assert.rejects(
await assert.rejects(
async () => {
await setupBeam.install('elixir', {
hexMirror: 'https://builds.hex.pm',
@@ -133,7 +133,7 @@ describe('Elixir install', () => {
describe('Gleam install', () => {
it('fails for unknown OTP', async () => {
const gleamVersion = '0.1.3'
assert.rejects(
await assert.rejects(
async () => {
await setupBeam.install('gleam', { gleamVersion })
},
@@ -149,7 +149,7 @@ describe('Gleam install', () => {
describe('rebar3 install', () => {
it('fails for unknown OTP', async () => {
const r3Version = '0.14.4'
assert.rejects(
await assert.rejects(
async () => {
await setupBeam.install('rebar3', { r3Version })
},
@@ -179,14 +179,6 @@ describe('.getOTPVersion(_) - Erlang', () => {
process.env.RUNNER_ARCH = 'X64'
it('is Ok for known linux version', async () => {
before = simulateInput('version-type', 'strict')
spec = '26'
osVersion = 'ubuntu-24.04'
expected = 'maint-26'
got = await setupBeam.getOTPVersion(spec, osVersion)
assert.deepStrictEqual(got, expected)
simulateInput('version-type', before)
before = simulateInput('version-type', 'strict')
spec = '27.0'
osVersion = 'ubuntu-24.04'
@@ -311,19 +303,46 @@ describe('.getOTPVersion(_) - Erlang', () => {
}
if (process.platform === 'linux') {
it('is Ok for known linux ARM64 version', async () => {
it('is main-... only if strict/maint-... is used as input', async () => {
const arm64Options = setupBeam.githubARMRunnerArchs()
process.env.RUNNER_ARCH =
arm64Options[Math.floor(Math.random() * arm64Options.length)]
before = simulateInput('version-type', 'strict')
spec = '26'
osVersion = 'ubuntu-24.04'
expected = 'maint-26'
got = await setupBeam.getOTPVersion(spec, osVersion)
assert.deepStrictEqual(got, expected)
await assert.rejects(
async () => {
await setupBeam.getOTPVersion('27', 'ubuntu-24.04')
},
(err) => {
assert.ok(err instanceof Error)
return true
},
`Fetching strict OTP version maint-<v> with input <v> is supposed to fail`,
)
simulateInput('version-type', before)
before = simulateInput('version-type', 'strict')
assert.strictEqual(
await setupBeam.getOTPVersion('maint-27', 'ubuntu-24.04'),
'maint-27',
)
simulateInput('version-type', before)
before = simulateInput('version-type', 'loose')
assert.strictEqual(
await setupBeam.getOTPVersion('maint-27', 'ubuntu-24.04'),
'maint-27',
)
simulateInput('version-type', before)
process.env.RUNNER_ARCH = previousRunnerArch
})
it('is Ok for known linux ARM64 version', async () => {
const arm64Options = setupBeam.githubARMRunnerArchs()
process.env.RUNNER_ARCH =
arm64Options[Math.floor(Math.random() * arm64Options.length)]
before = simulateInput('version-type', 'strict')
spec = '27.0'
osVersion = 'ubuntu-24.04'
@@ -388,14 +407,6 @@ describe('.getOTPVersion(_) - Erlang', () => {
process.env.RUNNER_ARCH =
amd64Options[Math.floor(Math.random() * amd64Options.length)]
before = simulateInput('version-type', 'strict')
spec = '26'
osVersion = 'ubuntu-24.04'
expected = 'maint-26'
got = await setupBeam.getOTPVersion(spec, osVersion)
assert.deepStrictEqual(got, expected)
simulateInput('version-type', before)
before = simulateInput('version-type', 'strict')
spec = '27.0'
osVersion = 'ubuntu-24.04'
@@ -475,6 +486,35 @@ describe('.getOTPVersion(_) - Erlang', () => {
}
if (process.platform === 'darwin') {
it('is main-... only if strict/maint-... is used as input', async () => {
const arm64Options = setupBeam.githubARMRunnerArchs()
process.env.RUNNER_ARCH =
arm64Options[Math.floor(Math.random() * arm64Options.length)]
before = simulateInput('version-type', 'strict')
await assert.rejects(
async () => {
await setupBeam.getOTPVersion('27')
},
(err) => {
assert.ok(err instanceof Error)
return true
},
`Fetching strict OTP version maint-<v> with input <v> is supposed to fail`,
)
simulateInput('version-type', before)
before = simulateInput('version-type', 'strict')
assert.strictEqual(await setupBeam.getOTPVersion('maint-27'), 'maint-27')
simulateInput('version-type', before)
before = simulateInput('version-type', 'loose')
assert.strictEqual(await setupBeam.getOTPVersion('maint-27'), 'maint-27')
simulateInput('version-type', before)
process.env.RUNNER_ARCH = previousRunnerArch
})
it('is Ok for known macos ARM64 version', async () => {
const arm64Options = setupBeam.githubARMRunnerArchs()
process.env.RUNNER_ARCH =
@@ -517,7 +557,7 @@ describe('OTP arch-specific install', () => {
const spec = '26'
const osVersion = 'ubuntu-24.04'
assert.rejects(
await assert.rejects(
async () => {
await setupBeam.getOTPVersion(spec, osVersion)
},
@@ -583,7 +623,7 @@ describe('.getOTPVersion(_) - Elixir', () => {
before = simulateInput('version-type', 'strict')
spec = 'v1.15.0-rc.2'
otpVersion = 'OTP-26'
otpVersion = 'OTP-26.0'
expected = 'v1.15.0-rc.2-otp-26'
await setupBeam.installOTP(otpVersion)
got = await setupBeam.getElixirVersion(spec, otpVersion)
@@ -617,6 +657,12 @@ describe('.getOTPVersion(_) - Gleam', () => {
got = await setupBeam.getGleamVersion(spec, otpVersion)
assert.deepStrictEqual(got, expected)
spec = 'nightly'
otpVersion = '28'
expected = 'nightly'
got = await setupBeam.getGleamVersion(spec, otpVersion)
assert.deepStrictEqual(got, expected)
spec = '0.13.2'
otpVersion = 'OTP-24'
expected = 'v0.13.2'
@@ -870,11 +916,6 @@ describe('.getVersionFromSpec(_)', () => {
got = setupBeam.getVersionFromSpec(spec, matrix.otp['ubuntu-20.04'])
assert.deepStrictEqual(got, expected)
spec = 'maint-24'
expected = 'maint-24'
got = setupBeam.getVersionFromSpec(spec, matrix.otp['ubuntu-22.04'])
assert.deepStrictEqual(got, expected)
spec = '> 0'
expected = 'OTP-26.2.5'
got = setupBeam.getVersionFromSpec(spec, matrix.otp['ubuntu-22.04'])
@@ -885,11 +926,6 @@ describe('.getVersionFromSpec(_)', () => {
got = setupBeam.getVersionFromSpec(spec, matrix.otp['ubuntu-22.04'])
assert.deepStrictEqual(got, expected)
spec = 'maint-26'
expected = 'maint-26'
got = setupBeam.getVersionFromSpec(spec, matrix.otp['ubuntu-22.04'])
assert.deepStrictEqual(got, expected)
spec = '> 0'
expected = 'OTP-27.0'
got = setupBeam.getVersionFromSpec(spec, matrix.otp['ubuntu-24.04'])