mirror of
				https://kkgithub.com/actions/setup-node.git
				synced 2025-10-31 10:41:57 +08:00 
			
		
		
		
	download from node-versions and fallback to node dist (#147)
* download LTS versions from releases * support upcoiming ghes Co-authored-by: eric sciple <ericsciple@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										286
									
								
								src/installer.ts
									
									
									
									
									
								
							
							
						
						
									
										286
									
								
								src/installer.ts
									
									
									
									
									
								
							| @ -1,54 +1,130 @@ | ||||
| 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 os from 'os'; | ||||
| import * as path from 'path'; | ||||
| import * as semver from 'semver'; | ||||
|  | ||||
| let osPlat: string = os.platform(); | ||||
| let osArch: string = translateArchToDistUrl(os.arch()); | ||||
| import fs = require('fs'); | ||||
|  | ||||
| // | ||||
| // Node versions interface | ||||
| // see https://nodejs.org/dist/index.json | ||||
| // | ||||
| interface INodeVersion { | ||||
| export interface INodeVersion { | ||||
|   version: string; | ||||
|   files: string[]; | ||||
| } | ||||
|  | ||||
| export async function getNode(versionSpec: string) { | ||||
| interface INodeVersionInfo { | ||||
|   downloadUrl: string; | ||||
|   resolvedVersion: string; | ||||
|   fileName: string; | ||||
| } | ||||
|  | ||||
| export async function getNode( | ||||
|   versionSpec: string, | ||||
|   stable: boolean, | ||||
|   auth: string | undefined | ||||
| ) { | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(os.arch()); | ||||
|  | ||||
|   // check cache | ||||
|   let toolPath: string; | ||||
|   toolPath = tc.find('node', versionSpec); | ||||
|  | ||||
|   // 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) { | ||||
|   if (toolPath) { | ||||
|     console.log(`Found in cache @ ${toolPath}`); | ||||
|   } else { | ||||
|     console.log(`Attempting to download ${versionSpec}...`); | ||||
|     let downloadPath = ''; | ||||
|     let info: INodeVersionInfo | null = null; | ||||
|  | ||||
|     // | ||||
|     // Try download from internal distribution (popular versions only) | ||||
|     // | ||||
|     try { | ||||
|       info = await getInfoFromManifest(versionSpec, stable, auth); | ||||
|       if (info) { | ||||
|         console.log( | ||||
|           `Acquiring ${info.resolvedVersion} from ${info.downloadUrl}` | ||||
|         ); | ||||
|         downloadPath = await tc.downloadTool(info.downloadUrl, undefined, auth); | ||||
|       } else { | ||||
|         console.log( | ||||
|           'Not found in manifest.  Falling back to download directly from Node' | ||||
|         ); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       // Rate limit? | ||||
|       if ( | ||||
|         err instanceof tc.HTTPError && | ||||
|         (err.httpStatusCode === 403 || err.httpStatusCode === 429) | ||||
|       ) { | ||||
|         console.log( | ||||
|           `Received HTTP status code ${err.httpStatusCode}.  This usually indicates the rate limit has been exceeded` | ||||
|         ); | ||||
|       } else { | ||||
|         console.log(err.message); | ||||
|       } | ||||
|       core.debug(err.stack); | ||||
|       console.log('Falling back to download directly from Node'); | ||||
|     } | ||||
|  | ||||
|     // | ||||
|     // Download from nodejs.org | ||||
|     // | ||||
|     if (!downloadPath) { | ||||
|       info = await getInfoFromDist(versionSpec); | ||||
|       if (!info) { | ||||
|         throw new Error( | ||||
|           `Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.` | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       // check cache | ||||
|       toolPath = tc.find('node', version); | ||||
|       console.log(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`); | ||||
|       try { | ||||
|         downloadPath = await tc.downloadTool(info.downloadUrl); | ||||
|       } catch (err) { | ||||
|         if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { | ||||
|           return await acquireNodeFromFallbackLocation(info.resolvedVersion); | ||||
|         } | ||||
|  | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (!toolPath) { | ||||
|       // download, extract, cache | ||||
|       toolPath = await acquireNode(version); | ||||
|     // | ||||
|     // Extract | ||||
|     // | ||||
|     console.log('Extracting ...'); | ||||
|     let extPath: string; | ||||
|     info = info || ({} as INodeVersionInfo); // satisfy compiler, never null when reaches here | ||||
|     if (osPlat == 'win32') { | ||||
|       let _7zPath = path.join(__dirname, '..', 'externals', '7zr.exe'); | ||||
|       extPath = await tc.extract7z(downloadPath, undefined, _7zPath); | ||||
|       // 7z extracts to folder matching file name | ||||
|       let nestedPath = path.join(extPath, path.basename(info.fileName, '.7z')); | ||||
|       if (fs.existsSync(nestedPath)) { | ||||
|         extPath = nestedPath; | ||||
|       } | ||||
|     } else { | ||||
|       extPath = await tc.extractTar(downloadPath, undefined, [ | ||||
|         'xz', | ||||
|         '--strip', | ||||
|         '1' | ||||
|       ]); | ||||
|     } | ||||
|  | ||||
|     // | ||||
|     // Install into the local tool cache - node extracts with a root folder that matches the fileName downloaded | ||||
|     // | ||||
|     console.log('Adding to the cache ...'); | ||||
|     toolPath = await tc.cacheDir(extPath, 'node', info.resolvedVersion); | ||||
|     console.log('Done'); | ||||
|   } | ||||
|  | ||||
|   // | ||||
| @ -65,41 +141,60 @@ export async function getNode(versionSpec: string) { | ||||
|   core.addPath(toolPath); | ||||
| } | ||||
|  | ||||
| async function queryLatestMatch(versionSpec: string): Promise<string> { | ||||
|   // node offers a json list of versions | ||||
|   let dataFileName: string; | ||||
|   switch (osPlat) { | ||||
|     case 'linux': | ||||
|       dataFileName = `linux-${osArch}`; | ||||
|       break; | ||||
|     case 'darwin': | ||||
|       dataFileName = `osx-${osArch}-tar`; | ||||
|       break; | ||||
|     case 'win32': | ||||
|       dataFileName = `win-${osArch}-exe`; | ||||
|       break; | ||||
|     default: | ||||
|       throw new Error(`Unexpected OS '${osPlat}'`); | ||||
| async function getInfoFromManifest( | ||||
|   versionSpec: string, | ||||
|   stable: boolean, | ||||
|   auth: string | undefined | ||||
| ): Promise<INodeVersionInfo | null> { | ||||
|   let info: INodeVersionInfo | null = null; | ||||
|   const releases = await tc.getManifestFromRepo( | ||||
|     'actions', | ||||
|     'node-versions', | ||||
|     auth | ||||
|   ); | ||||
|   console.log(`matching ${versionSpec}...`); | ||||
|   const rel = await tc.findFromManifest(versionSpec, stable, releases); | ||||
|  | ||||
|   if (rel && rel.files.length > 0) { | ||||
|     info = <INodeVersionInfo>{}; | ||||
|     info.resolvedVersion = rel.version; | ||||
|     info.downloadUrl = rel.files[0].download_url; | ||||
|     info.fileName = rel.files[0].filename; | ||||
|   } | ||||
|  | ||||
|   let versions: string[] = []; | ||||
|   let dataUrl = 'https://nodejs.org/dist/index.json'; | ||||
|   let httpClient = new hc.HttpClient('setup-node', [], { | ||||
|     allowRetries: true, | ||||
|     maxRetries: 3 | ||||
|   }); | ||||
|   let response = await httpClient.getJson<INodeVersion[]>(dataUrl); | ||||
|   let nodeVersions = response.result || []; | ||||
|   nodeVersions.forEach((nodeVersion: INodeVersion) => { | ||||
|     // ensure this version supports your os and platform | ||||
|     if (nodeVersion.files.indexOf(dataFileName) >= 0) { | ||||
|       versions.push(nodeVersion.version); | ||||
|     } | ||||
|   }); | ||||
|   return info; | ||||
| } | ||||
|  | ||||
|   // get the latest version that matches the version spec | ||||
|   let version: string = evaluateVersions(versions, versionSpec); | ||||
|   return version; | ||||
| async function getInfoFromDist( | ||||
|   versionSpec: string | ||||
| ): Promise<INodeVersionInfo | null> { | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(os.arch()); | ||||
|  | ||||
|   let version: string; | ||||
|  | ||||
|   version = await queryDistForMatch(versionSpec); | ||||
|   if (!version) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   // | ||||
|   // 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-${osArch}` | ||||
|       : `node-v${version}-${osPlat}-${osArch}`; | ||||
|   let urlFileName: string = | ||||
|     osPlat == 'win32' ? `${fileName}.7z` : `${fileName}.tar.gz`; | ||||
|   let url = `https://nodejs.org/dist/v${version}/${urlFileName}`; | ||||
|  | ||||
|   return <INodeVersionInfo>{ | ||||
|     downloadUrl: url, | ||||
|     resolvedVersion: version, | ||||
|     fileName: fileName | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // TODO - should we just export this from @actions/tool-cache? Lifted directly from there | ||||
| @ -130,47 +225,49 @@ function evaluateVersions(versions: string[], versionSpec: string): string { | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| async function acquireNode(version: string): Promise<string> { | ||||
|   // | ||||
|   // 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-${osArch}` | ||||
|       : `node-v${version}-${osPlat}-${osArch}`; | ||||
|   let urlFileName: string = | ||||
|     osPlat == 'win32' ? `${fileName}.7z` : `${fileName}.tar.gz`; | ||||
|   let downloadUrl = `https://nodejs.org/dist/v${version}/${urlFileName}`; | ||||
| async function queryDistForMatch(versionSpec: string): Promise<string> { | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(os.arch()); | ||||
|  | ||||
|   let downloadPath: string; | ||||
|   // node offers a json list of versions | ||||
|   let dataFileName: string; | ||||
|   switch (osPlat) { | ||||
|     case 'linux': | ||||
|       dataFileName = `linux-${osArch}`; | ||||
|       break; | ||||
|     case 'darwin': | ||||
|       dataFileName = `osx-${osArch}-tar`; | ||||
|       break; | ||||
|     case 'win32': | ||||
|       dataFileName = `win-${osArch}-exe`; | ||||
|       break; | ||||
|     default: | ||||
|       throw new Error(`Unexpected OS '${osPlat}'`); | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     downloadPath = await tc.downloadTool(downloadUrl); | ||||
|   } catch (err) { | ||||
|     if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { | ||||
|       return await acquireNodeFromFallbackLocation(version); | ||||
|   let versions: string[] = []; | ||||
|   let nodeVersions = await module.exports.getVersionsFromDist(); | ||||
|  | ||||
|   nodeVersions.forEach((nodeVersion: INodeVersion) => { | ||||
|     // ensure this version supports your os and platform | ||||
|     if (nodeVersion.files.indexOf(dataFileName) >= 0) { | ||||
|       versions.push(nodeVersion.version); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|     throw err; | ||||
|   } | ||||
|   // get the latest version that matches the version spec | ||||
|   let version: string = evaluateVersions(versions, versionSpec); | ||||
|   return version; | ||||
| } | ||||
|  | ||||
|   // | ||||
|   // 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); | ||||
| export async function getVersionsFromDist(): Promise<INodeVersion[]> { | ||||
|   let dataUrl = 'https://nodejs.org/dist/index.json'; | ||||
|   let httpClient = new hc.HttpClient('setup-node', [], { | ||||
|     allowRetries: true, | ||||
|     maxRetries: 3 | ||||
|   }); | ||||
|   let response = await httpClient.getJson<INodeVersion[]>(dataUrl); | ||||
|   return response.result || []; | ||||
| } | ||||
|  | ||||
| // For non LTS versions of Node, the files we need (for Windows) are sometimes located | ||||
| @ -188,6 +285,9 @@ async function acquireNode(version: string): Promise<string> { | ||||
| async function acquireNodeFromFallbackLocation( | ||||
|   version: string | ||||
| ): Promise<string> { | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(os.arch()); | ||||
|  | ||||
|   // Create temporary folder to download in to | ||||
|   const tempDownloadFolder: string = | ||||
|     'temp_' + Math.floor(Math.random() * 2000000000); | ||||
| @ -201,6 +301,8 @@ async function acquireNodeFromFallbackLocation( | ||||
|     exeUrl = `https://nodejs.org/dist/v${version}/win-${osArch}/node.exe`; | ||||
|     libUrl = `https://nodejs.org/dist/v${version}/win-${osArch}/node.lib`; | ||||
|  | ||||
|     console.log(`Downloading only node binary from ${exeUrl}`); | ||||
|  | ||||
|     const exePath = await tc.downloadTool(exeUrl); | ||||
|     await io.cp(exePath, path.join(tempDir, 'node.exe')); | ||||
|     const libPath = await tc.downloadTool(libUrl); | ||||
| @ -218,7 +320,9 @@ async function acquireNodeFromFallbackLocation( | ||||
|       throw err; | ||||
|     } | ||||
|   } | ||||
|   return await tc.cacheDir(tempDir, 'node', version); | ||||
|   let toolPath = await tc.cacheDir(tempDir, 'node', version); | ||||
|   core.addPath(toolPath); | ||||
|   return toolPath; | ||||
| } | ||||
|  | ||||
| // os.arch does not always match the relative download url, e.g. | ||||
|  | ||||
							
								
								
									
										50
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as installer from './installer'; | ||||
| import * as auth from './authutil'; | ||||
| import * as path from 'path'; | ||||
| import {URL} from 'url'; | ||||
|  | ||||
| export async function run() { | ||||
|   try { | ||||
|     // | ||||
|     // 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'); | ||||
|     } | ||||
|  | ||||
|     console.log(`version: ${version}`); | ||||
|     if (version) { | ||||
|       let token = core.getInput('token'); | ||||
|       let auth = !token || isGhes() ? undefined : `token ${token}`; | ||||
|       let stable = (core.getInput('stable') || 'true').toUpperCase() === 'TRUE'; | ||||
|       await installer.getNode(version, stable, auth); | ||||
|     } | ||||
|  | ||||
|     const registryUrl: string = core.getInput('registry-url'); | ||||
|     const alwaysAuth: string = core.getInput('always-auth'); | ||||
|     if (registryUrl) { | ||||
|       auth.configAuthentication(registryUrl, alwaysAuth); | ||||
|     } | ||||
|  | ||||
|     const matchersPath = path.join(__dirname, '..', '.github'); | ||||
|     console.log(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`); | ||||
|     console.log( | ||||
|       `##[add-matcher]${path.join(matchersPath, 'eslint-stylish.json')}` | ||||
|     ); | ||||
|     console.log( | ||||
|       `##[add-matcher]${path.join(matchersPath, 'eslint-compact.json')}` | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     core.setFailed(error.message); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function isGhes(): boolean { | ||||
|   const ghUrl = new URL( | ||||
|     process.env['GITHUB_SERVER_URL'] || 'https://github.com' | ||||
|   ); | ||||
|   return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'; | ||||
| } | ||||
| @ -1,49 +1,3 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as exec from '@actions/exec'; | ||||
| import * as io from '@actions/io'; | ||||
| import * as installer from './installer'; | ||||
| import * as auth from './authutil'; | ||||
| import * as path from 'path'; | ||||
|  | ||||
| async function run() { | ||||
|   try { | ||||
|     // | ||||
|     // 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'); | ||||
|     } | ||||
|     if (version) { | ||||
|       await installer.getNode(version); | ||||
|     } | ||||
|  | ||||
|     // Output version of node and npm that are being used | ||||
|     await exec.exec('node', ['--version']); | ||||
|  | ||||
|     // Older versions of Node don't include npm, so don't let this call fail | ||||
|     await exec.exec('npm', ['--version'], { | ||||
|       ignoreReturnCode: true | ||||
|     }); | ||||
|  | ||||
|     const registryUrl: string = core.getInput('registry-url'); | ||||
|     const alwaysAuth: string = core.getInput('always-auth'); | ||||
|     if (registryUrl) { | ||||
|       auth.configAuthentication(registryUrl, alwaysAuth); | ||||
|     } | ||||
|  | ||||
|     const matchersPath = path.join(__dirname, '..', '.github'); | ||||
|     console.log(`##[add-matcher]${path.join(matchersPath, 'tsc.json')}`); | ||||
|     console.log( | ||||
|       `##[add-matcher]${path.join(matchersPath, 'eslint-stylish.json')}` | ||||
|     ); | ||||
|     console.log( | ||||
|       `##[add-matcher]${path.join(matchersPath, 'eslint-compact.json')}` | ||||
|     ); | ||||
|   } catch (error) { | ||||
|     core.setFailed(error.message); | ||||
|   } | ||||
| } | ||||
| import {run} from './main'; | ||||
|  | ||||
| run(); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Bryan MacFarlane
					Bryan MacFarlane