From 62a25ae2c272a51760bad28488f1e36ff555e683 Mon Sep 17 00:00:00 2001 From: "Jimi (Dimitris) Charalampidis" Date: Fri, 13 Sep 2019 13:15:45 +0300 Subject: [PATCH] Add support for node version codename Refactored for optimization and maintainability --- __tests__/__snapshots__/authutil.test.ts.snap | 10 +- __tests__/authutil.test.ts | 47 ++- __tests__/installer.test.ts | 296 +++++++++++++----- action.yml | 1 - lib/authutil.js | 4 +- lib/installer.js | 226 ++++++------- lib/setup-node.js | 14 +- src/authutil.ts | 4 +- src/installer.ts | 291 ++++++++--------- src/setup-node.ts | 15 +- 10 files changed, 507 insertions(+), 401 deletions(-) diff --git a/__tests__/__snapshots__/authutil.test.ts.snap b/__tests__/__snapshots__/authutil.test.ts.snap index c142cf4aa..7bf76e6f3 100644 --- a/__tests__/__snapshots__/authutil.test.ts.snap +++ b/__tests__/__snapshots__/authutil.test.ts.snap @@ -1,30 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`installer tests Appends trailing slash to registry 1`] = ` +exports[`auth tests Appends trailing slash to registry 1`] = ` "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN} registry=https://registry.npmjs.org/ always-auth=false" `; -exports[`installer tests Automatically configures GPR scope 1`] = ` +exports[`auth tests Automatically configures GPR scope 1`] = ` "npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN} @ownername:registry=npm.pkg.github.com/ always-auth=false" `; -exports[`installer tests Configures scoped npm registries 1`] = ` +exports[`auth tests Configures scoped npm registries 1`] = ` "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN} @myscope:registry=https://registry.npmjs.org/ always-auth=false" `; -exports[`installer tests Sets up npmrc for always-auth true 1`] = ` +exports[`auth tests Sets up npmrc for always-auth true 1`] = ` "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN} registry=https://registry.npmjs.org/ always-auth=true" `; -exports[`installer tests Sets up npmrc for npmjs 1`] = ` +exports[`auth tests Sets up npmrc for npmjs 1`] = ` "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN} registry=https://registry.npmjs.org/ always-auth=false" diff --git a/__tests__/authutil.test.ts b/__tests__/authutil.test.ts index c75d5b65a..58545396b 100644 --- a/__tests__/authutil.test.ts +++ b/__tests__/authutil.test.ts @@ -1,25 +1,20 @@ -import io = require('@actions/io'); -import fs = require('fs'); -import path = require('path'); - -const tempDir = path.join( - __dirname, - 'runner', - path.join( - Math.random() - .toString(36) - .substring(7) - ), - 'temp' -); - +import * as io from '@actions/io'; +import * as fs from 'fs'; +import * as path from'path'; + +const runnerDir = path.join(__dirname, 'runner'); +const randomizer = () => + Math.random() + .toString(36) + .substring(7); +const tempDir = path.join(runnerDir, randomizer(), 'temp'); const rcFile = path.join(tempDir, '.npmrc'); process.env['GITHUB_REPOSITORY'] = 'OwnerName/repo'; process.env['RUNNER_TEMP'] = tempDir; import * as auth from '../src/authutil'; -describe('installer tests', () => { +describe('auth tests', () => { beforeAll(async () => { await io.rmRF(tempDir); await io.mkdirP(tempDir); @@ -32,36 +27,36 @@ describe('installer tests', () => { process.env['INPUT_SCOPE'] = ''; }); - it('Sets up npmrc for npmjs', async () => { - await auth.configAuthentication('https://registry.npmjs.org/', 'false'); + it('Sets up npmrc for npmjs', () => { + auth.configAuthentication('https://registry.npmjs.org/', 'false'); expect(fs.existsSync(rcFile)).toBe(true); expect(fs.readFileSync(rcFile, {encoding: 'utf8'})).toMatchSnapshot(); }); - it('Appends trailing slash to registry', async () => { - await auth.configAuthentication('https://registry.npmjs.org', 'false'); + it('Appends trailing slash to registry', () => { + auth.configAuthentication('https://registry.npmjs.org', 'false'); expect(fs.existsSync(rcFile)).toBe(true); expect(fs.readFileSync(rcFile, {encoding: 'utf8'})).toMatchSnapshot(); }); - it('Configures scoped npm registries', async () => { + it('Configures scoped npm registries', () => { process.env['INPUT_SCOPE'] = 'myScope'; - await auth.configAuthentication('https://registry.npmjs.org', 'false'); + auth.configAuthentication('https://registry.npmjs.org', 'false'); expect(fs.existsSync(rcFile)).toBe(true); expect(fs.readFileSync(rcFile, {encoding: 'utf8'})).toMatchSnapshot(); }); - it('Automatically configures GPR scope', async () => { - await auth.configAuthentication('npm.pkg.github.com', 'false'); + it('Automatically configures GPR scope', () => { + auth.configAuthentication('npm.pkg.github.com', 'false'); expect(fs.existsSync(rcFile)).toBe(true); expect(fs.readFileSync(rcFile, {encoding: 'utf8'})).toMatchSnapshot(); }); - it('Sets up npmrc for always-auth true', async () => { - await auth.configAuthentication('https://registry.npmjs.org/', 'true'); + it('Sets up npmrc for always-auth true', () => { + auth.configAuthentication('https://registry.npmjs.org/', 'true'); expect(fs.existsSync(rcFile)).toBe(true); expect(fs.readFileSync(rcFile, {encoding: 'utf8'})).toMatchSnapshot(); }); diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index e0ada3252..5d5b0141e 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -1,123 +1,255 @@ -import io = require('@actions/io'); -import fs = require('fs'); -import os = require('os'); -import path = require('path'); - -const toolDir = path.join( - __dirname, - 'runner', - path.join( - Math.random() - .toString(36) - .substring(7) - ), - 'tools' -); -const tempDir = path.join( - __dirname, - 'runner', - path.join( - Math.random() - .toString(36) - .substring(7) - ), - 'temp' -); +import * as io from '@actions/io'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as restm from 'typed-rest-client/RestClient'; + +const IS_WINDOWS = process.platform === 'win32'; +const osArch = os.arch(); +const runnerDir = path.join(__dirname, 'runner'); +const randomizer = () => + Math.random() + .toString(36) + .substring(7); +const toolDir = path.join(runnerDir, randomizer(), 'tools'); +const tempDir = path.join(runnerDir, randomizer(), 'temp'); process.env['RUNNER_TOOL_CACHE'] = toolDir; process.env['RUNNER_TEMP'] = tempDir; import * as installer from '../src/installer'; -const IS_WINDOWS = process.platform === 'win32'; - describe('installer tests', () => { beforeAll(async () => { await io.rmRF(toolDir); await io.rmRF(tempDir); }, 100000); - it('Acquires version of node if no matching version is installed', async () => { - await installer.getNode('10.16.0'); - const nodeDir = path.join(toolDir, 'node', '10.16.0', os.arch()); - - expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(nodeDir, 'bin', 'node'))).toBe(true); - } - }, 100000); - if (IS_WINDOWS) { - it('Falls back to backup location if first one doesnt contain correct version', async () => { + it(`Falls back to backup location if first one doesn't contain correct version`, async () => { await installer.getNode('5.10.1'); - const nodeDir = path.join(toolDir, 'node', '5.10.1', os.arch()); + const nodeDir = path.join(toolDir, 'node', '5.10.1', osArch); expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); }, 100000); - it('Falls back to third location if second one doesnt contain correct version', async () => { + it(`Falls back to third location if second one doesn't contain correct version`, async () => { await installer.getNode('0.12.18'); - const nodeDir = path.join(toolDir, 'node', '0.12.18', os.arch()); + const nodeDir = path.join(toolDir, 'node', '0.12.18', osArch); expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); }, 100000); } - it('Throws if no location contains correct node version', async () => { - let thrown = false; - try { - await installer.getNode('1000'); - } catch { - thrown = true; - } - expect(thrown).toBe(true); - }); - - it('Acquires version of node with long paths', async () => { - const toolpath = await installer.getNode('8.8.1'); - const nodeDir = path.join(toolDir, 'node', '8.8.1', os.arch()); - - expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(nodeDir, 'bin', 'node'))).toBe(true); - } - }, 100000); - it('Uses version of node installed in cache', async () => { - const nodeDir: string = path.join(toolDir, 'node', '250.0.0', os.arch()); + const nodeDir: string = path.join(toolDir, 'node', '250.0.0', osArch); await io.mkdirP(nodeDir); fs.writeFileSync(`${nodeDir}.complete`, 'hello'); // This will throw if it doesn't find it in the cache (because no such version exists) await installer.getNode('250.0.0'); - return; }); - it('Doesnt use version of node that was only partially installed in cache', async () => { - const nodeDir: string = path.join(toolDir, 'node', '251.0.0', os.arch()); + it(`Doesn't use version of node that was only partially installed in cache`, async () => { + const nodeDir: string = path.join(toolDir, 'node', '251.0.0', osArch); await io.mkdirP(nodeDir); - let thrown = false; try { // This will throw if it doesn't find it in the cache (because no such version exists) await installer.getNode('251.0.0'); - } catch { - thrown = true; + } catch (error) { + expect(error).toBeInstanceOf(Error); } - expect(thrown).toBe(true); - return; }); - it('Resolves semantic versions of node installed in cache', async () => { - const nodeDir: string = path.join(toolDir, 'node', '252.0.0', os.arch()); - await io.mkdirP(nodeDir); - fs.writeFileSync(`${nodeDir}.complete`, 'hello'); - // These will throw if it doesn't find it in the cache (because no such version exists) - await installer.getNode('252.0.0'); - await installer.getNode('252'); - await installer.getNode('252.0'); + describe('Throws', () => { + it('on unknown node version codename', async () => { + try { + await installer.getNode('potterium'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + } + }); + + it('if no location contains correct node version', async () => { + try { + await installer.getNode('1000'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + } + }); + }); + + describe(`Acquires version of node if no matching version is installed of pattern`, () => { + it(`'x'`, async () => { + await installer.getNode('6'); + const nodeDir = path.join(toolDir, 'node', '6.17.1', osArch); + + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + if (IS_WINDOWS) { + expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); + } else { + expect(fs.existsSync(path.join(nodeDir, 'bin', 'node'))).toBe(true); + } + }, 100000); + + it(`'x.x'`, async () => { + await installer.getNode('6.x'); + const nodeDir = path.join(toolDir, 'node', '6.17.1', osArch); + + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + if (IS_WINDOWS) { + expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); + } else { + expect(fs.existsSync(path.join(nodeDir, 'bin', 'node'))).toBe(true); + } + }, 100000); + + it(`'x.x.x'`, async () => { + await installer.getNode('6.17.1'); + const nodeDir = path.join(toolDir, 'node', '6.17.1', osArch); + + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + if (IS_WINDOWS) { + expect(fs.existsSync(path.join(nodeDir, 'node.exe'))).toBe(true); + } else { + expect(fs.existsSync(path.join(nodeDir, 'bin', 'node'))).toBe(true); + } + }, 100000); + }); + + describe(`Resolves`, () => { + let getLatestNodeVersionOf: ( + predicator: (...args: any[]) => boolean + ) => string; + + beforeAll(async () => { + const restClient = new restm.RestClient('setup-node'); + const request = await restClient.get( + 'https://nodejs.org/dist/index.json' + ); + const nodeVersions = request.result || []; + const osPlat = IS_WINDOWS + ? 'win' + : process.platform === 'darwin' + ? 'osx' + : 'linux'; + + getLatestNodeVersionOf = (predicator: (...args: any[]) => boolean) => { + const osPlatArchRegExp = new RegExp( + `^${osPlat}-${osArch}-?(?:7z|tar)?` + ); + const version = nodeVersions + .filter((version: installer.INodeVersion) => + version.files.some((file: string) => osPlatArchRegExp.test(file)) + ) + .filter(predicator) + .sort((a: installer.INodeVersion, b: installer.INodeVersion) => + semver.gt(b.version, a.version) ? 1 : -1 + )[0].version; + return semver.clean(version) || ''; + }; + }); + + it('semantic versions of node installed in cache', async () => { + const nodeDir: string = path.join(toolDir, 'node', '252.0.0', osArch); + await io.mkdirP(nodeDir); + fs.writeFileSync(`${nodeDir}.complete`, 'hello'); + // These will throw if it doesn't find it in the cache (because no such version exists) + await installer.getNode('252.0.0'); + await installer.getNode('252'); + await installer.getNode('252.0'); + }); + + it(`'latest' to latest node version`, async () => { + await installer.getNode('latest'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf( + (nv: installer.INodeVersion) => typeof nv.lts !== 'string' + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); + + it(`'current' to current node version`, async () => { + await installer.getNode('current'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf( + (nv: installer.INodeVersion) => typeof nv.lts !== 'string' + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); + + it(`'lts' to latest LTS node version`, async () => { + await installer.getNode('lts'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf( + (nv: installer.INodeVersion) => typeof nv.lts === 'string' + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); + + it(`'argon' to latest node version 4`, async () => { + await installer.getNode('argon'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf((nv: installer.INodeVersion) => + /v4.\d+.\d+/.test(nv.version) + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); + + it(`'boron' to latest node version 6`, async () => { + await installer.getNode('boron'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf((nv: installer.INodeVersion) => + /v6.\d+.\d+/.test(nv.version) + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); + + it(`'carbon' to latest node version 8`, async () => { + await installer.getNode('carbon'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf((nv: installer.INodeVersion) => + /v8.\d+.\d+/.test(nv.version) + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); + + it(`'dubnium' to latest node version 10`, async () => { + await installer.getNode('dubnium'); + const nodeDir = path.join( + toolDir, + 'node', + getLatestNodeVersionOf((nv: installer.INodeVersion) => + /v10.\d+.\d+/.test(nv.version) + ), + osArch + ); + expect(fs.existsSync(`${nodeDir}.complete`)).toBe(true); + }, 100000); }); }); diff --git a/action.yml b/action.yml index d6feb020a..35b4c6bbe 100644 --- a/action.yml +++ b/action.yml @@ -7,7 +7,6 @@ inputs: default: 'false' node-version: description: 'Version Spec of the version to use. Examples: 10.x, 10.15.1, >=10.15.0' - default: '10.x' registry-url: description: 'Optional registry to set up for auth. Will set the registry in a project level .npmrc and .yarnrc file, and set up auth to read in from env.NODE_AUTH_TOKEN' scope: diff --git a/lib/authutil.js b/lib/authutil.js index 6da4630b6..dc0aaff4b 100644 --- a/lib/authutil.js +++ b/lib/authutil.js @@ -7,11 +7,11 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const github = __importStar(require("@actions/github")); const fs = __importStar(require("fs")); const os = __importStar(require("os")); const path = __importStar(require("path")); -const core = __importStar(require("@actions/core")); -const github = __importStar(require("@actions/github")); function configAuthentication(registryUrl, alwaysAuth) { const npmrc = path.resolve(process.env['RUNNER_TEMP'] || process.cwd(), '.npmrc'); if (!registryUrl.endsWith('/')) { diff --git a/lib/installer.js b/lib/installer.js index c86924c1d..96d68b940 100644 --- a/lib/installer.js +++ b/lib/installer.js @@ -15,170 +15,141 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -// Load tempDirectory before it gets wiped by tool-cache -let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || ''; const core = __importStar(require("@actions/core")); const io = __importStar(require("@actions/io")); const tc = __importStar(require("@actions/tool-cache")); -const restm = __importStar(require("typed-rest-client/RestClient")); const os = __importStar(require("os")); const path = __importStar(require("path")); const semver = __importStar(require("semver")); +const restm = __importStar(require("typed-rest-client/RestClient")); let osPlat = os.platform(); let osArch = os.arch(); -if (!tempDirectory) { - let baseLocation; - if (process.platform === 'win32') { - // On windows use the USERPROFILE env variable - baseLocation = process.env['USERPROFILE'] || 'C:\\'; - } - else { - if (process.platform === 'darwin') { - baseLocation = '/Users'; - } - else { - baseLocation = '/home'; - } - } - tempDirectory = path.join(baseLocation, 'actions', 'temp'); -} +const IS_WINDOWS = osPlat === 'win32'; function getNode(versionSpec) { return __awaiter(this, void 0, void 0, function* () { + versionSpec = versionSpec.trim(); + // resolve node codenames + let version = yield resolve(versionSpec); + if (!version) { + throw new Error(`Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.`); + } // check cache - let toolPath; - toolPath = tc.find('node', versionSpec); - // If not found in cache, download + let toolPath = tc.find('node', version, osArch); + // Not found in cache -> download if (!toolPath) { - let version; - const c = semver.clean(versionSpec) || ''; - // If explicit version - if (semver.valid(c) != null) { - // version to download - version = versionSpec; - } - else { - // query nodejs.org for a matching version - version = yield queryLatestMatch(versionSpec); - if (!version) { - throw new Error(`Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.`); - } - // check cache - toolPath = tc.find('node', version); - } - if (!toolPath) { - // download, extract, cache - toolPath = yield acquireNode(version); - } + // download, extract, cache + toolPath = yield acquireNode(version); } - // // a tool installer initimately knows details about the layout of that tool // for example, node binary is in the bin folder after the extract on Mac/Linux. // layouts could change by version, by platform etc... but that's the tool installers job - // - if (osPlat != 'win32') { + if (!IS_WINDOWS) { toolPath = path.join(toolPath, 'bin'); } - // // prepend the tools path. instructs the agent to prepend for future tasks core.addPath(toolPath); }); } exports.getNode = getNode; -function queryLatestMatch(versionSpec) { +function resolve(versionSpec) { return __awaiter(this, void 0, void 0, function* () { + let version = semver.clean(versionSpec) || ''; + return semver.valid(version) || tc.find('node', versionSpec, osArch) + ? version || versionSpec + : queryNodeVersions(versionSpec); + }); +} +function queryNodeVersions(versionSpec) { + return __awaiter(this, void 0, void 0, function* () { + core.debug(`querying Node.js for ${versionSpec}`); // node offers a json list of versions + let dataUrl = 'https://nodejs.org/dist/index.json'; + let rest = new restm.RestClient('setup-node'); + let nodeVersions = (yield rest.get(dataUrl)).result || []; let dataFileName; switch (osPlat) { case 'linux': - dataFileName = 'linux-' + osArch; + dataFileName = `linux-${osArch}`; break; case 'darwin': - dataFileName = 'osx-' + osArch + '-tar'; + dataFileName = `osx-${osArch}-tar`; break; case 'win32': - dataFileName = 'win-' + osArch + '-exe'; + dataFileName = `win-${osArch}-7z`; break; default: throw new Error(`Unexpected OS '${osPlat}'`); } - let versions = []; - let dataUrl = 'https://nodejs.org/dist/index.json'; - let rest = new restm.RestClient('setup-node'); - let nodeVersions = (yield rest.get(dataUrl)).result || []; - nodeVersions.forEach((nodeVersion) => { - // ensure this version supports your os and platform - if (nodeVersion.files.indexOf(dataFileName) >= 0) { - versions.push(nodeVersion.version); - } - }); + // ensure this version supports your os and platform + nodeVersions = nodeVersions.filter((nodeVersion) => nodeVersion.files.indexOf(dataFileName) > -1); + // sort node versions by descending version + nodeVersions = nodeVersions.sort((a, b) => semver.gt(b.version, a.version) ? 1 : -1); + const isLatestSpec = /^latest|current$/i.test(versionSpec); + const isLTSSpec = /^lts$/i.test(versionSpec); + const isLTSCodenameSpec = !isLatestSpec && !isLTSSpec && /^[a-zA-Z]+$/.test(versionSpec); + const findNodeVersion = (predicator) => { + nodeVersions = nodeVersions.filter(predicator); + return nodeVersions.length + ? semver.clean(nodeVersions[0].version) || '' + : ''; + }; + // resolve latest or current node version + if (isLatestSpec) { + return findNodeVersion((nodeVersion) => typeof nodeVersion.lts !== 'string'); + } + // resolve lts node version + if (isLTSSpec) { + return findNodeVersion((nodeVersion) => typeof nodeVersion.lts === 'string'); + } + // resolve node version codename + if (isLTSCodenameSpec) { + return findNodeVersion((nodeVersion) => typeof nodeVersion.lts === 'string' && + nodeVersion.lts.toLowerCase() === versionSpec.toLowerCase()); + } // get the latest version that matches the version spec - let version = evaluateVersions(versions, versionSpec); - return version; + return evaluateVersions(nodeVersions, versionSpec); }); } // TODO - should we just export this from @actions/tool-cache? Lifted directly from there -function evaluateVersions(versions, versionSpec) { - let version = ''; - core.debug(`evaluating ${versions.length} versions`); - versions = versions.sort((a, b) => { - if (semver.gt(a, b)) { - return 1; - } - return -1; - }); - for (let i = versions.length - 1; i >= 0; i--) { - const potential = versions[i]; - const satisfied = semver.satisfies(potential, versionSpec); - if (satisfied) { - version = potential; - break; - } - } +function evaluateVersions(nodeVersions, versionSpec) { + core.debug(`evaluating ${nodeVersions.length} versions`); + const versions = nodeVersions.map((nodeVersion) => nodeVersion.version); + const version = versions.find((potential) => semver.satisfies(potential, versionSpec)) || ''; if (version) { core.debug(`matched: ${version}`); } else { core.debug('match not found'); } - return version; + return semver.clean(version) || ''; } function acquireNode(version) { return __awaiter(this, void 0, void 0, function* () { - // // Download - a tool installer intimately knows how to get the tool (and construct urls) - // - version = semver.clean(version) || ''; - let fileName = osPlat == 'win32' - ? 'node-v' + version + '-win-' + os.arch() - : 'node-v' + version + '-' + osPlat + '-' + os.arch(); - let urlFileName = osPlat == 'win32' ? fileName + '.7z' : fileName + '.tar.gz'; - let downloadUrl = 'https://nodejs.org/dist/v' + version + '/' + urlFileName; + const fileName = `node-v${version}-${IS_WINDOWS ? 'win' : osPlat}-${osArch}`; + const urlFileName = `${fileName}.${IS_WINDOWS ? '7z' : 'tar.gz'}`; + const downloadUrl = `https://nodejs.org/dist/v${version}/${urlFileName}`; let downloadPath; try { downloadPath = yield tc.downloadTool(downloadUrl); } catch (err) { - if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - return yield acquireNodeFromFallbackLocation(version); + if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { + if (IS_WINDOWS) { + return acquireNodeFromFallbackLocation(version); + } } throw err; } - // // Extract - // - let extPath; - if (osPlat == 'win32') { - let _7zPath = path.join(__dirname, '..', 'externals', '7zr.exe'); - extPath = yield tc.extract7z(downloadPath, undefined, _7zPath); - } - else { - extPath = yield tc.extractTar(downloadPath); - } - // - // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded - // - let toolRoot = path.join(extPath, fileName); - return yield tc.cacheDir(toolRoot, 'node', version); + const _7zPath = path.join(__dirname, '..', 'externals', '7zr.exe'); + const extPath = IS_WINDOWS + ? yield tc.extract7z(downloadPath, undefined, _7zPath) + : yield tc.extractTar(downloadPath); + // Install into the local tool cache + // node extracts with a root folder that matches the fileName downloaded + const toolRoot = path.join(extPath, fileName); + return tc.cacheDir(toolRoot, 'node', version, osArch); }); } // For non LTS versions of Node, the files we need (for Windows) are sometimes located @@ -196,32 +167,39 @@ function acquireNode(version) { function acquireNodeFromFallbackLocation(version) { return __awaiter(this, void 0, void 0, function* () { // Create temporary folder to download in to - let tempDownloadFolder = 'temp_' + Math.floor(Math.random() * 2000000000); - let tempDir = path.join(tempDirectory, tempDownloadFolder); - yield io.mkdirP(tempDir); - let exeUrl; - let libUrl; + const tempDownloadFolder = `temp_${Math.floor(Math.random() * 2000000000)}`; + const tempDir = path.join(getTempDirectory(), tempDownloadFolder); + const baseUrl = `https://nodejs.org/dist/v${version}/`; + const tryDownload = (url) => __awaiter(this, void 0, void 0, function* () { + const exeFileName = 'node.exe'; + const libFileName = 'node.lib'; + const exePath = yield tc.downloadTool(`${url}${exeFileName}`); + yield io.cp(exePath, path.join(tempDir, exeFileName)); + const libPath = yield tc.downloadTool(`${url}${libFileName}`); + yield io.cp(libPath, path.join(tempDir, libFileName)); + }); try { - exeUrl = `https://nodejs.org/dist/v${version}/win-${os.arch()}/node.exe`; - libUrl = `https://nodejs.org/dist/v${version}/win-${os.arch()}/node.lib`; - const exePath = yield tc.downloadTool(exeUrl); - yield io.cp(exePath, path.join(tempDir, 'node.exe')); - const libPath = yield tc.downloadTool(libUrl); - yield io.cp(libPath, path.join(tempDir, 'node.lib')); + yield io.mkdirP(tempDir); + yield tryDownload(`${baseUrl}win-${osArch}/`); } catch (err) { - if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - exeUrl = `https://nodejs.org/dist/v${version}/node.exe`; - libUrl = `https://nodejs.org/dist/v${version}/node.lib`; - const exePath = yield tc.downloadTool(exeUrl); - yield io.cp(exePath, path.join(tempDir, 'node.exe')); - const libPath = yield tc.downloadTool(libUrl); - yield io.cp(libPath, path.join(tempDir, 'node.lib')); + if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { + yield tryDownload(baseUrl); } else { throw err; } } - return yield tc.cacheDir(tempDir, 'node', version); + return tc.cacheDir(tempDir, 'node', version, osArch); }); } +function getTempDirectory() { + const baseLocation = + // On windows use the USERPROFILE env variable + process.platform === 'win32' + ? process.env['USERPROFILE'] || 'C:\\' + : process.platform === 'darwin' + ? '/Users' + : '/home'; + return (process.env['RUNNER_TEMP'] || path.join(baseLocation, 'actions', 'temp')); +} diff --git a/lib/setup-node.js b/lib/setup-node.js index d7b35185e..67061ee8b 100644 --- a/lib/setup-node.js +++ b/lib/setup-node.js @@ -16,20 +16,16 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); -const installer = __importStar(require("./installer")); -const auth = __importStar(require("./authutil")); const path = __importStar(require("path")); +const auth = __importStar(require("./authutil")); +const installer = __importStar(require("./installer")); function run() { return __awaiter(this, void 0, void 0, function* () { try { - // - // Version is optional. If supplied, install / use from the tool cache + // 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('version'); - if (!version) { - version = core.getInput('node-version'); - } + const version = core.getInput('version') || core.getInput('node-version'); + // allow user to not specify a node version if (version) { // TODO: installer doesn't support proxy yield installer.getNode(version); diff --git a/src/authutil.ts b/src/authutil.ts index 07e0b24cb..fca9f55e6 100644 --- a/src/authutil.ts +++ b/src/authutil.ts @@ -1,8 +1,8 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import * as core from '@actions/core'; -import * as github from '@actions/github'; export function configAuthentication(registryUrl: string, alwaysAuth: string) { const npmrc: string = path.resolve( diff --git a/src/installer.ts b/src/installer.ts index 0abb1fe00..280bdee57 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -1,138 +1,149 @@ -// Load tempDirectory before it gets wiped by tool-cache -let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || ''; import * as core from '@actions/core'; import * as io from '@actions/io'; import * as tc from '@actions/tool-cache'; -import * as restm from 'typed-rest-client/RestClient'; import * as os from 'os'; import * as path from 'path'; import * as semver from 'semver'; +import * as restm from 'typed-rest-client/RestClient'; let osPlat: string = os.platform(); let osArch: string = os.arch(); +const IS_WINDOWS: boolean = osPlat === 'win32'; -if (!tempDirectory) { - let baseLocation; - if (process.platform === 'win32') { - // On windows use the USERPROFILE env variable - baseLocation = process.env['USERPROFILE'] || 'C:\\'; - } else { - if (process.platform === 'darwin') { - baseLocation = '/Users'; - } else { - baseLocation = '/home'; - } - } - tempDirectory = path.join(baseLocation, 'actions', 'temp'); -} - -// -// Node versions interface -// see https://nodejs.org/dist/index.json -// -interface INodeVersion { +/* + * Node versions interface + * see https://nodejs.org/dist/index.json + */ +export interface INodeVersion { version: string; files: string[]; + lts: string | false; } -export async function getNode(versionSpec: string) { - // check cache - let toolPath: string; - toolPath = tc.find('node', versionSpec); +export async function getNode(versionSpec: string): Promise { + versionSpec = versionSpec.trim(); - // If not found in cache, download - if (!toolPath) { - let version: string; - const c = semver.clean(versionSpec) || ''; - // If explicit version - if (semver.valid(c) != null) { - // version to download - version = versionSpec; - } else { - // query nodejs.org for a matching version - version = await queryLatestMatch(versionSpec); - if (!version) { - throw new Error( - `Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.` - ); - } + // resolve node codenames + let version = await resolve(versionSpec); - // check cache - toolPath = tc.find('node', version); - } + if (!version) { + throw new Error( + `Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.` + ); + } - if (!toolPath) { - // download, extract, cache - toolPath = await acquireNode(version); - } + // check cache + let toolPath = tc.find('node', version, osArch); + + // Not found in cache -> download + if (!toolPath) { + // download, extract, cache + toolPath = await acquireNode(version); } - // // a tool installer initimately knows details about the layout of that tool // for example, node binary is in the bin folder after the extract on Mac/Linux. // layouts could change by version, by platform etc... but that's the tool installers job - // - if (osPlat != 'win32') { + if (!IS_WINDOWS) { toolPath = path.join(toolPath, 'bin'); } - // // prepend the tools path. instructs the agent to prepend for future tasks core.addPath(toolPath); } -async function queryLatestMatch(versionSpec: string): Promise { +async function resolve(versionSpec: string): Promise { + let version = semver.clean(versionSpec) || ''; + return semver.valid(version) || tc.find('node', versionSpec, osArch) + ? version || versionSpec + : queryNodeVersions(versionSpec); +} + +async function queryNodeVersions(versionSpec: string): Promise { + core.debug(`querying Node.js for ${versionSpec}`); // node offers a json list of versions + let dataUrl = 'https://nodejs.org/dist/index.json'; + let rest = new restm.RestClient('setup-node'); + let nodeVersions: INodeVersion[] = + (await rest.get(dataUrl)).result || []; let dataFileName: string; switch (osPlat) { case 'linux': - dataFileName = 'linux-' + osArch; + dataFileName = `linux-${osArch}`; break; case 'darwin': - dataFileName = 'osx-' + osArch + '-tar'; + dataFileName = `osx-${osArch}-tar`; break; case 'win32': - dataFileName = 'win-' + osArch + '-exe'; + dataFileName = `win-${osArch}-7z`; break; default: throw new Error(`Unexpected OS '${osPlat}'`); } - let versions: string[] = []; - let dataUrl = 'https://nodejs.org/dist/index.json'; - let rest: restm.RestClient = new restm.RestClient('setup-node'); - let nodeVersions: INodeVersion[] = - (await rest.get(dataUrl)).result || []; - nodeVersions.forEach((nodeVersion: INodeVersion) => { - // ensure this version supports your os and platform - if (nodeVersion.files.indexOf(dataFileName) >= 0) { - versions.push(nodeVersion.version); - } - }); + // ensure this version supports your os and platform + nodeVersions = nodeVersions.filter( + (nodeVersion: INodeVersion) => nodeVersion.files.indexOf(dataFileName) > -1 + ); + + // sort node versions by descending version + nodeVersions = nodeVersions.sort((a: INodeVersion, b: INodeVersion) => + semver.gt(b.version, a.version) ? 1 : -1 + ); + + const isLatestSpec = /^latest|current$/i.test(versionSpec); + const isLTSSpec = /^lts$/i.test(versionSpec); + const isLTSCodenameSpec = + !isLatestSpec && !isLTSSpec && /^[a-zA-Z]+$/.test(versionSpec); + const findNodeVersion = ( + predicator: (nodeVersion: INodeVersion) => boolean + ): string => { + nodeVersions = nodeVersions.filter(predicator); + return nodeVersions.length + ? semver.clean(nodeVersions[0].version) || '' + : ''; + }; + + // resolve latest or current node version + if (isLatestSpec) { + return findNodeVersion( + (nodeVersion: INodeVersion) => typeof nodeVersion.lts !== 'string' + ); + } + + // resolve lts node version + if (isLTSSpec) { + return findNodeVersion( + (nodeVersion: INodeVersion) => typeof nodeVersion.lts === 'string' + ); + } + + // resolve node version codename + if (isLTSCodenameSpec) { + return findNodeVersion( + (nodeVersion: INodeVersion) => + typeof nodeVersion.lts === 'string' && + nodeVersion.lts.toLowerCase() === versionSpec.toLowerCase() + ); + } // get the latest version that matches the version spec - let version: string = evaluateVersions(versions, versionSpec); - return version; + return evaluateVersions(nodeVersions, versionSpec); } // TODO - should we just export this from @actions/tool-cache? Lifted directly from there -function evaluateVersions(versions: string[], versionSpec: string): string { - let version = ''; - core.debug(`evaluating ${versions.length} versions`); - versions = versions.sort((a, b) => { - if (semver.gt(a, b)) { - return 1; - } - return -1; - }); - for (let i = versions.length - 1; i >= 0; i--) { - const potential: string = versions[i]; - const satisfied: boolean = semver.satisfies(potential, versionSpec); - if (satisfied) { - version = potential; - break; - } - } +function evaluateVersions( + nodeVersions: INodeVersion[], + versionSpec: string +): string { + core.debug(`evaluating ${nodeVersions.length} versions`); + const versions = nodeVersions.map( + (nodeVersion: INodeVersion) => nodeVersion.version + ); + const version = + versions.find((potential: string) => + semver.satisfies(potential, versionSpec) + ) || ''; if (version) { core.debug(`matched: ${version}`); @@ -140,51 +151,39 @@ function evaluateVersions(versions: string[], versionSpec: string): string { core.debug('match not found'); } - return version; + return semver.clean(version) || ''; } async function acquireNode(version: string): Promise { - // // Download - a tool installer intimately knows how to get the tool (and construct urls) - // - version = semver.clean(version) || ''; - let fileName: string = - osPlat == 'win32' - ? 'node-v' + version + '-win-' + os.arch() - : 'node-v' + version + '-' + osPlat + '-' + os.arch(); - let urlFileName: string = - osPlat == 'win32' ? fileName + '.7z' : fileName + '.tar.gz'; - - let downloadUrl = 'https://nodejs.org/dist/v' + version + '/' + urlFileName; + const fileName: string = `node-v${version}-${ + IS_WINDOWS ? 'win' : osPlat + }-${osArch}`; + const urlFileName: string = `${fileName}.${IS_WINDOWS ? '7z' : 'tar.gz'}`; + const downloadUrl = `https://nodejs.org/dist/v${version}/${urlFileName}`; let downloadPath: string; - try { downloadPath = await tc.downloadTool(downloadUrl); } catch (err) { - if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - return await acquireNodeFromFallbackLocation(version); + if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { + if (IS_WINDOWS) { + return acquireNodeFromFallbackLocation(version); + } } - throw err; } - // // Extract - // - let extPath: string; - if (osPlat == 'win32') { - let _7zPath = path.join(__dirname, '..', 'externals', '7zr.exe'); - extPath = await tc.extract7z(downloadPath, undefined, _7zPath); - } else { - extPath = await tc.extractTar(downloadPath); - } - - // - // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded - // - let toolRoot = path.join(extPath, fileName); - return await tc.cacheDir(toolRoot, 'node', version); + const _7zPath = path.join(__dirname, '..', 'externals', '7zr.exe'); + const extPath: string = IS_WINDOWS + ? await tc.extract7z(downloadPath, undefined, _7zPath) + : await tc.extractTar(downloadPath); + + // Install into the local tool cache + // node extracts with a root folder that matches the fileName downloaded + const toolRoot = path.join(extPath, fileName); + return tc.cacheDir(toolRoot, 'node', version, osArch); } // For non LTS versions of Node, the files we need (for Windows) are sometimes located @@ -203,32 +202,42 @@ async function acquireNodeFromFallbackLocation( version: string ): Promise { // Create temporary folder to download in to - let tempDownloadFolder: string = - 'temp_' + Math.floor(Math.random() * 2000000000); - let tempDir: string = path.join(tempDirectory, tempDownloadFolder); - await io.mkdirP(tempDir); - let exeUrl: string; - let libUrl: string; - try { - exeUrl = `https://nodejs.org/dist/v${version}/win-${os.arch()}/node.exe`; - libUrl = `https://nodejs.org/dist/v${version}/win-${os.arch()}/node.lib`; + const tempDownloadFolder: string = `temp_${Math.floor( + Math.random() * 2000000000 + )}`; + const tempDir: string = path.join(getTempDirectory(), tempDownloadFolder); + const baseUrl = `https://nodejs.org/dist/v${version}/`; + const tryDownload = async (url: string) => { + const exeFileName = 'node.exe'; + const libFileName = 'node.lib'; + const exePath = await tc.downloadTool(`${url}${exeFileName}`); + await io.cp(exePath, path.join(tempDir, exeFileName)); + const libPath = await tc.downloadTool(`${url}${libFileName}`); + await io.cp(libPath, path.join(tempDir, libFileName)); + }; - const exePath = await tc.downloadTool(exeUrl); - await io.cp(exePath, path.join(tempDir, 'node.exe')); - const libPath = await tc.downloadTool(libUrl); - await io.cp(libPath, path.join(tempDir, 'node.lib')); + try { + await io.mkdirP(tempDir); + await tryDownload(`${baseUrl}win-${osArch}/`); } catch (err) { - if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - exeUrl = `https://nodejs.org/dist/v${version}/node.exe`; - libUrl = `https://nodejs.org/dist/v${version}/node.lib`; - - const exePath = await tc.downloadTool(exeUrl); - await io.cp(exePath, path.join(tempDir, 'node.exe')); - const libPath = await tc.downloadTool(libUrl); - await io.cp(libPath, path.join(tempDir, 'node.lib')); + if (err instanceof tc.HTTPError && err.httpStatusCode === 404) { + await tryDownload(baseUrl); } else { throw err; } } - return await tc.cacheDir(tempDir, 'node', version); + return tc.cacheDir(tempDir, 'node', version, osArch); +} + +function getTempDirectory(): string { + const baseLocation: string = + // On windows use the USERPROFILE env variable + process.platform === 'win32' + ? process.env['USERPROFILE'] || 'C:\\' + : process.platform === 'darwin' + ? '/Users' + : '/home'; + return ( + process.env['RUNNER_TEMP'] || path.join(baseLocation, 'actions', 'temp') + ); } diff --git a/src/setup-node.ts b/src/setup-node.ts index 51deccbe2..ae212e847 100644 --- a/src/setup-node.ts +++ b/src/setup-node.ts @@ -1,18 +1,15 @@ import * as core from '@actions/core'; -import * as installer from './installer'; -import * as auth from './authutil'; import * as path from 'path'; +import * as auth from './authutil'; +import * as installer from './installer'; async function run() { try { - // - // Version is optional. If supplied, install / use from the tool cache + // 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('version'); - if (!version) { - version = core.getInput('node-version'); - } + const version = core.getInput('version') || core.getInput('node-version'); + + // allow user to not specify a node version if (version) { // TODO: installer doesn't support proxy await installer.getNode(version);