diff --git a/README.md b/README.md index 14b08651e..e8348d853 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This action sets by node environment for use in actions by: - optionally downloading and caching a version of node - npm by version spec and add to PATH -- registering problem matchers for error output +- registering problem matchers for error output # Usage @@ -101,6 +101,8 @@ steps: ``` +Additionally, the architecture can be selected using `node-arch`. Values are `x86`, `x64`, `arm64`, `armv6l`, `armv7l, `ppc64le`, `s390x`. **Not all architectures are available on all platforms.** + # License The scripts and documentation in this project are released under the [MIT License](LICENSE) diff --git a/__tests__/__fixtures__/mock-node-v8.8.0-linux-x86.tar.gz b/__tests__/__fixtures__/mock-node-v8.8.0-linux-x86.tar.gz new file mode 100644 index 000000000..fd88664a8 Binary files /dev/null and b/__tests__/__fixtures__/mock-node-v8.8.0-linux-x86.tar.gz differ diff --git a/__tests__/__fixtures__/mock-node-v8.8.0-win-x86.7z b/__tests__/__fixtures__/mock-node-v8.8.0-win-x86.7z new file mode 100644 index 000000000..9a16b0912 Binary files /dev/null and b/__tests__/__fixtures__/mock-node-v8.8.0-win-x86.7z differ diff --git a/__tests__/__fixtures__/mock-node-v8.9.1-linux-x64.tar.gz b/__tests__/__fixtures__/mock-node-v8.9.1-linux-x64.tar.gz new file mode 100644 index 000000000..3a4824456 Binary files /dev/null and b/__tests__/__fixtures__/mock-node-v8.9.1-linux-x64.tar.gz differ diff --git a/__tests__/__fixtures__/mock-node-v8.9.1-win-x64.7z b/__tests__/__fixtures__/mock-node-v8.9.1-win-x64.7z new file mode 100644 index 000000000..dda8188f3 Binary files /dev/null and b/__tests__/__fixtures__/mock-node-v8.9.1-win-x64.7z differ diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index e0ada3252..edbd9af08 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -1,5 +1,6 @@ import io = require('@actions/io'); import fs = require('fs'); +import nock = require('nock'); import os = require('os'); import path = require('path'); @@ -36,6 +37,10 @@ describe('installer tests', () => { await io.rmRF(tempDir); }, 100000); + beforeEach(() => { + nock.cleanAll(); + }); + 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()); @@ -120,4 +125,62 @@ describe('installer tests', () => { await installer.getNode('252'); await installer.getNode('252.0'); }); + + it('Acquires specified x86 version of node if no matching version is installed', async () => { + const arch = 'x86'; + const version = '8.8.0'; + const fileExtension = IS_WINDOWS ? '7z' : 'tar.gz'; + const platform = { + linux: 'linux', + darwin: 'darwin', + win32: 'win' + }[process.platform]; + const fileName = `node-v${version}-${platform}-${arch}.${fileExtension}`; + const pathOnNodeJs = `/dist/v${version}/${fileName}`; + const scope = nock('https://nodejs.org') + .get(pathOnNodeJs) + .replyWithFile( + 200, + path.join(__dirname, '__fixtures__', `mock-${fileName}`) + ); + await installer.getNode(version, arch); + const nodeDir = path.join(toolDir, 'node', version, arch); + + expect(scope.isDone()).toBe(true); + 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('Acquires specified x64 version of node if no matching version is installed', async () => { + const arch = 'x64'; + const version = '8.9.1'; + const fileExtension = IS_WINDOWS ? '7z' : 'tar.gz'; + const platform = { + linux: 'linux', + darwin: 'darwin', + win32: 'win' + }[process.platform]; + const fileName = `node-v${version}-${platform}-${arch}.${fileExtension}`; + const pathOnNodeJs = `/dist/v${version}/${fileName}`; + const scope = nock('https://nodejs.org') + .get(pathOnNodeJs) + .replyWithFile( + 200, + path.join(__dirname, '__fixtures__', `mock-${fileName}`) + ); + await installer.getNode(version, arch); + const nodeDir = path.join(toolDir, 'node', version, arch); + + expect(scope.isDone()).toBe(true); + 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); }); diff --git a/action.yml b/action.yml index 77b6ca0b5..0bb0f1b04 100644 --- a/action.yml +++ b/action.yml @@ -8,6 +8,8 @@ inputs: node-version: description: 'Version Spec of the version to use. Examples: 10.x, 10.15.1, >=10.15.0' default: '10.x' + node-arch: + description: 'Target architecture for Node to use. Examples: x86, x64. Will use system architecture by default.' 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/installer.js b/lib/installer.js index c86924c1d..630b8054f 100644 --- a/lib/installer.js +++ b/lib/installer.js @@ -25,7 +25,6 @@ const os = __importStar(require("os")); const path = __importStar(require("path")); const semver = __importStar(require("semver")); let osPlat = os.platform(); -let osArch = os.arch(); if (!tempDirectory) { let baseLocation; if (process.platform === 'win32') { @@ -42,11 +41,11 @@ if (!tempDirectory) { } tempDirectory = path.join(baseLocation, 'actions', 'temp'); } -function getNode(versionSpec) { +function getNode(versionSpec, osArch = os.arch()) { return __awaiter(this, void 0, void 0, function* () { // check cache let toolPath; - toolPath = tc.find('node', versionSpec); + toolPath = tc.find('node', versionSpec, osArch); // If not found in cache, download if (!toolPath) { let version; @@ -58,16 +57,16 @@ function getNode(versionSpec) { } else { // query nodejs.org for a matching version - version = yield queryLatestMatch(versionSpec); + version = yield queryLatestMatch(versionSpec, osArch); if (!version) { throw new Error(`Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.`); } // check cache - toolPath = tc.find('node', version); + toolPath = tc.find('node', version, osArch); } if (!toolPath) { // download, extract, cache - toolPath = yield acquireNode(version); + toolPath = yield acquireNode(version, osArch); } } // @@ -84,7 +83,7 @@ function getNode(versionSpec) { }); } exports.getNode = getNode; -function queryLatestMatch(versionSpec) { +function queryLatestMatch(versionSpec, osArch) { return __awaiter(this, void 0, void 0, function* () { // node offers a json list of versions let dataFileName; @@ -142,15 +141,15 @@ function evaluateVersions(versions, versionSpec) { } return version; } -function acquireNode(version) { +function acquireNode(version, osArch) { 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(); + ? 'node-v' + version + '-win-' + osArch + : 'node-v' + version + '-' + osPlat + '-' + osArch; let urlFileName = osPlat == 'win32' ? fileName + '.7z' : fileName + '.tar.gz'; let downloadUrl = 'https://nodejs.org/dist/v' + version + '/' + urlFileName; let downloadPath; @@ -159,7 +158,7 @@ function acquireNode(version) { } catch (err) { if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - return yield acquireNodeFromFallbackLocation(version); + return yield acquireNodeFromFallbackLocation(version, osArch); } throw err; } @@ -178,7 +177,7 @@ function acquireNode(version) { // 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); + return yield tc.cacheDir(toolRoot, 'node', version, osArch); }); } // For non LTS versions of Node, the files we need (for Windows) are sometimes located @@ -193,7 +192,7 @@ function acquireNode(version) { // This method attempts to download and cache the resources from these alternative locations. // Note also that the files are normally zipped but in this case they are just an exe // and lib file in a folder, not zipped. -function acquireNodeFromFallbackLocation(version) { +function acquireNodeFromFallbackLocation(version, osArch) { return __awaiter(this, void 0, void 0, function* () { // Create temporary folder to download in to let tempDownloadFolder = 'temp_' + Math.floor(Math.random() * 2000000000); @@ -202,8 +201,8 @@ function acquireNodeFromFallbackLocation(version) { let exeUrl; let libUrl; 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`; + exeUrl = `https://nodejs.org/dist/v${version}/win-${osArch}/node.exe`; + libUrl = `https://nodejs.org/dist/v${version}/win-${osArch}/node.lib`; const exePath = yield tc.downloadTool(exeUrl); yield io.cp(exePath, path.join(tempDir, 'node.exe')); const libPath = yield tc.downloadTool(libUrl); @@ -222,6 +221,6 @@ function acquireNodeFromFallbackLocation(version) { throw err; } } - return yield tc.cacheDir(tempDir, 'node', version); + return yield tc.cacheDir(tempDir, 'node', version, osArch); }); } diff --git a/lib/setup-node.js b/lib/setup-node.js index d7b35185e..1607c6606 100644 --- a/lib/setup-node.js +++ b/lib/setup-node.js @@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(require("@actions/core")); const installer = __importStar(require("./installer")); const auth = __importStar(require("./authutil")); +const os = __importStar(require("os")); const path = __importStar(require("path")); function run() { return __awaiter(this, void 0, void 0, function* () { @@ -31,8 +32,9 @@ function run() { version = core.getInput('node-version'); } if (version) { + const osArch = core.getInput('node-arch') || os.arch(); // TODO: installer doesn't support proxy - yield installer.getNode(version); + yield installer.getNode(version, osArch); } const registryUrl = core.getInput('registry-url'); const alwaysAuth = core.getInput('always-auth'); diff --git a/package-lock.json b/package-lock.json index 6c1535390..bda8d2e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -821,6 +821,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -1149,6 +1155,20 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1160,6 +1180,12 @@ "supports-color": "^5.3.0" } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -1403,6 +1429,15 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -2423,6 +2458,12 @@ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-stdin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", @@ -3895,6 +3936,37 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "nock": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-11.3.5.tgz", + "integrity": "sha512-6WGeZcWc3RExkBcMSYSrUm/5YukDo52m/jhwniQyrnuiCnKRljBwwje9vTwJyEi4J6m2bq0Aj6C1vzuM6iuaeg==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.13", + "mkdirp": "^0.5.0", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -4236,6 +4308,12 @@ "pify": "^3.0.0" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4363,6 +4441,12 @@ "sisteransi": "^1.0.0" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "psl": { "version": "1.1.33", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.33.tgz", @@ -5247,6 +5331,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz", diff --git a/package.json b/package.json index 19b641504..ead9c73f8 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "@actions/github": "^1.0.0", "@actions/io": "^1.0.0", "@actions/tool-cache": "^1.0.0", - "typed-rest-client": "^1.5.0", - "semver": "^6.1.1" + "semver": "^6.1.1", + "typed-rest-client": "^1.5.0" }, "devDependencies": { "@types/jest": "^24.0.13", @@ -36,6 +36,7 @@ "husky": "^2.3.0", "jest": "^24.8.0", "jest-circus": "^24.7.1", + "nock": "^11.3.5", "prettier": "^1.17.1", "ts-jest": "^24.0.2", "typescript": "^3.5.1" diff --git a/src/installer.ts b/src/installer.ts index 0abb1fe00..9f995b589 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -9,7 +9,6 @@ import * as path from 'path'; import * as semver from 'semver'; let osPlat: string = os.platform(); -let osArch: string = os.arch(); if (!tempDirectory) { let baseLocation; @@ -35,10 +34,13 @@ interface INodeVersion { files: string[]; } -export async function getNode(versionSpec: string) { +export async function getNode( + versionSpec: string, + osArch: string | undefined = os.arch() +) { // check cache let toolPath: string; - toolPath = tc.find('node', versionSpec); + toolPath = tc.find('node', versionSpec, osArch); // If not found in cache, download if (!toolPath) { @@ -50,7 +52,7 @@ export async function getNode(versionSpec: string) { version = versionSpec; } else { // query nodejs.org for a matching version - version = await queryLatestMatch(versionSpec); + version = await queryLatestMatch(versionSpec, osArch); if (!version) { throw new Error( `Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.` @@ -58,12 +60,12 @@ export async function getNode(versionSpec: string) { } // check cache - toolPath = tc.find('node', version); + toolPath = tc.find('node', version, osArch); } if (!toolPath) { // download, extract, cache - toolPath = await acquireNode(version); + toolPath = await acquireNode(version, osArch); } } @@ -81,7 +83,10 @@ export async function getNode(versionSpec: string) { core.addPath(toolPath); } -async function queryLatestMatch(versionSpec: string): Promise { +async function queryLatestMatch( + versionSpec: string, + osArch: string +): Promise { // node offers a json list of versions let dataFileName: string; switch (osPlat) { @@ -143,15 +148,15 @@ function evaluateVersions(versions: string[], versionSpec: string): string { return version; } -async function acquireNode(version: string): Promise { +async function acquireNode(version: string, osArch: 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(); + ? 'node-v' + version + '-win-' + osArch + : 'node-v' + version + '-' + osPlat + '-' + osArch; let urlFileName: string = osPlat == 'win32' ? fileName + '.7z' : fileName + '.tar.gz'; @@ -163,7 +168,7 @@ async function acquireNode(version: string): Promise { downloadPath = await tc.downloadTool(downloadUrl); } catch (err) { if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { - return await acquireNodeFromFallbackLocation(version); + return await acquireNodeFromFallbackLocation(version, osArch); } throw err; @@ -184,7 +189,7 @@ async function acquireNode(version: string): Promise { // 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); + return await tc.cacheDir(toolRoot, 'node', version, osArch); } // For non LTS versions of Node, the files we need (for Windows) are sometimes located @@ -200,7 +205,8 @@ async function acquireNode(version: string): Promise { // Note also that the files are normally zipped but in this case they are just an exe // and lib file in a folder, not zipped. async function acquireNodeFromFallbackLocation( - version: string + version: string, + osArch: string ): Promise { // Create temporary folder to download in to let tempDownloadFolder: string = @@ -210,8 +216,8 @@ async function acquireNodeFromFallbackLocation( 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`; + exeUrl = `https://nodejs.org/dist/v${version}/win-${osArch}/node.exe`; + libUrl = `https://nodejs.org/dist/v${version}/win-${osArch}/node.lib`; const exePath = await tc.downloadTool(exeUrl); await io.cp(exePath, path.join(tempDir, 'node.exe')); @@ -230,5 +236,5 @@ async function acquireNodeFromFallbackLocation( throw err; } } - return await tc.cacheDir(tempDir, 'node', version); + return await tc.cacheDir(tempDir, 'node', version, osArch); } diff --git a/src/setup-node.ts b/src/setup-node.ts index 51deccbe2..ceee15511 100644 --- a/src/setup-node.ts +++ b/src/setup-node.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core'; import * as installer from './installer'; import * as auth from './authutil'; +import * as os from 'os'; import * as path from 'path'; async function run() { @@ -13,9 +14,11 @@ async function run() { if (!version) { version = core.getInput('node-version'); } + if (version) { + const osArch = core.getInput('node-arch') || os.arch(); // TODO: installer doesn't support proxy - await installer.getNode(version); + await installer.getNode(version, osArch); } const registryUrl: string = core.getInput('registry-url');