diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 524b514e..641bdb56 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -6,7 +6,8 @@ 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 nv from '../src/node-version'; +import * as nvf from '../src/node-version-file'; import * as auth from '../src/authutil'; import {context} from '@actions/github'; @@ -40,6 +41,7 @@ describe('setup-node', () => { let mkdirpSpy: jest.SpyInstance; let execSpy: jest.SpyInstance; let authSpy: jest.SpyInstance; + let parseNodeVersionSpy: jest.SpyInstance; beforeEach(() => { // @actions/core @@ -63,7 +65,8 @@ describe('setup-node', () => { exSpy = jest.spyOn(tc, 'extractTar'); cacheSpy = jest.spyOn(tc, 'cacheDir'); getManifestSpy = jest.spyOn(tc, 'getManifestFromRepo'); - getDistSpy = jest.spyOn(im, 'getVersionsFromDist'); + getDistSpy = jest.spyOn(nv, 'getVersionsFromDist'); + parseNodeVersionSpy = jest.spyOn(nvf, 'parseNodeVersionFile'); // io whichSpy = jest.spyOn(io, 'which'); @@ -79,7 +82,7 @@ describe('setup-node', () => { getManifestSpy.mockImplementation( () => nodeTestManifest ); - getDistSpy.mockImplementation(() => nodeTestDist); + getDistSpy.mockImplementation(() => nodeTestDist); // writes cnSpy = jest.spyOn(process.stdout, 'write'); @@ -124,7 +127,7 @@ describe('setup-node', () => { }); it('can mock dist versions', async () => { - let versions: im.INodeVersion[] = await im.getVersionsFromDist(); + let versions: nv.INodeVersion[] = await nv.getVersionsFromDist(); expect(versions).toBeDefined(); expect(versions?.length).toBe(23); }); @@ -517,10 +520,12 @@ describe('setup-node', () => { // Arrange const versionSpec = 'v12'; const versionFile = '.nvmrc'; + const expectedVersionSpec = '12'; inputs['node-version-file'] = versionFile; readFileSyncSpy.mockImplementation(() => versionSpec); + parseNodeVersionSpy.mockImplementation(() => expectedVersionSpec); // Act await main.run(); @@ -531,8 +536,9 @@ describe('setup-node', () => { path.join(__dirname, '..', versionFile), 'utf8' ); + expect(parseNodeVersionSpy).toHaveBeenCalledWith(versionSpec); expect(logSpy).toHaveBeenCalledWith( - `Resolved ${versionFile} as ${versionSpec}` + `Resolved ${versionFile} as ${expectedVersionSpec}` ); }); }); diff --git a/__tests__/node-version-file.test.ts b/__tests__/node-version-file.test.ts new file mode 100644 index 00000000..fd1d8b88 --- /dev/null +++ b/__tests__/node-version-file.test.ts @@ -0,0 +1,88 @@ +import * as nv from '../src/node-version'; +import * as nvf from '../src/node-version-file'; + +let nodeTestDist = require('./data/node-dist-index.json'); + +describe('node-version-file', () => { + let getVersionsFromDist: jest.SpyInstance; + + beforeEach(() => { + // @actions/core + console.log('::stop-commands::stoptoken'); // Disable executing of runner commands when running tests in actions + + getVersionsFromDist = jest.spyOn(nv, 'getVersionsFromDist'); + + // gets + getVersionsFromDist.mockImplementation(() => nodeTestDist); + }); + + afterEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + //jest.restoreAllMocks(); + }); + + afterAll(async () => { + console.log('::stoptoken::'); // Re-enable executing of runner commands when running tests in actions + }, 100000); + + //-------------------------------------------------- + // Manifest find tests + //-------------------------------------------------- + describe('parseNodeVersionFile', () => { + it('without `v` prefix', async () => { + // Arrange + const versionSpec = '12'; + + // Act + const result = await nvf.parseNodeVersionFile(versionSpec); + + // Assert + expect(result).toBe(versionSpec); + }); + + it('lts/*', async () => { + // Arrange + const versionSpec = 'lts/*'; + + // Act + const result = await nvf.parseNodeVersionFile(versionSpec); + + // Assert + expect(result).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('lts/erbium', async () => { + // Arrange + const versionSpec = 'lts/*'; + + // Act + const result = await nvf.parseNodeVersionFile(versionSpec); + + // Assert + expect(result).toMatch(/\d\.\d\.\d/); + }); + + it('partial syntax like 12', async () => { + // Arrange + const versionSpec = '12'; + + // Act + const result = await nvf.parseNodeVersionFile(versionSpec); + + // Assert + expect(result).toBe(versionSpec); + }); + + it('partial syntax like 12.16', async () => { + // Arrange + const versionSpec = '12.16'; + + // Act + const result = await nvf.parseNodeVersionFile(versionSpec); + + // Assert + expect(result).toBe(versionSpec); + }); + }); +}); diff --git a/dist/index.js b/dist/index.js index 4e24b3d5..2a5ebddf 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3028,6 +3028,82 @@ const windowsRelease = release => { module.exports = windowsRelease; +/***/ }), + +/***/ 74: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const semvar = __importStar(__webpack_require__(280)); +const node_version_1 = __webpack_require__(708); +function parseNodeVersionFile(contents) { + return __awaiter(this, void 0, void 0, function* () { + contents = contents.trim(); + if (/^v\d/.test(contents)) { + contents = contents.substring(1); + } + const nodeVersions = yield node_version_1.getVersionsFromDist(); + let nodeVersion; + if (contents.startsWith('lts/')) { + nodeVersion = findLatestLts(nodeVersions, contents).version; + } + else if (semvar.valid(contents) || isPartialMatch(contents)) { + nodeVersion = contents; + } + else { + throw new Error(`Couldn't resolve node version: '${contents}'`); + } + return stripVPrefix(nodeVersion); + }); +} +exports.parseNodeVersionFile = parseNodeVersionFile; +function findLatestLts(nodeVersions, codename) { + let nodeVersion; + if (codename === 'lts/*') { + nodeVersion = nodeVersions.reduce((latest, nodeVersion) => { + if (!nodeVersion.lts) { + return latest; + } + return semvar.gt(nodeVersion.version, latest.version) + ? nodeVersion + : latest; + }); + } + else { + codename = codename.replace('lts/', '').toLowerCase(); + nodeVersion = nodeVersions.find(nodeVersion => `${nodeVersion.lts}`.toLowerCase() === codename); + } + if (!nodeVersion) { + throw new Error(`Couldn't find matching release for codename: '${codename}'`); + } + return nodeVersion; +} +function isPartialMatch(version) { + return /^\d+(\.\d+(\.\d+)?)?$/.test(version); +} +function stripVPrefix(version) { + return /^v\d/.test(version) ? version.substring(1) : version; +} +//# sourceMappingURL=node-version-file.js.map + /***/ }), /***/ 82: @@ -4695,6 +4771,7 @@ const auth = __importStar(__webpack_require__(202)); const fs = __webpack_require__(747); const path = __importStar(__webpack_require__(622)); const url_1 = __webpack_require__(835); +const node_version_file_1 = __webpack_require__(74); function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -4710,7 +4787,7 @@ function run() { const versionFile = core.getInput('node-version-file'); if (!!versionFile) { const versionFilePath = path.join(__dirname, '..', versionFile); - version = fs.readFileSync(versionFilePath, 'utf8'); + version = yield node_version_file_1.parseNodeVersionFile(fs.readFileSync(versionFilePath, 'utf8')); core.info(`Resolved ${versionFile} as ${version}`); } } @@ -12969,6 +13046,45 @@ module.exports = {"activity":{"checkStarringRepo":{"method":"GET","params":{"own /***/ }), +/***/ 708: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const hc = __importStar(__webpack_require__(539)); +function getVersionsFromDist() { + return __awaiter(this, void 0, void 0, function* () { + let dataUrl = 'https://nodejs.org/dist/index.json'; + let httpClient = new hc.HttpClient('setup-node', [], { + allowRetries: true, + maxRetries: 3 + }); + let response = yield httpClient.getJson(dataUrl); + return response.result || []; + }); +} +exports.getVersionsFromDist = getVersionsFromDist; +//# sourceMappingURL=node-version.js.map + +/***/ }), + /***/ 722: /***/ (function(module) { @@ -13072,7 +13188,7 @@ module.exports = require("fs"); /***/ }), /***/ 749: -/***/ (function(module, exports, __webpack_require__) { +/***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; @@ -13096,12 +13212,12 @@ Object.defineProperty(exports, "__esModule", { value: true }); const os = __webpack_require__(87); const assert = __importStar(__webpack_require__(357)); const core = __importStar(__webpack_require__(470)); -const hc = __importStar(__webpack_require__(539)); const io = __importStar(__webpack_require__(1)); const tc = __importStar(__webpack_require__(533)); const path = __importStar(__webpack_require__(622)); const semver = __importStar(__webpack_require__(280)); const fs = __webpack_require__(747); +const node_version_1 = __webpack_require__(708); function getNode(versionSpec, stable, checkLatest, auth) { return __awaiter(this, void 0, void 0, function* () { let osPlat = os.platform(); @@ -13312,7 +13428,7 @@ function queryDistForMatch(versionSpec) { throw new Error(`Unexpected OS '${osPlat}'`); } let versions = []; - let nodeVersions = yield module.exports.getVersionsFromDist(); + let nodeVersions = yield node_version_1.getVersionsFromDist(); nodeVersions.forEach((nodeVersion) => { // ensure this version supports your os and platform if (nodeVersion.files.indexOf(dataFileName) >= 0) { @@ -13324,18 +13440,6 @@ function queryDistForMatch(versionSpec) { return version; }); } -function getVersionsFromDist() { - return __awaiter(this, void 0, void 0, function* () { - let dataUrl = 'https://nodejs.org/dist/index.json'; - let httpClient = new hc.HttpClient('setup-node', [], { - allowRetries: true, - maxRetries: 3 - }); - let response = yield httpClient.getJson(dataUrl); - return response.result || []; - }); -} -exports.getVersionsFromDist = getVersionsFromDist; // For non LTS versions of Node, the files we need (for Windows) are sometimes located // in a different folder than they normally are for other versions. // Normally the format is similar to: https://nodejs.org/dist/v5.10.1/node-v5.10.1-win-x64.7z diff --git a/src/installer.ts b/src/installer.ts index feb8349c..c009f7b4 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -1,21 +1,12 @@ import os = require('os'); import * as assert from 'assert'; import * as core from '@actions/core'; -import * as hc from '@actions/http-client'; import * as io from '@actions/io'; import * as tc from '@actions/tool-cache'; import * as path from 'path'; import * as semver from 'semver'; import fs = require('fs'); - -// -// Node versions interface -// see https://nodejs.org/dist/index.json -// -export interface INodeVersion { - version: string; - files: string[]; -} +import {INodeVersion, getVersionsFromDist} from './node-version'; interface INodeVersionInfo { downloadUrl: string; @@ -274,7 +265,7 @@ async function queryDistForMatch(versionSpec: string): Promise { } 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 @@ -288,16 +279,6 @@ async function queryDistForMatch(versionSpec: string): Promise { return version; } -export async function getVersionsFromDist(): Promise { - let dataUrl = 'https://nodejs.org/dist/index.json'; - let httpClient = new hc.HttpClient('setup-node', [], { - allowRetries: true, - maxRetries: 3 - }); - let response = await httpClient.getJson(dataUrl); - return response.result || []; -} - // For non LTS versions of Node, the files we need (for Windows) are sometimes located // in a different folder than they normally are for other versions. // Normally the format is similar to: https://nodejs.org/dist/v5.10.1/node-v5.10.1-win-x64.7z diff --git a/src/main.ts b/src/main.ts index 0bea67ad..36565858 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import * as auth from './authutil'; import fs = require('fs'); import * as path from 'path'; import {URL} from 'url'; +import {parseNodeVersionFile} from './node-version-file'; export async function run() { try { @@ -21,7 +22,9 @@ export async function run() { if (!!versionFile) { const versionFilePath = path.join(__dirname, '..', versionFile); - version = fs.readFileSync(versionFilePath, 'utf8'); + version = await parseNodeVersionFile( + fs.readFileSync(versionFilePath, 'utf8') + ); core.info(`Resolved ${versionFile} as ${version}`); } } diff --git a/src/node-version-file.ts b/src/node-version-file.ts new file mode 100644 index 00000000..d2d3568b --- /dev/null +++ b/src/node-version-file.ts @@ -0,0 +1,65 @@ +import * as semvar from 'semver'; +import {INodeVersion, getVersionsFromDist} from './node-version'; + +export async function parseNodeVersionFile(contents: string): Promise { + contents = contents.trim(); + + if (/^v\d/.test(contents)) { + contents = contents.substring(1); + } + + const nodeVersions = await getVersionsFromDist(); + + let nodeVersion: string; + + if (contents.startsWith('lts/')) { + nodeVersion = findLatestLts(nodeVersions, contents).version; + } else if (semvar.valid(contents) || isPartialMatch(contents)) { + nodeVersion = contents; + } else { + throw new Error(`Couldn't resolve node version: '${contents}'`); + } + + return stripVPrefix(nodeVersion); +} + +function findLatestLts( + nodeVersions: INodeVersion[], + codename: string +): INodeVersion { + let nodeVersion: INodeVersion | undefined; + + if (codename === 'lts/*') { + nodeVersion = nodeVersions.reduce((latest, nodeVersion) => { + if (!nodeVersion.lts) { + return latest; + } + + return semvar.gt(nodeVersion.version, latest.version) + ? nodeVersion + : latest; + }); + } else { + codename = codename.replace('lts/', '').toLowerCase(); + + nodeVersion = nodeVersions.find( + nodeVersion => `${nodeVersion.lts}`.toLowerCase() === codename + ); + } + + if (!nodeVersion) { + throw new Error( + `Couldn't find matching release for codename: '${codename}'` + ); + } + + return nodeVersion; +} + +function isPartialMatch(version: string): boolean { + return /^\d+(\.\d+(\.\d+)?)?$/.test(version); +} + +function stripVPrefix(version: string): string { + return /^v\d/.test(version) ? version.substring(1) : version; +} diff --git a/src/node-version.ts b/src/node-version.ts new file mode 100644 index 00000000..eb49b049 --- /dev/null +++ b/src/node-version.ts @@ -0,0 +1,21 @@ +import * as hc from '@actions/http-client'; + +// +// Node versions interface +// see https://nodejs.org/dist/index.json +// +export interface INodeVersion { + version: string; + files: string[]; + lts: boolean | string; +} + +export async function getVersionsFromDist(): Promise { + let dataUrl = 'https://nodejs.org/dist/index.json'; + let httpClient = new hc.HttpClient('setup-node', [], { + allowRetries: true, + maxRetries: 3 + }); + let response = await httpClient.getJson(dataUrl); + return response.result || []; +}