Ease consumption by means of .tool-versions (#159)

c373088 · Paulo F. Oliveira · 2022-11-22 22:34

7 files +226 -31
Message
{commit_body(@commit)}

Files changed

modified .github/workflows/action.yml
+6 −0
@@ -40,6 +40,12 @@ jobs:
40 40 node-version: '14'
41 41 - run: npm ci
42 42 - run: npm test
43 + - name: .tool-versions test
44 + id: setup-beam
45 + uses: ./
46 + with:
47 + version-file: __tests__/.tool-versions
48 + version-type: strict
43 49
44 50 unit_tests_windows:
45 51 name: Unit tests (Windows)
modified .gitignore
+1 −0
@@ -1,2 +1,3 @@
1 1 node_modules/**
2 2 dist/.github/workflows
3 +__tests__/.tool-versions
modified README.md
+27 −0
@@ -16,9 +16,11 @@ workflow by:
16 16 - optionally, installing [Gleam](https://gleam.run/)
17 17 - optionally, installing [`rebar3`](https://www.rebar3.org/)
18 18 - optionally, installing [`hex`](https://hex.pm/)
19 +- optionally, opting for strict or loose version matching
19 20 - optionally, having
20 21 [problem matchers](https://github.com/actions/toolkit/blob/main/docs/problem-matchers.md) show
21 22 warnings and errors on pull requests
23 +- optionally, using a version file (as explained in "Version file", below), to identify versions
22 24
23 25 **Note**: currently, this action only supports Actions' `ubuntu-` and `windows-` runtimes.
24 26
@@ -86,6 +88,31 @@ jobs:
86 88 ...
87 89 ```
88 90
91 +### Version file
92 +
93 +A version file is specified via input `version-file` (e.g.`.tool-versions`). This
94 +allows not having to use YML input for versions, though the action does check (and
95 +will exit with error) if both inputs are set.
96 +
97 +**Note**: if you're using a version file, option `version-type` is checked to be `strict`,
98 +and will make the action exit with error otherwise.
99 +
100 +The following version file formats are supported:
101 +
102 +- `.tool-versions`, as specified by [asdf: Configuration](https://asdf-vm.com/manage/configuration.html)
103 +
104 +Supported version elements are the same as the ones defined for the YML portion of the action,
105 +with the following correspondence.
106 +
107 +#### `.tool-versions` format
108 +
109 +| YML | `.tool-versions` |
110 +|- |-
111 +| `otp-version` | `erlang`
112 +| `elixir-version` | `elixir`
113 +| `gleam-version` | `gleam`
114 +| `rebar3-version` | `rebar`
115 +
89 116 ### Example (Erlang/OTP + Elixir, on Ubuntu)
90 117
91 118 ```yaml
modified __tests__/setup-beam.test.js
+35 −1
@@ -5,6 +5,7 @@ simulateInput('install-rebar', 'true')
5 5 simulateInput('install-hex', 'true')
6 6
7 7 const assert = require('assert')
8 +const fs = require('fs')
8 9 const setupBeam = require('../src/setup-beam')
9 10 const installer = require('../src/installer')
10 11
@@ -20,6 +21,8 @@ async function all() {
20 21 await testRebar3Versions()
21 22
22 23 await testGetVersionFromSpec()
24 +
25 + await testParseVersionFile()
23 26 }
24 27
25 28 async function testFailInstallOTP() {
@@ -405,8 +408,39 @@ async function testGetVersionFromSpec() {
405 408 simulateInput('version-type', 'loose')
406 409 }
407 410
411 +async function testParseVersionFile() {
412 + const otpVersion = unsimulateInput('otp-version')
413 + const elixirVersion = unsimulateInput('elixir-version')
414 +
415 + const erlang = '25.1.1'
416 + const elixir = '1.14.1'
417 + const toolVersions = `# a comment
418 +erlang ${erlang}# comment, no space
419 +elixir ${elixir} # comment, with space
420 + gleam 0.23 # not picked up`
421 + const filename = '__tests__/.tool-versions'
422 + fs.writeFileSync(filename, toolVersions)
423 + process.env.GITHUB_WORKSPACE = ''
424 + const appVersions = setupBeam.parseVersionFile(filename)
425 + assert.strictEqual(appVersions.get('erlang'), erlang)
426 + assert.strictEqual(appVersions.get('elixir'), elixir)
427 +
428 + simulateInput('otp-version', otpVersion)
429 + simulateInput('elixir-version', elixirVersion)
430 +}
431 +
432 +function unsimulateInput(key) {
433 + const before = process.env[input(key)]
434 + simulateInput(key, '')
435 + return before
436 +}
437 +
408 438 function simulateInput(key, value) {
409 process.env[`INPUT_${key.replace(/ /g, '_').toUpperCase()}`] = value
439 + process.env[input(key)] = value
440 +}
441 +
442 +function input(key) {
443 + return `INPUT_${key.replace(/ /g, '_').toUpperCase()}`
410 444 }
411 445
412 446 all()
modified action.yml
+5 −2
@@ -11,7 +11,6 @@ inputs:
11 11 otp-version:
12 12 description: Version range or exact version of Erlang/OTP to use,
13 13 or false when installing only Gleam without OTP
14 required: true
15 14 elixir-version:
16 15 description: Version range or exact version of Elixir to use
17 16 gleam-version:
@@ -31,8 +30,12 @@ inputs:
31 30 to guess versions based on semver rules
32 31 default: loose
33 32 disable_problem_matchers:
34 description: whether to have the problem matchers present in the results (or not)
33 + description: whether to have the problem matchers present in the results
34 + (or not)
35 35 default: false
36 + version-file:
37 + description: a versions file (e.g. as used by `asdf`), which defines inputs
38 + default: ''
36 39 outputs:
37 40 elixir-version:
38 41 description: Exact version of Elixir that was installed
modified dist/index.js
+76 −14
@@ -7196,6 +7196,7 @@ const { exec } = __nccwpck_require__(1514)
7196 7196 const path = __nccwpck_require__(1017)
7197 7197 const semver = __nccwpck_require__(1383)
7198 7198 const https = __nccwpck_require__(5687)
7199 +const fs = __nccwpck_require__(7147)
7199 7200 const installer = __nccwpck_require__(2127)
7200 7201
7201 7202 main().catch((err) => {
@@ -7205,24 +7206,31 @@ main().catch((err) => {
7205 7206 async function main() {
7206 7207 installer.checkPlatform()
7207 7208
7209 + const versionFilePath = getInput('version-file', false)
7210 + let versions
7211 + if (versionFilePath) {
7212 + if (!isStrictVersion()) {
7213 + throw new Error(
7214 + "you have to set version-type=strict if you're using version-file",
7215 + )
7216 + }
7217 + versions = parseVersionFile(versionFilePath)
7218 + }
7219 +
7208 7220 const osVersion = getRunnerOSVersion()
7209 const otpSpec = core.getInput('otp-version', { required: true })
7210 const elixirSpec = core.getInput('elixir-version', { required: false })
7211 const gleamSpec = core.getInput('gleam-version', { required: false })
7212 const rebar3Spec = core.getInput('rebar3-version', { required: false })
7221 + const otpSpec = getInput('otp-version', true, 'erlang', versions)
7222 + const elixirSpec = getInput('elixir-version', false, 'elixir', versions)
7223 + const gleamSpec = getInput('gleam-version', false, 'gleam', versions)
7224 + const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions)
7213 7225
7214 7226 if (otpSpec !== 'false') {
7215 7227 await installOTP(otpSpec, osVersion)
7216 7228 const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec)
7217 7229
7218 7230 if (elixirInstalled === true) {
7219 const shouldMixRebar = core.getInput('install-rebar', {
7220 required: false,
7221 })
7231 + const shouldMixRebar = getInput('install-rebar', false)
7222 7232 await mix(shouldMixRebar, 'rebar')
7223 const shouldMixHex = core.getInput('install-hex', {
7224 required: false,
7225 })
7233 + const shouldMixHex = getInput('install-hex', false)
7226 7234 await mix(shouldMixHex, 'hex')
7227 7235 }
7228 7236 } else if (!gleamSpec) {
@@ -7254,9 +7262,7 @@ async function maybeInstallElixir(elixirSpec, otpVersion) {
7254 7262 console.log(`##[group]Installing Elixir ${elixirVersion}`)
7255 7263 await installer.installElixir(elixirVersion)
7256 7264 core.setOutput('elixir-version', elixirVersion)
7257 const disableProblemMatchers = core.getInput('disable_problem_matchers', {
7258 required: false,
7259 })
7265 + const disableProblemMatchers = getInput('disable_problem_matchers', false)
7260 7266 if (disableProblemMatchers === 'false') {
7261 7267 const matchersPath = __nccwpck_require__.ab + ".github"
7262 7268 console.log(
@@ -7503,7 +7509,7 @@ async function getRebar3Versions() {
7503 7509 }
7504 7510
7505 7511 function isStrictVersion() {
7506 return core.getInput('version-type', { required: false }) === 'strict'
7512 + return getInput('version-type', false) === 'strict'
7507 7513 }
7508 7514
7509 7515 function getVersionFromSpec(spec, versions, maybePrependWithV0) {
@@ -7653,12 +7659,68 @@ function isVersion(v) {
7653 7659 return /^v?\d+/.test(v)
7654 7660 }
7655 7661
7662 +function getInput(inputName, required, alternativeName, alternatives) {
7663 + const alternativeValue = (alternatives || new Map()).get(alternativeName)
7664 + let input = core.getInput(inputName, {
7665 + required: alternativeValue ? false : required,
7666 + })
7667 + // We can't have both input and alternativeValue set
7668 + if (input && alternativeValue) {
7669 + throw new Error(
7670 + `Found input ${inputName}=${input} (from the YML) \
7671 +alongside ${alternativeName}=${alternativeValue} \
7672 +(from the version file). You must choose one or the other.`,
7673 + )
7674 + } else if (!input) {
7675 + input = alternativeValue
7676 + }
7677 + return input
7678 +}
7679 +
7680 +function parseVersionFile(versionFilePath0) {
7681 + const versionFilePath = path.join(
7682 + process.env.GITHUB_WORKSPACE,
7683 + versionFilePath0,
7684 + )
7685 + if (!fs.existsSync(versionFilePath)) {
7686 + throw new Error(
7687 + `The specified version file, ${versionFilePath0}, does not exist`,
7688 + )
7689 + }
7690 + console.log(`##[group]Parsing version file at ${versionFilePath0}`)
7691 + const appVersions = new Map()
7692 + const versions = fs.readFileSync(versionFilePath, 'utf8')
7693 + // For the time being we parse .tool-versions
7694 + // If we ever start parsing something else, this should
7695 + // become default in a new option named e.g. version-file-type
7696 + versions.split('\n').forEach((line) => {
7697 + const appVersion = line.match(/^([^ ]+)[ ]+([^ #]+)/)
7698 + if (appVersion) {
7699 + const app = appVersion[1]
7700 + if (['erlang', 'elixir', 'gleam', 'rebar'].includes(app)) {
7701 + const [, , version] = appVersion
7702 + console.log(`Consuming ${app} at version ${version}`)
7703 + appVersions.set(app, version)
7704 + }
7705 + }
7706 + })
7707 + if (!appVersions.size) {
7708 + console.log('There was apparently nothing to consume')
7709 + } else {
7710 + console.log('... done!')
7711 + }
7712 + console.log('##[endgroup]')
7713 +
7714 + return appVersions
7715 +}
7716 +
7656 7717 module.exports = {
7657 7718 getOTPVersion,
7658 7719 getElixirVersion,
7659 7720 getGleamVersion,
7660 7721 getRebar3Version,
7661 7722 getVersionFromSpec,
7723 + parseVersionFile,
7662 7724 }
7663 7725
7664 7726
modified src/setup-beam.js
+76 −14
@@ -3,6 +3,7 @@ const { exec } = require('@actions/exec')
3 3 const path = require('path')
4 4 const semver = require('semver')
5 5 const https = require('https')
6 +const fs = require('fs')
6 7 const installer = require('./installer')
7 8
8 9 main().catch((err) => {
@@ -12,24 +13,31 @@ main().catch((err) => {
12 13 async function main() {
13 14 installer.checkPlatform()
14 15
16 + const versionFilePath = getInput('version-file', false)
17 + let versions
18 + if (versionFilePath) {
19 + if (!isStrictVersion()) {
20 + throw new Error(
21 + "you have to set version-type=strict if you're using version-file",
22 + )
23 + }
24 + versions = parseVersionFile(versionFilePath)
25 + }
26 +
15 27 const osVersion = getRunnerOSVersion()
16 const otpSpec = core.getInput('otp-version', { required: true })
17 const elixirSpec = core.getInput('elixir-version', { required: false })
18 const gleamSpec = core.getInput('gleam-version', { required: false })
19 const rebar3Spec = core.getInput('rebar3-version', { required: false })
28 + const otpSpec = getInput('otp-version', true, 'erlang', versions)
29 + const elixirSpec = getInput('elixir-version', false, 'elixir', versions)
30 + const gleamSpec = getInput('gleam-version', false, 'gleam', versions)
31 + const rebar3Spec = getInput('rebar3-version', false, 'rebar', versions)
20 32
21 33 if (otpSpec !== 'false') {
22 34 await installOTP(otpSpec, osVersion)
23 35 const elixirInstalled = await maybeInstallElixir(elixirSpec, otpSpec)
24 36
25 37 if (elixirInstalled === true) {
26 const shouldMixRebar = core.getInput('install-rebar', {
27 required: false,
28 })
38 + const shouldMixRebar = getInput('install-rebar', false)
29 39 await mix(shouldMixRebar, 'rebar')
30 const shouldMixHex = core.getInput('install-hex', {
31 required: false,
32 })
40 + const shouldMixHex = getInput('install-hex', false)
33 41 await mix(shouldMixHex, 'hex')
34 42 }
35 43 } else if (!gleamSpec) {
@@ -61,9 +69,7 @@ async function maybeInstallElixir(elixirSpec, otpVersion) {
61 69 console.log(`##[group]Installing Elixir ${elixirVersion}`)
62 70 await installer.installElixir(elixirVersion)
63 71 core.setOutput('elixir-version', elixirVersion)
64 const disableProblemMatchers = core.getInput('disable_problem_matchers', {
65 required: false,
66 })
72 + const disableProblemMatchers = getInput('disable_problem_matchers', false)
67 73 if (disableProblemMatchers === 'false') {
68 74 const matchersPath = path.join(__dirname, '..', '.github')
69 75 console.log(
@@ -310,7 +316,7 @@ async function getRebar3Versions() {
310 316 }
311 317
312 318 function isStrictVersion() {
313 return core.getInput('version-type', { required: false }) === 'strict'
319 + return getInput('version-type', false) === 'strict'
314 320 }
315 321
316 322 function getVersionFromSpec(spec, versions, maybePrependWithV0) {
@@ -460,10 +466,66 @@ function isVersion(v) {
460 466 return /^v?\d+/.test(v)
461 467 }
462 468
469 +function getInput(inputName, required, alternativeName, alternatives) {
470 + const alternativeValue = (alternatives || new Map()).get(alternativeName)
471 + let input = core.getInput(inputName, {
472 + required: alternativeValue ? false : required,
473 + })
474 + // We can't have both input and alternativeValue set
475 + if (input && alternativeValue) {
476 + throw new Error(
477 + `Found input ${inputName}=${input} (from the YML) \
478 +alongside ${alternativeName}=${alternativeValue} \
479 +(from the version file). You must choose one or the other.`,
480 + )
481 + } else if (!input) {
482 + input = alternativeValue
483 + }
484 + return input
485 +}
486 +
487 +function parseVersionFile(versionFilePath0) {
488 + const versionFilePath = path.join(
489 + process.env.GITHUB_WORKSPACE,
490 + versionFilePath0,
491 + )
492 + if (!fs.existsSync(versionFilePath)) {
493 + throw new Error(
494 + `The specified version file, ${versionFilePath0}, does not exist`,
495 + )
496 + }
497 + console.log(`##[group]Parsing version file at ${versionFilePath0}`)
498 + const appVersions = new Map()
499 + const versions = fs.readFileSync(versionFilePath, 'utf8')
500 + // For the time being we parse .tool-versions
501 + // If we ever start parsing something else, this should
502 + // become default in a new option named e.g. version-file-type
503 + versions.split('\n').forEach((line) => {
504 + const appVersion = line.match(/^([^ ]+)[ ]+([^ #]+)/)
505 + if (appVersion) {
506 + const app = appVersion[1]
507 + if (['erlang', 'elixir', 'gleam', 'rebar'].includes(app)) {
508 + const [, , version] = appVersion
509 + console.log(`Consuming ${app} at version ${version}`)
510 + appVersions.set(app, version)
511 + }
512 + }
513 + })
514 + if (!appVersions.size) {
515 + console.log('There was apparently nothing to consume')
516 + } else {
517 + console.log('... done!')
518 + }
519 + console.log('##[endgroup]')
520 +
521 + return appVersions
522 +}
523 +
463 524 module.exports = {
464 525 getOTPVersion,
465 526 getElixirVersion,
466 527 getGleamVersion,
467 528 getRebar3Version,
468 529 getVersionFromSpec,
530 + parseVersionFile,
469 531 }

Parents: 529221d