diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml index cb2b210e6..459f33cdc 100644 --- a/.github/workflows/versions.yml +++ b/.github/workflows/versions.yml @@ -78,6 +78,21 @@ jobs: run: __tests__/verify-node.sh "${{ matrix.node-version }}" shell: bash + version-file: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v2 + - name: Setup node from node version file + uses: ./ + with: + node-version-file: '__tests__/data/.nvmrc' + - name: Verify node + run: __tests__/verify-node.sh 14 + node-dist: runs-on: ${{ matrix.os }} strategy: diff --git a/README.md b/README.md index 8b405f9ea..3e01b7fe2 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,13 @@ jobs: ## Advanced usage 1. [Check latest version](docs/advanced-usage.md#check-latest-version) -2. [Using different architectures](docs/advanced-usage.md#architecture) -3. [Caching packages dependencies](docs/advanced-usage.md#caching-packages-dependencies) -4. [Using multiple operating systems and architectures](docs/advanced-usage.md#multiple-operating-systems-and-architectures) -5. [Publishing to npmjs and GPR with npm](docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-npm) -6. [Publishing to npmjs and GPR with yarn](docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-yarn) -7. [Using private packages](docs/advanced-usage.md#use-private-packages) +2. [Using a node version file](docs/advanced-usage.md#node-version-file) +3. [Using different architectures](docs/advanced-usage.md#architecture) +4. [Caching packages dependencies](docs/advanced-usage.md#caching-packages-dependencies) +5. [Using multiple operating systems and architectures](docs/advanced-usage.md#multiple-operating-systems-and-architectures) +6. [Publishing to npmjs and GPR with npm](docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-npm) +7. [Publishing to npmjs and GPR with yarn](docs/advanced-usage.md#publish-to-npmjs-and-gpr-with-yarn) +8. [Using private packages](docs/advanced-usage.md#use-private-packages) # License diff --git a/__tests__/data/.nvmrc b/__tests__/data/.nvmrc new file mode 100644 index 000000000..ca3f1e5c8 --- /dev/null +++ b/__tests__/data/.nvmrc @@ -0,0 +1 @@ +v14 \ No newline at end of file diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 994892365..4565dd4ce 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -1,12 +1,12 @@ import * as core from '@actions/core'; import * as io from '@actions/io'; import * as tc from '@actions/tool-cache'; +import * as im from '../src/installer'; import fs from 'fs'; import cp from 'child_process'; import osm = require('os'); import path from 'path'; import * as main from '../src/main'; -import * as im from '../src/installer'; import * as auth from '../src/authutil'; let nodeTestManifest = require('./data/versions-manifest.json'); @@ -31,9 +31,11 @@ describe('setup-node', () => { let dbgSpy: jest.SpyInstance; let whichSpy: jest.SpyInstance; let existsSpy: jest.SpyInstance; + let readFileSyncSpy: jest.SpyInstance; let mkdirpSpy: jest.SpyInstance; let execSpy: jest.SpyInstance; let authSpy: jest.SpyInstance; + let parseNodeVersionSpy: jest.SpyInstance; beforeEach(() => { // @actions/core @@ -58,6 +60,7 @@ describe('setup-node', () => { cacheSpy = jest.spyOn(tc, 'cacheDir'); getManifestSpy = jest.spyOn(tc, 'getManifestFromRepo'); getDistSpy = jest.spyOn(im, 'getVersionsFromDist'); + parseNodeVersionSpy = jest.spyOn(im, 'parseNodeVersionFile'); // io whichSpy = jest.spyOn(io, 'which'); @@ -91,6 +94,10 @@ describe('setup-node', () => { // uncomment to see debug output // process.stderr.write(msg + '\n'); }); + warningSpy.mockImplementation(msg => { + // uncomment to debug + // process.stderr.write('log:' + line + '\n'); + }); }); afterEach(() => { @@ -101,6 +108,7 @@ describe('setup-node', () => { afterAll(async () => { console.log('::stoptoken::'); // Re-enable executing of runner commands when running tests in actions + jest.restoreAllMocks(); }, 100000); //-------------------------------------------------- @@ -343,7 +351,7 @@ describe('setup-node', () => { expect(cnSpy).toHaveBeenCalledWith(`::error::${errMsg}${osm.EOL}`); }); - it('Acquires specified architecture of node', async () => { + it('acquires specified architecture of node', async () => { for (const {arch, version, osSpec} of [ {arch: 'x86', version: '12.16.2', osSpec: 'win32'}, {arch: 'x86', version: '14.0.0', osSpec: 'win32'} @@ -549,6 +557,93 @@ describe('setup-node', () => { }); }); + describe('node-version-file flag', () => { + it('not used if node-version is provided', async () => { + // Arrange + inputs['node-version'] = '12'; + + // Act + await main.run(); + + // Assert + expect(parseNodeVersionSpy).toHaveBeenCalledTimes(0); + }); + + it('not used if node-version-file not provided', async () => { + // Act + await main.run(); + + // Assert + expect(parseNodeVersionSpy).toHaveBeenCalledTimes(0); + }); + + it('reads node-version-file if provided', async () => { + // Arrange + const versionSpec = 'v14'; + const versionFile = '.nvmrc'; + const expectedVersionSpec = '14'; + process.env['GITHUB_WORKSPACE'] = path.join(__dirname, 'data'); + inputs['node-version-file'] = versionFile; + + parseNodeVersionSpy.mockImplementation(() => expectedVersionSpec); + existsSpy.mockImplementationOnce( + input => input === path.join(__dirname, 'data', versionFile) + ); + // Act + await main.run(); + + // Assert + expect(existsSpy).toHaveBeenCalledTimes(1); + expect(existsSpy).toHaveReturnedWith(true); + expect(parseNodeVersionSpy).toHaveBeenCalledWith(versionSpec); + expect(logSpy).toHaveBeenCalledWith( + `Resolved ${versionFile} as ${expectedVersionSpec}` + ); + }); + + it('both node-version-file and node-version are provided', async () => { + inputs['node-version'] = '12'; + const versionSpec = 'v14'; + const versionFile = '.nvmrc'; + const expectedVersionSpec = '14'; + process.env['GITHUB_WORKSPACE'] = path.join(__dirname, '..'); + inputs['node-version-file'] = versionFile; + + parseNodeVersionSpy.mockImplementation(() => expectedVersionSpec); + + // Act + await main.run(); + + // Assert + expect(existsSpy).toHaveBeenCalledTimes(0); + expect(parseNodeVersionSpy).not.toHaveBeenCalled(); + expect(warningSpy).toHaveBeenCalledWith( + 'Both node-version and node-version-file inputs are specified, only node-version will be used' + ); + }); + + it('should throw an error if node-version-file is not found', async () => { + const versionFile = '.nvmrc'; + const versionFilePath = path.join(__dirname, '..', versionFile); + inputs['node-version-file'] = versionFile; + + inSpy.mockImplementation(name => inputs[name]); + existsSpy.mockImplementationOnce( + input => input === path.join(__dirname, 'data', versionFile) + ); + + // Act + await main.run(); + + // Assert + expect(existsSpy).toHaveBeenCalled(); + expect(existsSpy).toHaveReturnedWith(false); + expect(parseNodeVersionSpy).not.toHaveBeenCalled(); + expect(cnSpy).toHaveBeenCalledWith( + `::error::The specified node version file at: ${versionFilePath} does not exist${osm.EOL}` + ); + }); + }); describe('LTS version', () => { beforeEach(() => { os.platform = 'linux'; diff --git a/action.yml b/action.yml index e50fa0e62..879ac00b5 100644 --- a/action.yml +++ b/action.yml @@ -7,6 +7,8 @@ inputs: default: 'false' node-version: description: 'Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0' + node-version-file: + description: 'File containing the version Spec of the version to use. Examples: .nvmrc, .node-version' architecture: description: 'Target architecture for Node to use. Examples: x86, x64. Will use system architecture by default.' check-latest: diff --git a/dist/setup/index.js b/dist/setup/index.js index c7a407d0f..7e637f09e 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -6938,9 +6938,13 @@ var __importStar = (this && this.__importStar) || function (mod) { result["default"] = mod; return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); const installer = __importStar(__webpack_require__(923)); +const fs_1 = __importDefault(__webpack_require__(747)); const auth = __importStar(__webpack_require__(749)); const path = __importStar(__webpack_require__(622)); const cache_restore_1 = __webpack_require__(409); @@ -6953,10 +6957,7 @@ function run() { // Version is optional. If supplied, install / use from the tool cache // If not supplied then task is still used to setup proxy, auth, etc... // - let version = core.getInput('node-version'); - if (!version) { - version = core.getInput('version'); - } + let version = resolveVersionInput(); let arch = core.getInput('architecture'); const cache = core.getInput('cache'); // if architecture supplied but node-version is not @@ -6991,8 +6992,8 @@ function run() { core.info(`##[add-matcher]${path.join(matchersPath, 'eslint-stylish.json')}`); core.info(`##[add-matcher]${path.join(matchersPath, 'eslint-compact.json')}`); } - catch (error) { - core.setFailed(error.message); + catch (err) { + core.setFailed(err.message); } }); } @@ -7001,6 +7002,25 @@ function isGhes() { const ghUrl = new url_1.URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'; } +function resolveVersionInput() { + let version = core.getInput('node-version') || core.getInput('version'); + const versionFileInput = core.getInput('node-version-file'); + if (version && versionFileInput) { + core.warning('Both node-version and node-version-file inputs are specified, only node-version will be used'); + } + if (version) { + return version; + } + if (versionFileInput) { + const versionFilePath = path.join(process.env.GITHUB_WORKSPACE, versionFileInput); + if (!fs_1.default.existsSync(versionFilePath)) { + throw new Error(`The specified node version file at: ${versionFilePath} does not exist`); + } + version = installer.parseNodeVersionFile(fs_1.default.readFileSync(versionFilePath, 'utf8')); + core.info(`Resolved ${versionFileInput} as ${version}`); + } + return version; +} /***/ }), @@ -65340,7 +65360,7 @@ exports.NOOP_TEXT_MAP_PROPAGATOR = new NoopTextMapPropagator(); /* 921 */, /* 922 */, /* 923 */ -/***/ (function(module, exports, __webpack_require__) { +/***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; @@ -65617,7 +65637,7 @@ function queryDistForMatch(versionSpec, arch = os.arch()) { throw new Error(`Unexpected OS '${osPlat}'`); } let versions = []; - let nodeVersions = yield module.exports.getVersionsFromDist(); + let nodeVersions = yield getVersionsFromDist(); nodeVersions.forEach((nodeVersion) => { // ensure this version supports your os and platform if (nodeVersion.files.indexOf(dataFileName) >= 0) { @@ -65705,6 +65725,14 @@ function translateArchToDistUrl(arch) { return arch; } } +function parseNodeVersionFile(contents) { + let nodeVersion = contents.trim(); + if (/^v\d/.test(nodeVersion)) { + nodeVersion = nodeVersion.substring(1); + } + return nodeVersion; +} +exports.parseNodeVersionFile = parseNodeVersionFile; /***/ }), diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index c84918fab..960772f5c 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -19,6 +19,22 @@ steps: - run: npm test ``` +## Node version file + +The `node-version-file` input accepts a path to a file containing the version of Node.js to be used by a project, for example `.nvmrc` or `.node-version`. If both the `node-version` and the `node-version-file` inputs are provided then the `node-version` input is used. +See [supported version syntax](https://github.com/actions/setup-node#supported-version-syntax) +> The action will search for the node version file relative to the repository root. + +```yaml +steps: +- uses: actions/checkout@v2 +- uses: actions/setup-node@v2 + with: + node-version-file: '.nvmrc' +- run: npm install +- run: npm test +``` + ## Architecture You can use any of the [supported operating systems](https://docs.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners), and the compatible `architecture` can be selected using `architecture`. Values are `x86`, `x64`, `arm64`, `armv6l`, `armv7l`, `ppc64le`, `s390x` (not all of the architectures are available on all platforms). diff --git a/src/installer.ts b/src/installer.ts index e2084b95a..a9baae0a1 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -371,7 +371,7 @@ async function queryDistForMatch( } let versions: string[] = []; - let nodeVersions = await module.exports.getVersionsFromDist(); + let nodeVersions = await getVersionsFromDist(); nodeVersions.forEach((nodeVersion: INodeVersion) => { // ensure this version supports your os and platform @@ -464,3 +464,12 @@ function translateArchToDistUrl(arch: string): string { return arch; } } + +export function parseNodeVersionFile(contents: string): string { + let nodeVersion = contents.trim(); + + if (/^v\d/.test(nodeVersion)) { + nodeVersion = nodeVersion.substring(1); + } + return nodeVersion; +} diff --git a/src/main.ts b/src/main.ts index 956bc1449..58c902b85 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core'; import * as installer from './installer'; +import fs from 'fs'; import * as auth from './authutil'; import * as path from 'path'; import {restoreCache} from './cache-restore'; @@ -12,10 +13,7 @@ export async function run() { // Version is optional. If supplied, install / use from the tool cache // If not supplied then task is still used to setup proxy, auth, etc... // - let version = core.getInput('node-version'); - if (!version) { - version = core.getInput('version'); - } + let version = resolveVersionInput(); let arch = core.getInput('architecture'); const cache = core.getInput('cache'); @@ -63,8 +61,8 @@ export async function run() { core.info( `##[add-matcher]${path.join(matchersPath, 'eslint-compact.json')}` ); - } catch (error) { - core.setFailed(error.message); + } catch (err) { + core.setFailed(err.message); } } @@ -74,3 +72,36 @@ function isGhes(): boolean { ); return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'; } + +function resolveVersionInput(): string { + let version = core.getInput('node-version') || core.getInput('version'); + const versionFileInput = core.getInput('node-version-file'); + + if (version && versionFileInput) { + core.warning( + 'Both node-version and node-version-file inputs are specified, only node-version will be used' + ); + } + + if (version) { + return version; + } + + if (versionFileInput) { + const versionFilePath = path.join( + process.env.GITHUB_WORKSPACE!, + versionFileInput + ); + if (!fs.existsSync(versionFilePath)) { + throw new Error( + `The specified node version file at: ${versionFilePath} does not exist` + ); + } + version = installer.parseNodeVersionFile( + fs.readFileSync(versionFilePath, 'utf8') + ); + core.info(`Resolved ${versionFileInput} as ${version}`); + } + + return version; +}