From c373088147072bf71387ba2cd6128d1cbea7369d Mon Sep 17 00:00:00 2001 From: "Paulo F. Oliveira" Date: Tue, 22 Nov 2022 22:34:21 +0000 Subject: [PATCH] Ease consumption by means of .tool-versions (#159) * Ease consumption by means of .tool-versions * Update as per self-review * Update as per self-review * Act on CI results * Act on CI results * Act on CI results This value is check by code, in any case * Act on CI results * Act on CI results * Act on CI results It appears we can't install Elixir twice on Windows, but can on Ubuntu; this is outside the scope of this branch's context so I won't fix it now * Tweak visual consumption elements * Ease potential future maintenance and/or update --- .github/workflows/action.yml | 6 +++ .gitignore | 1 + README.md | 27 +++++++++++ __tests__/setup-beam.test.js | 36 ++++++++++++++- action.yml | 7 ++- dist/index.js | 90 ++++++++++++++++++++++++++++++------ src/setup-beam.js | 90 ++++++++++++++++++++++++++++++------ 7 files changed, 226 insertions(+), 31 deletions(-) diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index c050821..9cca3d1 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -40,6 +40,12 @@ jobs: node-version: '14' - run: npm ci - run: npm test + - name: .tool-versions test + id: setup-beam + uses: ./ + with: + version-file: __tests__/.tool-versions + version-type: strict unit_tests_windows: name: Unit tests (Windows) diff --git a/.gitignore b/.gitignore index f445c78..cf5dea3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/** dist/.github/workflows +__tests__/.tool-versions diff --git a/README.md b/README.md index b60c0ee..5c976c0 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ workflow by: - optionally, installing [Gleam](https://gleam.run/) - optionally, installing [`rebar3`](https://www.rebar3.org/) - optionally, installing [`hex`](https://hex.pm/) +- optionally, opting for strict or loose version matching - optionally, having [problem matchers](https://github.com/actions/toolkit/blob/main/docs/problem-matchers.md) show warnings and errors on pull requests +- optionally, using a version file (as explained in "Version file", below), to identify versions **Note**: currently, this action only supports Actions' `ubuntu-` and `windows-` runtimes. @@ -86,6 +88,31 @@ jobs: ... ``` +### Version file + +A version file is specified via input `version-file` (e.g.`.tool-versions`). This +allows not having to use YML input for versions, though the action does check (and +will exit with error) if both inputs are set. + +**Note**: if you're using a version file, option `version-type` is checked to be `strict`, +and will make the action exit with error otherwise. + +The following version file formats are supported: + +- `.tool-versions`, as specified by [asdf: Configuration](https://asdf-vm.com/manage/configuration.html) + +Supported version elements are the same as the ones defined for the YML portion of the action, +with the following correspondence. + +#### `.tool-versions` format + +| YML | `.tool-versions` | +|- |- +| `otp-version` | `erlang` +| `elixir-version` | `elixir` +| `gleam-version` | `gleam` +| `rebar3-version` | `rebar` + ### Example (Erlang/OTP + Elixir, on Ubuntu) ```yaml diff --git a/__tests__/setup-beam.test.js b/__tests__/setup-beam.test.js index a4d118f..3e715d5 100644 --- a/__tests__/setup-beam.test.js +++ b/__tests__/setup-beam.test.js @@ -5,6 +5,7 @@ simulateInput('install-rebar', 'true') simulateInput('install-hex', 'true') const assert = require('assert') +const fs = require('fs') const setupBeam = require('../src/setup-beam') const installer = require('../src/installer') @@ -20,6 +21,8 @@ async function all() { await testRebar3Versions() await testGetVersionFromSpec() + + await testParseVersionFile() } async function testFailInstallOTP() { @@ -405,8 +408,39 @@ async function testGetVersionFromSpec() { simulateInput('version-type', 'loose') } +async function testParseVersionFile() { + const otpVersion = unsimulateInput('otp-version') + const elixirVersion = unsimulateInput('elixir-version') + + const erlang = '25.1.1' + const elixir = '1.14.1' + const toolVersions = `# a comment +erlang ${erlang}# comment, no space +elixir ${elixir} # comment, with space + gleam 0.23 # not picked up` + const filename = '__tests__/.tool-versions' + fs.writeFileSync(filename, toolVersions) + process.env.GITHUB_WORKSPACE = '' + const appVersions = setupBeam.parseVersionFile(filename) + assert.strictEqual(appVersions.get('erlang'), erlang) + assert.strictEqual(appVersions.get('elixir'), elixir) + + simulateInput('otp-version', otpVersion) + simulateInput('elixir-version', elixirVersion) +} + +function unsimulateInput(key) { + const before = process.env[input(key)] + simulateInput(key, '') + return before +} + function simulateInput(key, value) { - process.env[`INPUT_${key.replace(/ /g, '_').toUpperCase()}`] = value + process.env[input(key)] = value +} + +function input(key) { + return `INPUT_${key.replace(/ /g, '_').toUpperCase()}` } all() diff --git a/action.yml b/action.yml index abad24c..d9d7ca5 100644 --- a/action.yml +++ b/action.yml @@ -11,7 +11,6 @@ inputs: otp-version: description: Version range or exact version of Erlang/OTP to use, or false when installing only Gleam without OTP - required: true elixir-version: description: Version range or exact version of Elixir to use gleam-version: @@ -31,8 +30,12 @@ inputs: to guess versions based on semver rules default: loose disable_problem_matchers: - description: whether to have the problem matchers present in the results (or not) + description: whether to have the problem matchers present in the results + (or not) default: false + version-file: + description: a versions file (e.g. as used by `asdf`), which defines inputs + default: '' outputs: elixir-version: description: Exact version of Elixir that was installed diff --git a/dist/index.js b/dist/index.js index f61a3bb..cd438d9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -7196,6 +7196,7 @@ const { exec } = __nccwpck_require__(1514) const path = __nccwpck_require__(1017) const semver = __nccwpck_require__(1383) const https = __nccwpck_require__(5687) +const fs = __nccwpck_require__(7147) const installer = __nccwpck_require__(2127) main().catch((err) => { @@ -7205,24 +7206,31 @@ main().catch((err) => { 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 = core.getInput('otp-version', { required: true }) - const elixirSpec = core.getInput('elixir-version', { required: false }) - const gleamSpec = core.getInput('gleam-version', { required: false }) - const rebar3Spec = core.getInput('rebar3-version', { required: false }) + 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 = core.getInput('install-rebar', { - required: false, - }) + const shouldMixRebar = getInput('install-rebar', false) await mix(shouldMixRebar, 'rebar') - const shouldMixHex = core.getInput('install-hex', { - required: false, - }) + const shouldMixHex = getInput('install-hex', false) await mix(shouldMixHex, 'hex') } } else if (!gleamSpec) { @@ -7254,9 +7262,7 @@ async function maybeInstallElixir(elixirSpec, otpVersion) { console.log(`##[group]Installing Elixir ${elixirVersion}`) await installer.installElixir(elixirVersion) core.setOutput('elixir-version', elixirVersion) - const disableProblemMatchers = core.getInput('disable_problem_matchers', { - required: false, - }) + const disableProblemMatchers = getInput('disable_problem_matchers', false) if (disableProblemMatchers === 'false') { const matchersPath = __nccwpck_require__.ab + ".github" console.log( @@ -7503,7 +7509,7 @@ async function getRebar3Versions() { } function isStrictVersion() { - return core.getInput('version-type', { required: false }) === 'strict' + return getInput('version-type', false) === 'strict' } function getVersionFromSpec(spec, versions, maybePrependWithV0) { @@ -7653,12 +7659,68 @@ 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 +} + module.exports = { getOTPVersion, getElixirVersion, getGleamVersion, getRebar3Version, getVersionFromSpec, + parseVersionFile, } diff --git a/src/setup-beam.js b/src/setup-beam.js index 8e614b5..577b2a3 100644 --- a/src/setup-beam.js +++ b/src/setup-beam.js @@ -3,6 +3,7 @@ const { exec } = require('@actions/exec') const path = require('path') const semver = require('semver') const https = require('https') +const fs = require('fs') const installer = require('./installer') main().catch((err) => { @@ -12,24 +13,31 @@ main().catch((err) => { 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 = core.getInput('otp-version', { required: true }) - const elixirSpec = core.getInput('elixir-version', { required: false }) - const gleamSpec = core.getInput('gleam-version', { required: false }) - const rebar3Spec = core.getInput('rebar3-version', { required: false }) + 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 = core.getInput('install-rebar', { - required: false, - }) + const shouldMixRebar = getInput('install-rebar', false) await mix(shouldMixRebar, 'rebar') - const shouldMixHex = core.getInput('install-hex', { - required: false, - }) + const shouldMixHex = getInput('install-hex', false) await mix(shouldMixHex, 'hex') } } else if (!gleamSpec) { @@ -61,9 +69,7 @@ async function maybeInstallElixir(elixirSpec, otpVersion) { console.log(`##[group]Installing Elixir ${elixirVersion}`) await installer.installElixir(elixirVersion) core.setOutput('elixir-version', elixirVersion) - const disableProblemMatchers = core.getInput('disable_problem_matchers', { - required: false, - }) + const disableProblemMatchers = getInput('disable_problem_matchers', false) if (disableProblemMatchers === 'false') { const matchersPath = path.join(__dirname, '..', '.github') console.log( @@ -310,7 +316,7 @@ async function getRebar3Versions() { } function isStrictVersion() { - return core.getInput('version-type', { required: false }) === 'strict' + return getInput('version-type', false) === 'strict' } function getVersionFromSpec(spec, versions, maybePrependWithV0) { @@ -460,10 +466,66 @@ 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 +} + module.exports = { getOTPVersion, getElixirVersion, getGleamVersion, getRebar3Version, getVersionFromSpec, + parseVersionFile, }