Files
setup-beam/src/setup-beam.js
T
Bryan Paxton e3f6ffe287 Provide an input for github tokens (#172)
* Provide an input for github tokens

- By providing an input for github tokens with a default users no longer have to pass
  the github token via their environment across workflows.
- Remove GITHUB_TOKEN from our environment in all workflows

* Don't run workflows on main on pull requests

Co-authored-by: Paulo F. Oliveira <paulo.ferraz.oliveira@gmail.com>
2023-01-06 10:14:27 -06:00

547 lines
15 KiB
JavaScript

const core = require('@actions/core')
const { exec } = require('@actions/exec')
const os = require('os')
const path = require('path')
const semver = require('semver')
const https = require('https')
const fs = require('fs')
const installer = require('./installer')
main().catch((err) => {
core.setFailed(err.message)
})
async function main() {
installer.checkPlatform()
const versionFilePath = getInput('version-file', false)
let versions
if (versionFilePath) {
if (!isStrictVersion()) {
throw new Error(
"you have to set version-type=strict if you're using version-file",
)
}
versions = parseVersionFile(versionFilePath)
}
const osVersion = getRunnerOSVersion()
const otpSpec = getInput('otp-version', true, 'erlang', versions)
const elixirSpec = getInput('elixir-version', false, 'elixir', versions)
const gleamSpec = getInput('gleam-version', false, 'gleam', versions)
const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions)
if (otpSpec !== 'false') {
await installOTP(otpSpec, osVersion)
const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec)
if (elixirInstalled === true) {
const shouldMixRebar = getInput('install-rebar', false)
await mix(shouldMixRebar, 'rebar')
const shouldMixHex = getInput('install-hex', false)
await mix(shouldMixHex, 'hex')
}
} else if (!gleamSpec) {
throw new Error('otp-version=false is only available when installing Gleam')
}
await maybeInstallGleam(gleamSpec)
await maybeInstallRebar3(rebar3Spec)
}
async function installOTP(otpSpec, osVersion) {
const otpVersion = await getOTPVersion(otpSpec, osVersion)
console.log(
`##[group]Installing Erlang/OTP ${otpVersion} - built on ${osVersion}`,
)
await installer.installOTP(osVersion, otpVersion)
core.setOutput('otp-version', otpVersion)
core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/otp/bin`)
console.log('##[endgroup]')
return otpVersion
}
async function maybeInstallElixir(elixirSpec, otpVersion) {
let installed = false
if (elixirSpec) {
const elixirVersion = await getElixirVersion(elixirSpec, otpVersion)
console.log(`##[group]Installing Elixir ${elixirVersion}`)
await installer.installElixir(elixirVersion)
core.setOutput('elixir-version', elixirVersion)
const disableProblemMatchers = getInput('disable_problem_matchers', false)
if (disableProblemMatchers === 'false') {
const matchersPath = path.join(__dirname, '..', '.github')
console.log(
`##[add-matcher]${path.join(matchersPath, 'elixir-matchers.json')}`,
)
}
core.addPath(`${os.homedir()}/.mix/escripts`)
core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/elixir/bin`)
console.log('##[endgroup]')
installed = true
}
return installed
}
async function mix(shouldMix, what) {
if (shouldMix === 'true') {
const cmd = 'mix'
const args = [`local.${what}`, '--force']
console.log(`##[group]Running ${cmd} ${args}`)
await exec(cmd, args)
console.log('##[endgroup]')
}
}
async function maybeInstallGleam(gleamSpec) {
let installed = false
if (gleamSpec) {
const gleamVersion = await getGleamVersion(gleamSpec)
console.log(`##[group]Installing Gleam ${gleamVersion}`)
await installer.installGleam(gleamVersion)
core.setOutput('gleam-version', gleamVersion)
core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/gleam/bin`)
console.log('##[endgroup]')
installed = true
}
return installed
}
async function maybeInstallRebar3(rebar3Spec) {
let installed = false
let rebar3Version
if (rebar3Spec) {
if (rebar3Spec === 'nightly') {
rebar3Version = 'nightly'
} else {
rebar3Version = await getRebar3Version(rebar3Spec)
}
console.log(`##[group]Installing rebar3 ${rebar3Version}`)
await installer.installRebar3(rebar3Version)
core.setOutput('rebar3-version', rebar3Version)
core.addPath(`${process.env.RUNNER_TEMP}/.setup-beam/rebar3/bin`)
console.log('##[endgroup]')
installed = true
}
return installed
}
async function getOTPVersion(otpSpec0, osVersion) {
const otpVersions = await getOTPVersions(osVersion)
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!
}
if (otpVersion === null) {
throw new Error(
`Requested Erlang/OTP version (${otpSpec0}) not found in version list ` +
"(should you be using option 'version-type': 'strict'?)",
)
}
return otpVersions.get(otpVersion) // from the reference, for download
}
async function getElixirVersion(exSpec0, otpVersion0) {
const otpVersion = otpVersion0.match(/^([^-]+-)?(.+)$/)[2]
const otpVersionMajor = otpVersion.match(/^([^.]+).*$/)[1]
const elixirVersions = await getElixirVersions()
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}`
}
if (elixirVersionFromSpec === null) {
throw new Error(
`Requested Elixir version (${exSpec0}) not found in version list ` +
"(should you be using option 'version-type': 'strict'?)",
)
}
const elixirVersionComp = elixirVersions.get(elixirVersionFromSpec)
if (
(elixirVersionComp && elixirVersionComp.includes(otpVersionMajor)) ||
!isVersion(otpVersionMajor)
) {
core.info(
`Using Elixir ${elixirVersionFromSpec} (built for Erlang/OTP ${otpVersionMajor})`,
)
} else {
throw new Error(
`Requested Elixir / Erlang/OTP version (${exSpec0} / ${otpVersion0}) not ` +
'found in version list (did you check Compatibility between Elixir and Erlang/OTP?)',
)
}
return maybePrependWithV(elixirVersionForDownload)
}
async function getGleamVersion(gleamSpec0) {
const gleamSpec = gleamSpec0.match(/^v?(.+)$/)
const gleamVersions = await getGleamVersions()
const gleamVersion = getVersionFromSpec(gleamSpec[1], gleamVersions, true)
if (gleamVersion === null) {
throw new Error(
`Requested Gleam version (${gleamSpec0}) not found in version list ` +
"(should you be using option 'version-type': 'strict'?)",
)
}
return maybePrependWithV(gleamVersion, gleamVersion)
}
async function getRebar3Version(r3Spec) {
const rebar3Versions = await getRebar3Versions()
const rebar3Version = getVersionFromSpec(r3Spec, rebar3Versions)
if (rebar3Version === null) {
throw new Error(
`Requested rebar3 version (${r3Spec}) not found in version list ` +
"(should you be using option 'version-type': 'strict'?)",
)
}
return rebar3Version
}
async function getOTPVersions(osVersion) {
let originListing
let pageIdxs
if (process.platform === 'linux') {
originListing = `https://repo.hex.pm/builds/otp/${osVersion}/builds.txt`
pageIdxs = [null]
} else if (process.platform === 'win32') {
originListing =
'https://api.github.com/repos/erlang/otp/releases?per_page=100'
pageIdxs = [1, 2, 3]
}
const otpVersionsListings = await get(originListing, pageIdxs)
const otpVersions = new Map()
if (process.platform === 'linux') {
otpVersionsListings
.trim()
.split('\n')
.forEach((line) => {
const otpMatch = line
.match(/^([^ ]+)?( .+)/)[1]
.match(/^([^-]+-)?(.+)$/)
const otpVersion = otpMatch[2]
otpVersions.set(otpVersion, otpMatch[0]) // we keep the original for later reference
})
} else if (process.platform === 'win32') {
otpVersionsListings.forEach((otpVersionsListing) => {
jsonParse(otpVersionsListing)
.map((x) => x.assets)
.flat()
.filter((x) => x.name.match(/^otp_win64_.*.exe$/))
.forEach((x) => {
const otpMatch = x.name.match(/^otp_win64_(.*).exe$/)
const otpVersion = otpMatch[1]
otpVersions.set(otpVersion, otpVersion)
})
})
}
return otpVersions
}
async function getElixirVersions() {
const elixirVersionsListings = await get(
'https://repo.hex.pm/builds/elixir/builds.txt',
[null],
)
const otpVersionsForElixirMap = new Map()
elixirVersionsListings
.trim()
.split('\n')
.forEach((line) => {
const elixirMatch =
line.match(/^v?(.+)-otp-([^ ]+)/) || line.match(/^v?([^ ]+)/)
const elixirVersion = maybePrependWithV(elixirMatch[1])
const otpVersion = elixirMatch[2]
const otpVersions = otpVersionsForElixirMap.get(elixirVersion) || []
if (otpVersion) {
// -otp- present (special case)
otpVersions.push(otpVersion)
}
otpVersionsForElixirMap.set(elixirVersion, otpVersions)
})
return otpVersionsForElixirMap
}
async function getGleamVersions() {
const resultJSONs = await get(
'https://api.github.com/repos/gleam-lang/gleam/releases?per_page=100',
[1, 2, 3],
)
const gleamVersionsListing = []
resultJSONs.forEach((resultJSON) => {
jsonParse(resultJSON)
.map((x) => x.tag_name)
.sort()
.forEach((v) => gleamVersionsListing.push(v))
})
return gleamVersionsListing
}
async function getRebar3Versions() {
const resultJSONs = await get(
'https://api.github.com/repos/erlang/rebar3/releases?per_page=100',
[1, 2, 3],
)
const rebar3VersionsListing = []
resultJSONs.forEach((resultJSON) => {
jsonParse(resultJSON)
.map((x) => x.tag_name)
.sort()
.forEach((v) => rebar3VersionsListing.push(v))
})
return rebar3VersionsListing
}
function isStrictVersion() {
return getInput('version-type', false) === 'strict'
}
function getVersionFromSpec(spec, versions, maybePrependWithV0) {
let version = null
if (spec.match(/rc/) || isStrictVersion()) {
version = spec
}
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
}
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)
}
return v
}
function maybeCoerced(v) {
let ret
try {
ret = semver.coerce(v).version
} catch {
// some stuff can't be coerced, like 'master'
ret = v
}
return ret
}
function sortVersions(left, right) {
let ret = 0
const newL = verAsComparableStr(left)
const newR = verAsComparableStr(right)
function verAsComparableStr(ver) {
const matchGroups = 5
const verSpec = /([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?\.?([^.]+)?/
const matches = ver.match(verSpec).splice(1, matchGroups)
return matches.reduce((acc, v) => acc + (v || '0').padStart(3, '0'), '')
}
if (newL < newR) {
ret = -1
} else if (newL > newR) {
ret = 1
}
return ret
}
function getRunnerOSVersion() {
const ImageOSToContainer = {
ubuntu18: 'ubuntu-18.04',
ubuntu20: 'ubuntu-20.04',
ubuntu22: 'ubuntu-22.04',
win19: 'windows-2019',
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 " +
`${process.env.ImageOS}` +
"), but failed. If you're using a " +
"self-hosted runner, you should set 'env': 'ImageOS': ... to one of the following: " +
"['" +
`${Object.keys(ImageOSToContainer).join("', '")}` +
"']",
)
}
return containerFromEnvImageOS
}
async function get(url0, pageIdxs) {
function getPage(pageIdx) {
return new Promise((resolve, reject) => {
const url = new URL(url0)
const headers = {
'user-agent': 'setup-beam',
}
const GithubToken = getInput('github-token', false)
if (GithubToken) {
headers.authorization = `Bearer ${GithubToken}`
}
if (pageIdx !== null) {
url.searchParams.append('page', pageIdx)
}
https
.get(url, { headers }, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
if (res.statusCode >= 400 && res.statusCode <= 599) {
reject(
new Error(
`Got ${res.statusCode} from ${url}. Exiting with error`,
),
)
} else {
resolve(data)
}
})
})
.on('error', (err) => {
reject(err)
})
})
}
let ret
if (pageIdxs[0] === null) {
ret = getPage(null)
} else {
ret = Promise.all(pageIdxs.map((pageIdx) => getPage(pageIdx)))
}
return ret
}
function maybePrependWithV(v) {
if (isVersion(v)) {
return `v${v.replace('v', '')}`
}
return v
}
function isVersion(v) {
return /^v?\d+/.test(v)
}
function getInput(inputName, required, alternativeName, alternatives) {
const alternativeValue = (alternatives || new Map()).get(alternativeName)
let input = core.getInput(inputName, {
required: alternativeValue ? false : required,
})
// We can't have both input and alternativeValue set
if (input && alternativeValue) {
throw new Error(
`Found input ${inputName}=${input} (from the YML) \
alongside ${alternativeName}=${alternativeValue} \
(from the version file). You must choose one or the other.`,
)
} else if (!input) {
input = alternativeValue
}
return input
}
function parseVersionFile(versionFilePath0) {
const versionFilePath = path.join(
process.env.GITHUB_WORKSPACE,
versionFilePath0,
)
if (!fs.existsSync(versionFilePath)) {
throw new Error(
`The specified version file, ${versionFilePath0}, does not exist`,
)
}
console.log(`##[group]Parsing version file at ${versionFilePath0}`)
const appVersions = new Map()
const versions = fs.readFileSync(versionFilePath, 'utf8')
// For the time being we parse .tool-versions
// If we ever start parsing something else, this should
// become default in a new option named e.g. version-file-type
versions.split('\n').forEach((line) => {
const appVersion = line.match(/^([^ ]+)[ ]+([^ #]+)/)
if (appVersion) {
const app = appVersion[1]
if (['erlang', 'elixir', 'gleam', 'rebar'].includes(app)) {
const [, , version] = appVersion
console.log(`Consuming ${app} at version ${version}`)
appVersions.set(app, version)
}
}
})
if (!appVersions.size) {
console.log('There was apparently nothing to consume')
} else {
console.log('... done!')
}
console.log('##[endgroup]')
return appVersions
}
function jsonParse(maybeJson) {
try {
return JSON.parse(maybeJson)
} catch (exc) {
throw new Error(
`Got an exception when trying to parse non-JSON ${maybeJson}: ${exc}`,
)
}
}
module.exports = {
getOTPVersion,
getElixirVersion,
getGleamVersion,
getRebar3Version,
getVersionFromSpec,
parseVersionFile,
}