mirror of
				https://github.com/actions/setup-node.git
				synced 2025-10-31 16:14:00 +08:00 
			
		
		
		
	Add support for v8-canary, nightly and rc (#655)
This commit is contained in:
		
							
								
								
									
										53
									
								
								src/distributions/base-distribution-prerelease.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/distributions/base-distribution-prerelease.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import * as tc from '@actions/tool-cache'; | ||||
|  | ||||
| import semver from 'semver'; | ||||
|  | ||||
| import BaseDistribution from './base-distribution'; | ||||
| import {NodeInputs} from './base-models'; | ||||
|  | ||||
| export default abstract class BasePrereleaseNodejs extends BaseDistribution { | ||||
|   protected abstract distribution: string; | ||||
|   constructor(nodeInfo: NodeInputs) { | ||||
|     super(nodeInfo); | ||||
|   } | ||||
|  | ||||
|   protected findVersionInHostedToolCacheDirectory(): string { | ||||
|     let toolPath = ''; | ||||
|     const localVersionPaths = tc | ||||
|       .findAllVersions('node', this.nodeInfo.arch) | ||||
|       .filter(i => { | ||||
|         const prerelease = semver.prerelease(i); | ||||
|         if (!prerelease) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         return prerelease[0].includes(this.distribution); | ||||
|       }); | ||||
|     localVersionPaths.sort(semver.rcompare); | ||||
|     const localVersion = this.evaluateVersions(localVersionPaths); | ||||
|     if (localVersion) { | ||||
|       toolPath = tc.find('node', localVersion, this.nodeInfo.arch); | ||||
|     } | ||||
|  | ||||
|     return toolPath; | ||||
|   } | ||||
|  | ||||
|   protected validRange(versionSpec: string) { | ||||
|     let range: string; | ||||
|     const [raw, prerelease] = this.splitVersionSpec(versionSpec); | ||||
|     const isValidVersion = semver.valid(raw); | ||||
|     const rawVersion = (isValidVersion ? raw : semver.coerce(raw))!; | ||||
|  | ||||
|     if (prerelease !== this.distribution) { | ||||
|       range = versionSpec; | ||||
|     } else { | ||||
|       range = `${semver.validRange(`^${rawVersion}-${this.distribution}`)}-0`; | ||||
|     } | ||||
|  | ||||
|     return {range, options: {includePrerelease: !isValidVersion}}; | ||||
|   } | ||||
|  | ||||
|   protected splitVersionSpec(versionSpec: string) { | ||||
|     return versionSpec.split(/-(.*)/s); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										287
									
								
								src/distributions/base-distribution.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/distributions/base-distribution.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| import * as tc from '@actions/tool-cache'; | ||||
| import * as hc from '@actions/http-client'; | ||||
| import * as core from '@actions/core'; | ||||
| import * as io from '@actions/io'; | ||||
|  | ||||
| import semver from 'semver'; | ||||
| import * as assert from 'assert'; | ||||
|  | ||||
| import * as path from 'path'; | ||||
| import os from 'os'; | ||||
| import fs from 'fs'; | ||||
|  | ||||
| import {NodeInputs, INodeVersion, INodeVersionInfo} from './base-models'; | ||||
|  | ||||
| export default abstract class BaseDistribution { | ||||
|   protected httpClient: hc.HttpClient; | ||||
|   protected osPlat = os.platform(); | ||||
|  | ||||
|   constructor(protected nodeInfo: NodeInputs) { | ||||
|     this.httpClient = new hc.HttpClient('setup-node', [], { | ||||
|       allowRetries: true, | ||||
|       maxRetries: 3 | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   protected abstract getDistributionUrl(): string; | ||||
|  | ||||
|   public async setupNodeJs() { | ||||
|     let nodeJsVersions: INodeVersion[] | undefined; | ||||
|     if (this.nodeInfo.checkLatest) { | ||||
|       const evaluatedVersion = await this.findVersionInDist(nodeJsVersions); | ||||
|       this.nodeInfo.versionSpec = evaluatedVersion; | ||||
|     } | ||||
|  | ||||
|     let toolPath = this.findVersionInHostedToolCacheDirectory(); | ||||
|     if (toolPath) { | ||||
|       core.info(`Found in cache @ ${toolPath}`); | ||||
|     } else { | ||||
|       const evaluatedVersion = await this.findVersionInDist(nodeJsVersions); | ||||
|       const toolName = this.getNodejsDistInfo(evaluatedVersion); | ||||
|       toolPath = await this.downloadNodejs(toolName); | ||||
|     } | ||||
|  | ||||
|     if (this.osPlat != 'win32') { | ||||
|       toolPath = path.join(toolPath, 'bin'); | ||||
|     } | ||||
|  | ||||
|     core.addPath(toolPath); | ||||
|   } | ||||
|  | ||||
|   protected async findVersionInDist(nodeJsVersions?: INodeVersion[]) { | ||||
|     if (!nodeJsVersions) { | ||||
|       nodeJsVersions = await this.getNodeJsVersions(); | ||||
|     } | ||||
|     const versions = this.filterVersions(nodeJsVersions); | ||||
|     const evaluatedVersion = this.evaluateVersions(versions); | ||||
|     if (!evaluatedVersion) { | ||||
|       throw new Error( | ||||
|         `Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch}.` | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return evaluatedVersion; | ||||
|   } | ||||
|  | ||||
|   protected evaluateVersions(versions: string[]): string { | ||||
|     let version = ''; | ||||
|  | ||||
|     const {range, options} = this.validRange(this.nodeInfo.versionSpec); | ||||
|  | ||||
|     core.debug(`evaluating ${versions.length} versions`); | ||||
|  | ||||
|     for (let potential of versions) { | ||||
|       const satisfied: boolean = semver.satisfies(potential, range, options); | ||||
|       if (satisfied) { | ||||
|         version = potential; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (version) { | ||||
|       core.debug(`matched: ${version}`); | ||||
|     } else { | ||||
|       core.debug('match not found'); | ||||
|     } | ||||
|  | ||||
|     return version; | ||||
|   } | ||||
|  | ||||
|   protected findVersionInHostedToolCacheDirectory() { | ||||
|     return tc.find('node', this.nodeInfo.versionSpec, this.nodeInfo.arch); | ||||
|   } | ||||
|  | ||||
|   protected async getNodeJsVersions(): Promise<INodeVersion[]> { | ||||
|     const initialUrl = this.getDistributionUrl(); | ||||
|     const dataUrl = `${initialUrl}/index.json`; | ||||
|  | ||||
|     let response = await this.httpClient.getJson<INodeVersion[]>(dataUrl); | ||||
|     return response.result || []; | ||||
|   } | ||||
|  | ||||
|   protected getNodejsDistInfo(version: string) { | ||||
|     let osArch: string = this.translateArchToDistUrl(this.nodeInfo.arch); | ||||
|     version = semver.clean(version) || ''; | ||||
|     let fileName: string = | ||||
|       this.osPlat == 'win32' | ||||
|         ? `node-v${version}-win-${osArch}` | ||||
|         : `node-v${version}-${this.osPlat}-${osArch}`; | ||||
|     let urlFileName: string = | ||||
|       this.osPlat == 'win32' ? `${fileName}.7z` : `${fileName}.tar.gz`; | ||||
|     const initialUrl = this.getDistributionUrl(); | ||||
|     const url = `${initialUrl}/v${version}/${urlFileName}`; | ||||
|  | ||||
|     return <INodeVersionInfo>{ | ||||
|       downloadUrl: url, | ||||
|       resolvedVersion: version, | ||||
|       arch: osArch, | ||||
|       fileName: fileName | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   protected async downloadNodejs(info: INodeVersionInfo) { | ||||
|     let downloadPath = ''; | ||||
|     core.info( | ||||
|       `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}` | ||||
|     ); | ||||
|     try { | ||||
|       downloadPath = await tc.downloadTool(info.downloadUrl); | ||||
|     } catch (err) { | ||||
|       if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { | ||||
|         return await this.acquireNodeFromFallbackLocation( | ||||
|           info.resolvedVersion, | ||||
|           info.arch | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       throw err; | ||||
|     } | ||||
|  | ||||
|     let toolPath = await this.extractArchive(downloadPath, info); | ||||
|     core.info('Done'); | ||||
|  | ||||
|     return toolPath; | ||||
|   } | ||||
|  | ||||
|   protected validRange(versionSpec: string) { | ||||
|     let options: semver.Options | undefined; | ||||
|     const c = semver.clean(versionSpec) || ''; | ||||
|     const valid = semver.valid(c) ?? versionSpec; | ||||
|  | ||||
|     return {range: valid, options}; | ||||
|   } | ||||
|  | ||||
|   protected async acquireNodeFromFallbackLocation( | ||||
|     version: string, | ||||
|     arch: string = os.arch() | ||||
|   ): Promise<string> { | ||||
|     const initialUrl = this.getDistributionUrl(); | ||||
|     let osArch: string = this.translateArchToDistUrl(arch); | ||||
|  | ||||
|     // Create temporary folder to download in to | ||||
|     const tempDownloadFolder: string = | ||||
|       'temp_' + Math.floor(Math.random() * 2000000000); | ||||
|     const tempDirectory = process.env['RUNNER_TEMP'] || ''; | ||||
|     assert.ok(tempDirectory, 'Expected RUNNER_TEMP to be defined'); | ||||
|     const tempDir: string = path.join(tempDirectory, tempDownloadFolder); | ||||
|     await io.mkdirP(tempDir); | ||||
|     let exeUrl: string; | ||||
|     let libUrl: string; | ||||
|     try { | ||||
|       exeUrl = `${initialUrl}/v${version}/win-${osArch}/node.exe`; | ||||
|       libUrl = `${initialUrl}/v${version}/win-${osArch}/node.lib`; | ||||
|  | ||||
|       core.info(`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); | ||||
|       await io.cp(libPath, path.join(tempDir, 'node.lib')); | ||||
|     } catch (err) { | ||||
|       if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { | ||||
|         exeUrl = `${initialUrl}/v${version}/node.exe`; | ||||
|         libUrl = `${initialUrl}/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')); | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const toolPath = await tc.cacheDir(tempDir, 'node', version, arch); | ||||
|  | ||||
|     return toolPath; | ||||
|   } | ||||
|  | ||||
|   protected async extractArchive( | ||||
|     downloadPath: string, | ||||
|     info: INodeVersionInfo | null | ||||
|   ) { | ||||
|     // | ||||
|     // Extract | ||||
|     // | ||||
|     core.info('Extracting ...'); | ||||
|     let extPath: string; | ||||
|     info = info || ({} as INodeVersionInfo); // satisfy compiler, never null when reaches here | ||||
|     if (this.osPlat == 'win32') { | ||||
|       const _7zPath = path.join(__dirname, '../..', 'externals', '7zr.exe'); | ||||
|       extPath = await tc.extract7z(downloadPath, undefined, _7zPath); | ||||
|       // 7z extracts to folder matching file name | ||||
|       const 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 | ||||
|     // | ||||
|     core.info('Adding to the cache ...'); | ||||
|     const toolPath = await tc.cacheDir( | ||||
|       extPath, | ||||
|       'node', | ||||
|       info.resolvedVersion, | ||||
|       info.arch | ||||
|     ); | ||||
|  | ||||
|     return toolPath; | ||||
|   } | ||||
|  | ||||
|   protected getDistFileName(): string { | ||||
|     let osArch: string = this.translateArchToDistUrl(this.nodeInfo.arch); | ||||
|  | ||||
|     // node offers a json list of versions | ||||
|     let dataFileName: string; | ||||
|     switch (this.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 '${this.osPlat}'`); | ||||
|     } | ||||
|  | ||||
|     return dataFileName; | ||||
|   } | ||||
|  | ||||
|   protected filterVersions(nodeJsVersions: INodeVersion[]) { | ||||
|     const versions: string[] = []; | ||||
|  | ||||
|     const dataFileName = this.getDistFileName(); | ||||
|  | ||||
|     nodeJsVersions.forEach((nodeVersion: INodeVersion) => { | ||||
|       // ensure this version supports your os and platform | ||||
|       if (nodeVersion.files.indexOf(dataFileName) >= 0) { | ||||
|         versions.push(nodeVersion.version); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     return versions.sort(semver.rcompare); | ||||
|   } | ||||
|  | ||||
|   protected translateArchToDistUrl(arch: string): string { | ||||
|     switch (arch) { | ||||
|       case 'arm': | ||||
|         return 'armv7l'; | ||||
|       default: | ||||
|         return arch; | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/distributions/base-models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/distributions/base-models.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| export interface NodeInputs { | ||||
|   versionSpec: string; | ||||
|   arch: string; | ||||
|   auth?: string; | ||||
|   checkLatest: boolean; | ||||
|   stable: boolean; | ||||
| } | ||||
|  | ||||
| export interface INodeVersionInfo { | ||||
|   downloadUrl: string; | ||||
|   resolvedVersion: string; | ||||
|   arch: string; | ||||
|   fileName: string; | ||||
| } | ||||
|  | ||||
| export interface INodeVersion { | ||||
|   version: string; | ||||
|   files: string[]; | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/distributions/installer-factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/distributions/installer-factory.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import BaseDistribution from './base-distribution'; | ||||
| import {NodeInputs} from './base-models'; | ||||
| import NightlyNodejs from './nightly/nightly_builds'; | ||||
| import OfficialBuilds from './official_builds/official_builds'; | ||||
| import RcBuild from './rc/rc_builds'; | ||||
| import CanaryBuild from './v8-canary/canary_builds'; | ||||
|  | ||||
| enum Distributions { | ||||
|   DEFAULT = '', | ||||
|   CANARY = 'v8-canary', | ||||
|   NIGHTLY = 'nightly', | ||||
|   RC = 'rc' | ||||
| } | ||||
|  | ||||
| export function getNodejsDistribution( | ||||
|   installerOptions: NodeInputs | ||||
| ): BaseDistribution { | ||||
|   const versionSpec = installerOptions.versionSpec; | ||||
|   let distribution: BaseDistribution; | ||||
|   if (versionSpec.includes(Distributions.NIGHTLY)) { | ||||
|     distribution = new NightlyNodejs(installerOptions); | ||||
|   } else if (versionSpec.includes(Distributions.CANARY)) { | ||||
|     distribution = new CanaryBuild(installerOptions); | ||||
|   } else if (versionSpec.includes(Distributions.RC)) { | ||||
|     distribution = new RcBuild(installerOptions); | ||||
|   } else { | ||||
|     distribution = new OfficialBuilds(installerOptions); | ||||
|   } | ||||
|  | ||||
|   return distribution; | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/distributions/nightly/nightly_builds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/distributions/nightly/nightly_builds.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import BasePrereleaseNodejs from '../base-distribution-prerelease'; | ||||
| import {NodeInputs} from '../base-models'; | ||||
|  | ||||
| export default class NightlyNodejs extends BasePrereleaseNodejs { | ||||
|   protected distribution = 'nightly'; | ||||
|   constructor(nodeInfo: NodeInputs) { | ||||
|     super(nodeInfo); | ||||
|   } | ||||
|  | ||||
|   protected getDistributionUrl(): string { | ||||
|     return 'https://nodejs.org/download/nightly'; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										258
									
								
								src/distributions/official_builds/official_builds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								src/distributions/official_builds/official_builds.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,258 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as tc from '@actions/tool-cache'; | ||||
| import path from 'path'; | ||||
|  | ||||
| import BaseDistribution from '../base-distribution'; | ||||
| import {NodeInputs, INodeVersion, INodeVersionInfo} from '../base-models'; | ||||
|  | ||||
| interface INodeRelease extends tc.IToolRelease { | ||||
|   lts?: string; | ||||
| } | ||||
|  | ||||
| export default class OfficialBuilds extends BaseDistribution { | ||||
|   constructor(nodeInfo: NodeInputs) { | ||||
|     super(nodeInfo); | ||||
|   } | ||||
|  | ||||
|   public async setupNodeJs() { | ||||
|     let manifest: tc.IToolRelease[] | undefined; | ||||
|     let nodeJsVersions: INodeVersion[] | undefined; | ||||
|     const osArch = this.translateArchToDistUrl(this.nodeInfo.arch); | ||||
|     if (this.isLtsAlias(this.nodeInfo.versionSpec)) { | ||||
|       core.info('Attempt to resolve LTS alias from manifest...'); | ||||
|  | ||||
|       // No try-catch since it's not possible to resolve LTS alias without manifest | ||||
|       manifest = await this.getManifest(); | ||||
|  | ||||
|       this.nodeInfo.versionSpec = this.resolveLtsAliasFromManifest( | ||||
|         this.nodeInfo.versionSpec, | ||||
|         this.nodeInfo.stable, | ||||
|         manifest | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { | ||||
|       nodeJsVersions = await this.getNodeJsVersions(); | ||||
|       const versions = this.filterVersions(nodeJsVersions); | ||||
|       this.nodeInfo.versionSpec = this.evaluateVersions(versions); | ||||
|  | ||||
|       core.info('getting latest node version...'); | ||||
|     } | ||||
|  | ||||
|     if (this.nodeInfo.checkLatest) { | ||||
|       core.info('Attempt to resolve the latest version from manifest...'); | ||||
|       const resolvedVersion = await this.resolveVersionFromManifest( | ||||
|         this.nodeInfo.versionSpec, | ||||
|         this.nodeInfo.stable, | ||||
|         osArch, | ||||
|         manifest | ||||
|       ); | ||||
|       if (resolvedVersion) { | ||||
|         this.nodeInfo.versionSpec = resolvedVersion; | ||||
|         core.info(`Resolved as '${resolvedVersion}'`); | ||||
|       } else { | ||||
|         core.info( | ||||
|           `Failed to resolve version ${this.nodeInfo.versionSpec} from manifest` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     let toolPath = this.findVersionInHostedToolCacheDirectory(); | ||||
|  | ||||
|     if (toolPath) { | ||||
|       core.info(`Found in cache @ ${toolPath}`); | ||||
|     } else { | ||||
|       let downloadPath = ''; | ||||
|       try { | ||||
|         core.info(`Attempting to download ${this.nodeInfo.versionSpec}...`); | ||||
|  | ||||
|         const versionInfo = await this.getInfoFromManifest( | ||||
|           this.nodeInfo.versionSpec, | ||||
|           this.nodeInfo.stable, | ||||
|           osArch, | ||||
|           manifest | ||||
|         ); | ||||
|         if (versionInfo) { | ||||
|           core.info( | ||||
|             `Acquiring ${versionInfo.resolvedVersion} - ${versionInfo.arch} from ${versionInfo.downloadUrl}` | ||||
|           ); | ||||
|           downloadPath = await tc.downloadTool( | ||||
|             versionInfo.downloadUrl, | ||||
|             undefined, | ||||
|             this.nodeInfo.auth | ||||
|           ); | ||||
|  | ||||
|           if (downloadPath) { | ||||
|             toolPath = await this.extractArchive(downloadPath, versionInfo); | ||||
|           } | ||||
|         } else { | ||||
|           core.info( | ||||
|             '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) | ||||
|         ) { | ||||
|           core.info( | ||||
|             `Received HTTP status code ${err.httpStatusCode}. This usually indicates the rate limit has been exceeded` | ||||
|           ); | ||||
|         } else { | ||||
|           core.info(err.message); | ||||
|         } | ||||
|         core.debug(err.stack); | ||||
|         core.info('Falling back to download directly from Node'); | ||||
|       } | ||||
|  | ||||
|       if (!toolPath) { | ||||
|         const nodeJsVersions = await this.getNodeJsVersions(); | ||||
|         const versions = this.filterVersions(nodeJsVersions); | ||||
|         const evaluatedVersion = this.evaluateVersions(versions); | ||||
|         if (!evaluatedVersion) { | ||||
|           throw new Error( | ||||
|             `Unable to find Node version '${this.nodeInfo.versionSpec}' for platform ${this.osPlat} and architecture ${this.nodeInfo.arch}.` | ||||
|           ); | ||||
|         } | ||||
|         const toolName = this.getNodejsDistInfo(evaluatedVersion); | ||||
|         toolPath = await this.downloadNodejs(toolName); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (this.osPlat != 'win32') { | ||||
|       toolPath = path.join(toolPath, 'bin'); | ||||
|     } | ||||
|  | ||||
|     core.addPath(toolPath); | ||||
|   } | ||||
|  | ||||
|   protected evaluateVersions(versions: string[]): string { | ||||
|     let version = ''; | ||||
|  | ||||
|     if (this.isLatestSyntax(this.nodeInfo.versionSpec)) { | ||||
|       core.info(`getting latest node version...`); | ||||
|       return versions[0]; | ||||
|     } | ||||
|  | ||||
|     version = super.evaluateVersions(versions); | ||||
|  | ||||
|     return version; | ||||
|   } | ||||
|  | ||||
|   protected getDistributionUrl(): string { | ||||
|     return `https://nodejs.org/dist`; | ||||
|   } | ||||
|  | ||||
|   private getManifest(): Promise<tc.IToolRelease[]> { | ||||
|     core.debug('Getting manifest from actions/node-versions@main'); | ||||
|     return tc.getManifestFromRepo( | ||||
|       'actions', | ||||
|       'node-versions', | ||||
|       this.nodeInfo.auth, | ||||
|       'main' | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private resolveLtsAliasFromManifest( | ||||
|     versionSpec: string, | ||||
|     stable: boolean, | ||||
|     manifest: INodeRelease[] | ||||
|   ): string { | ||||
|     const alias = versionSpec.split('lts/')[1]?.toLowerCase(); | ||||
|  | ||||
|     if (!alias) { | ||||
|       throw new Error( | ||||
|         `Unable to parse LTS alias for Node version '${versionSpec}'` | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     core.debug(`LTS alias '${alias}' for Node version '${versionSpec}'`); | ||||
|  | ||||
|     // Supported formats are `lts/<alias>`, `lts/*`, and `lts/-n`. Where asterisk means highest possible LTS and -n means the nth-highest. | ||||
|     const n = Number(alias); | ||||
|     const aliases = Object.fromEntries( | ||||
|       manifest | ||||
|         .filter(x => x.lts && x.stable === stable) | ||||
|         .map(x => [x.lts!.toLowerCase(), x]) | ||||
|         .reverse() | ||||
|     ); | ||||
|     const numbered = Object.values(aliases); | ||||
|     const release = | ||||
|       alias === '*' | ||||
|         ? numbered[numbered.length - 1] | ||||
|         : n < 0 | ||||
|         ? numbered[numbered.length - 1 + n] | ||||
|         : aliases[alias]; | ||||
|  | ||||
|     if (!release) { | ||||
|       throw new Error( | ||||
|         `Unable to find LTS release '${alias}' for Node version '${versionSpec}'.` | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     core.debug( | ||||
|       `Found LTS release '${release.version}' for Node version '${versionSpec}'` | ||||
|     ); | ||||
|  | ||||
|     return release.version.split('.')[0]; | ||||
|   } | ||||
|  | ||||
|   private async resolveVersionFromManifest( | ||||
|     versionSpec: string, | ||||
|     stable: boolean, | ||||
|     osArch: string, | ||||
|     manifest: tc.IToolRelease[] | undefined | ||||
|   ): Promise<string | undefined> { | ||||
|     try { | ||||
|       const info = await this.getInfoFromManifest( | ||||
|         versionSpec, | ||||
|         stable, | ||||
|         osArch, | ||||
|         manifest | ||||
|       ); | ||||
|       return info?.resolvedVersion; | ||||
|     } catch (err) { | ||||
|       core.info('Unable to resolve version from manifest...'); | ||||
|       core.debug(err.message); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async getInfoFromManifest( | ||||
|     versionSpec: string, | ||||
|     stable: boolean, | ||||
|     osArch: string, | ||||
|     manifest: tc.IToolRelease[] | undefined | ||||
|   ): Promise<INodeVersionInfo | null> { | ||||
|     let info: INodeVersionInfo | null = null; | ||||
|     if (!manifest) { | ||||
|       core.debug('No manifest cached'); | ||||
|       manifest = await this.getManifest(); | ||||
|     } | ||||
|  | ||||
|     const rel = await tc.findFromManifest( | ||||
|       versionSpec, | ||||
|       stable, | ||||
|       manifest, | ||||
|       osArch | ||||
|     ); | ||||
|  | ||||
|     if (rel && rel.files.length > 0) { | ||||
|       info = <INodeVersionInfo>{}; | ||||
|       info.resolvedVersion = rel.version; | ||||
|       info.arch = rel.files[0].arch; | ||||
|       info.downloadUrl = rel.files[0].download_url; | ||||
|       info.fileName = rel.files[0].filename; | ||||
|     } | ||||
|  | ||||
|     return info; | ||||
|   } | ||||
|  | ||||
|   private isLtsAlias(versionSpec: string): boolean { | ||||
|     return versionSpec.startsWith('lts/'); | ||||
|   } | ||||
|  | ||||
|   private isLatestSyntax(versionSpec): boolean { | ||||
|     return ['current', 'latest', 'node'].includes(versionSpec); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										12
									
								
								src/distributions/rc/rc_builds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/distributions/rc/rc_builds.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import BaseDistribution from '../base-distribution'; | ||||
| import {NodeInputs} from '../base-models'; | ||||
|  | ||||
| export default class RcBuild extends BaseDistribution { | ||||
|   constructor(nodeInfo: NodeInputs) { | ||||
|     super(nodeInfo); | ||||
|   } | ||||
|  | ||||
|   getDistributionUrl(): string { | ||||
|     return 'https://nodejs.org/download/rc'; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/distributions/v8-canary/canary_builds.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/distributions/v8-canary/canary_builds.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import BasePrereleaseNodejs from '../base-distribution-prerelease'; | ||||
| import {NodeInputs} from '../base-models'; | ||||
|  | ||||
| export default class CanaryBuild extends BasePrereleaseNodejs { | ||||
|   protected distribution = 'v8-canary'; | ||||
|   constructor(nodeInfo: NodeInputs) { | ||||
|     super(nodeInfo); | ||||
|   } | ||||
|  | ||||
|   protected getDistributionUrl(): string { | ||||
|     return 'https://nodejs.org/download/v8-canary'; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										606
									
								
								src/installer.ts
									
									
									
									
									
								
							
							
						
						
									
										606
									
								
								src/installer.ts
									
									
									
									
									
								
							| @@ -1,606 +0,0 @@ | ||||
| import os from '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 from 'fs'; | ||||
|  | ||||
| // | ||||
| // Node versions interface | ||||
| // see https://nodejs.org/dist/index.json | ||||
| // for nightly https://nodejs.org/download/nightly/index.json | ||||
| // for rc https://nodejs.org/download/rc/index.json | ||||
| // | ||||
| export interface INodeVersion { | ||||
|   version: string; | ||||
|   files: string[]; | ||||
| } | ||||
|  | ||||
| interface INodeVersionInfo { | ||||
|   downloadUrl: string; | ||||
|   resolvedVersion: string; | ||||
|   arch: string; | ||||
|   fileName: string; | ||||
| } | ||||
|  | ||||
| interface INodeRelease extends tc.IToolRelease { | ||||
|   lts?: string; | ||||
| } | ||||
|  | ||||
| export async function getNode( | ||||
|   versionSpec: string, | ||||
|   stable: boolean, | ||||
|   checkLatest: boolean, | ||||
|   auth: string | undefined, | ||||
|   arch: string = os.arch() | ||||
| ) { | ||||
|   // Store manifest data to avoid multiple calls | ||||
|   let manifest: INodeRelease[] | undefined; | ||||
|   let nodeVersions: INodeVersion[] | undefined; | ||||
|   let isNightly = versionSpec.includes('nightly'); | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(arch); | ||||
|  | ||||
|   if (isLtsAlias(versionSpec)) { | ||||
|     core.info('Attempt to resolve LTS alias from manifest...'); | ||||
|  | ||||
|     // No try-catch since it's not possible to resolve LTS alias without manifest | ||||
|     manifest = await getManifest(auth); | ||||
|  | ||||
|     versionSpec = resolveLtsAliasFromManifest(versionSpec, stable, manifest); | ||||
|   } | ||||
|  | ||||
|   if (isLatestSyntax(versionSpec)) { | ||||
|     nodeVersions = await getVersionsFromDist(versionSpec); | ||||
|     versionSpec = await queryDistForMatch(versionSpec, arch, nodeVersions); | ||||
|     core.info(`getting latest node version...`); | ||||
|   } | ||||
|  | ||||
|   if (isNightly && checkLatest) { | ||||
|     nodeVersions = await getVersionsFromDist(versionSpec); | ||||
|     versionSpec = await queryDistForMatch(versionSpec, arch, nodeVersions); | ||||
|   } | ||||
|  | ||||
|   if (checkLatest && !isNightly) { | ||||
|     core.info('Attempt to resolve the latest version from manifest...'); | ||||
|     const resolvedVersion = await resolveVersionFromManifest( | ||||
|       versionSpec, | ||||
|       stable, | ||||
|       auth, | ||||
|       osArch, | ||||
|       manifest | ||||
|     ); | ||||
|     if (resolvedVersion) { | ||||
|       versionSpec = resolvedVersion; | ||||
|       core.info(`Resolved as '${versionSpec}'`); | ||||
|     } else { | ||||
|       core.info(`Failed to resolve version ${versionSpec} from manifest`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // check cache | ||||
|   let toolPath: string; | ||||
|   if (isNightly) { | ||||
|     const nightlyVersion = findNightlyVersionInHostedToolcache( | ||||
|       versionSpec, | ||||
|       osArch | ||||
|     ); | ||||
|     toolPath = nightlyVersion && tc.find('node', nightlyVersion, osArch); | ||||
|   } else { | ||||
|     toolPath = tc.find('node', versionSpec, osArch); | ||||
|   } | ||||
|  | ||||
|   // If not found in cache, download | ||||
|   if (toolPath) { | ||||
|     core.info(`Found in cache @ ${toolPath}`); | ||||
|   } else { | ||||
|     core.info(`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, | ||||
|         osArch, | ||||
|         manifest | ||||
|       ); | ||||
|       if (info) { | ||||
|         core.info( | ||||
|           `Acquiring ${info.resolvedVersion} - ${info.arch} from ${info.downloadUrl}` | ||||
|         ); | ||||
|         downloadPath = await tc.downloadTool(info.downloadUrl, undefined, auth); | ||||
|       } else { | ||||
|         core.info( | ||||
|           '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) | ||||
|       ) { | ||||
|         core.info( | ||||
|           `Received HTTP status code ${err.httpStatusCode}.  This usually indicates the rate limit has been exceeded` | ||||
|         ); | ||||
|       } else { | ||||
|         core.info(err.message); | ||||
|       } | ||||
|       core.debug(err.stack); | ||||
|       core.info('Falling back to download directly from Node'); | ||||
|     } | ||||
|  | ||||
|     // | ||||
|     // Download from nodejs.org | ||||
|     // | ||||
|     if (!downloadPath) { | ||||
|       info = await getInfoFromDist(versionSpec, arch, nodeVersions); | ||||
|       if (!info) { | ||||
|         throw new Error( | ||||
|           `Unable to find Node version '${versionSpec}' for platform ${osPlat} and architecture ${osArch}.` | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       core.info( | ||||
|         `Acquiring ${info.resolvedVersion} - ${info.arch} 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, | ||||
|             info.arch | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // | ||||
|     // Extract | ||||
|     // | ||||
|     core.info('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 | ||||
|     // | ||||
|     core.info('Adding to the cache ...'); | ||||
|     toolPath = await tc.cacheDir( | ||||
|       extPath, | ||||
|       'node', | ||||
|       info.resolvedVersion, | ||||
|       info.arch | ||||
|     ); | ||||
|     core.info('Done'); | ||||
|   } | ||||
|  | ||||
|   // | ||||
|   // 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') { | ||||
|     toolPath = path.join(toolPath, 'bin'); | ||||
|   } | ||||
|  | ||||
|   // | ||||
|   // prepend the tools path. instructs the agent to prepend for future tasks | ||||
|   core.addPath(toolPath); | ||||
| } | ||||
|  | ||||
| function findNightlyVersionInHostedToolcache( | ||||
|   versionsSpec: string, | ||||
|   osArch: string | ||||
| ) { | ||||
|   const foundAllVersions = tc.findAllVersions('node', osArch); | ||||
|   const version = evaluateVersions(foundAllVersions, versionsSpec); | ||||
|  | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| function isLtsAlias(versionSpec: string): boolean { | ||||
|   return versionSpec.startsWith('lts/'); | ||||
| } | ||||
|  | ||||
| function getManifest(auth: string | undefined): Promise<tc.IToolRelease[]> { | ||||
|   core.debug('Getting manifest from actions/node-versions@main'); | ||||
|   return tc.getManifestFromRepo('actions', 'node-versions', auth, 'main'); | ||||
| } | ||||
|  | ||||
| function resolveLtsAliasFromManifest( | ||||
|   versionSpec: string, | ||||
|   stable: boolean, | ||||
|   manifest: INodeRelease[] | ||||
| ): string { | ||||
|   const alias = versionSpec.split('lts/')[1]?.toLowerCase(); | ||||
|  | ||||
|   if (!alias) { | ||||
|     throw new Error( | ||||
|       `Unable to parse LTS alias for Node version '${versionSpec}'` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   core.debug(`LTS alias '${alias}' for Node version '${versionSpec}'`); | ||||
|  | ||||
|   // Supported formats are `lts/<alias>`, `lts/*`, and `lts/-n`. Where asterisk means highest possible LTS and -n means the nth-highest. | ||||
|   const n = Number(alias); | ||||
|   const aliases = Object.fromEntries( | ||||
|     manifest | ||||
|       .filter(x => x.lts && x.stable === stable) | ||||
|       .map(x => [x.lts!.toLowerCase(), x]) | ||||
|       .reverse() | ||||
|   ); | ||||
|   const numbered = Object.values(aliases); | ||||
|   const release = | ||||
|     alias === '*' | ||||
|       ? numbered[numbered.length - 1] | ||||
|       : n < 0 | ||||
|       ? numbered[numbered.length - 1 + n] | ||||
|       : aliases[alias]; | ||||
|  | ||||
|   if (!release) { | ||||
|     throw new Error( | ||||
|       `Unable to find LTS release '${alias}' for Node version '${versionSpec}'.` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   core.debug( | ||||
|     `Found LTS release '${release.version}' for Node version '${versionSpec}'` | ||||
|   ); | ||||
|  | ||||
|   return release.version.split('.')[0]; | ||||
| } | ||||
|  | ||||
| async function getInfoFromManifest( | ||||
|   versionSpec: string, | ||||
|   stable: boolean, | ||||
|   auth: string | undefined, | ||||
|   osArch: string = translateArchToDistUrl(os.arch()), | ||||
|   manifest: tc.IToolRelease[] | undefined | ||||
| ): Promise<INodeVersionInfo | null> { | ||||
|   let info: INodeVersionInfo | null = null; | ||||
|   if (!manifest) { | ||||
|     core.debug('No manifest cached'); | ||||
|     manifest = await getManifest(auth); | ||||
|   } | ||||
|  | ||||
|   const rel = await tc.findFromManifest(versionSpec, stable, manifest, osArch); | ||||
|  | ||||
|   if (rel && rel.files.length > 0) { | ||||
|     info = <INodeVersionInfo>{}; | ||||
|     info.resolvedVersion = rel.version; | ||||
|     info.arch = rel.files[0].arch; | ||||
|     info.downloadUrl = rel.files[0].download_url; | ||||
|     info.fileName = rel.files[0].filename; | ||||
|   } | ||||
|  | ||||
|   return info; | ||||
| } | ||||
|  | ||||
| async function getInfoFromDist( | ||||
|   versionSpec: string, | ||||
|   arch: string = os.arch(), | ||||
|   nodeVersions?: INodeVersion[] | ||||
| ): Promise<INodeVersionInfo | null> { | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(arch); | ||||
|  | ||||
|   let version: string = await queryDistForMatch( | ||||
|     versionSpec, | ||||
|     arch, | ||||
|     nodeVersions | ||||
|   ); | ||||
|  | ||||
|   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`; | ||||
|   const initialUrl = getNodejsDistUrl(versionSpec); | ||||
|   const url = `${initialUrl}/v${version}/${urlFileName}`; | ||||
|  | ||||
|   return <INodeVersionInfo>{ | ||||
|     downloadUrl: url, | ||||
|     resolvedVersion: version, | ||||
|     arch: arch, | ||||
|     fileName: fileName | ||||
|   }; | ||||
| } | ||||
|  | ||||
| async function resolveVersionFromManifest( | ||||
|   versionSpec: string, | ||||
|   stable: boolean, | ||||
|   auth: string | undefined, | ||||
|   osArch: string = translateArchToDistUrl(os.arch()), | ||||
|   manifest: tc.IToolRelease[] | undefined | ||||
| ): Promise<string | undefined> { | ||||
|   try { | ||||
|     const info = await getInfoFromManifest( | ||||
|       versionSpec, | ||||
|       stable, | ||||
|       auth, | ||||
|       osArch, | ||||
|       manifest | ||||
|     ); | ||||
|     return info?.resolvedVersion; | ||||
|   } catch (err) { | ||||
|     core.info('Unable to resolve version from manifest...'); | ||||
|     core.debug(err.message); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function evaluateNightlyVersions( | ||||
|   versions: string[], | ||||
|   versionSpec: string | ||||
| ): string { | ||||
|   let version = ''; | ||||
|   let range: string | undefined; | ||||
|   const [raw, prerelease] = versionSpec.split('-'); | ||||
|   const isValidVersion = semver.valid(raw); | ||||
|   const rawVersion = isValidVersion ? raw : semver.coerce(raw); | ||||
|   if (rawVersion) { | ||||
|     if (prerelease !== 'nightly') { | ||||
|       range = `${rawVersion}-${prerelease.replace('nightly', 'nightly.')}`; | ||||
|     } else { | ||||
|       range = `${semver.validRange(`^${rawVersion}-0`)}-0`; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (range) { | ||||
|     versions.sort(semver.rcompare); | ||||
|     for (const currentVersion of versions) { | ||||
|       const satisfied: boolean = | ||||
|         semver.satisfies( | ||||
|           currentVersion.replace('-nightly', '-nightly.'), | ||||
|           range, | ||||
|           {includePrerelease: true} | ||||
|         ) && currentVersion.includes('nightly'); | ||||
|       if (satisfied) { | ||||
|         version = currentVersion; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (version) { | ||||
|     core.debug(`matched: ${version}`); | ||||
|   } else { | ||||
|     core.debug('match not found'); | ||||
|   } | ||||
|  | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| // 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`); | ||||
|  | ||||
|   if (versionSpec.includes('nightly')) { | ||||
|     return evaluateNightlyVersions(versions, versionSpec); | ||||
|   } | ||||
|  | ||||
|   versions = versions.sort(semver.rcompare); | ||||
|   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; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (version) { | ||||
|     core.debug(`matched: ${version}`); | ||||
|   } else { | ||||
|     core.debug('match not found'); | ||||
|   } | ||||
|  | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| export function getNodejsDistUrl(version: string) { | ||||
|   const prerelease = semver.prerelease(version); | ||||
|   if (version.includes('nightly')) { | ||||
|     return 'https://nodejs.org/download/nightly'; | ||||
|   } else if (prerelease) { | ||||
|     return 'https://nodejs.org/download/rc'; | ||||
|   } | ||||
|  | ||||
|   return 'https://nodejs.org/dist'; | ||||
| } | ||||
|  | ||||
| async function queryDistForMatch( | ||||
|   versionSpec: string, | ||||
|   arch: string = os.arch(), | ||||
|   nodeVersions?: INodeVersion[] | ||||
| ): Promise<string> { | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(arch); | ||||
|  | ||||
|   // 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}'`); | ||||
|   } | ||||
|  | ||||
|   if (!nodeVersions) { | ||||
|     core.debug('No dist manifest cached'); | ||||
|     nodeVersions = await getVersionsFromDist(versionSpec); | ||||
|   } | ||||
|  | ||||
|   let versions: string[] = []; | ||||
|  | ||||
|   if (isLatestSyntax(versionSpec)) { | ||||
|     core.info(`getting latest node version...`); | ||||
|     return nodeVersions[0].version; | ||||
|   } | ||||
|  | ||||
|   nodeVersions.forEach((nodeVersion: INodeVersion) => { | ||||
|     // ensure this version supports your os and platform | ||||
|     if (nodeVersion.files.indexOf(dataFileName) >= 0) { | ||||
|       versions.push(nodeVersion.version); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // get the latest version that matches the version spec | ||||
|   let version = evaluateVersions(versions, versionSpec); | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| export async function getVersionsFromDist( | ||||
|   versionSpec: string | ||||
| ): Promise<INodeVersion[]> { | ||||
|   const initialUrl = getNodejsDistUrl(versionSpec); | ||||
|   const dataUrl = `${initialUrl}/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 | ||||
| // 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 | ||||
| // In this case, there will be two files located at: | ||||
| //      /dist/v5.10.1/win-x64/node.exe | ||||
| //      /dist/v5.10.1/win-x64/node.lib | ||||
| // If this is not the structure, there may also be two files located at: | ||||
| //      /dist/v0.12.18/node.exe | ||||
| //      /dist/v0.12.18/node.lib | ||||
| // 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. | ||||
| async function acquireNodeFromFallbackLocation( | ||||
|   version: string, | ||||
|   arch: string = os.arch() | ||||
| ): Promise<string> { | ||||
|   const initialUrl = getNodejsDistUrl(version); | ||||
|   let osPlat: string = os.platform(); | ||||
|   let osArch: string = translateArchToDistUrl(arch); | ||||
|  | ||||
|   // Create temporary folder to download in to | ||||
|   const tempDownloadFolder: string = | ||||
|     'temp_' + Math.floor(Math.random() * 2000000000); | ||||
|   const tempDirectory = process.env['RUNNER_TEMP'] || ''; | ||||
|   assert.ok(tempDirectory, 'Expected RUNNER_TEMP to be defined'); | ||||
|   const tempDir: string = path.join(tempDirectory, tempDownloadFolder); | ||||
|   await io.mkdirP(tempDir); | ||||
|   let exeUrl: string; | ||||
|   let libUrl: string; | ||||
|   try { | ||||
|     exeUrl = `${initialUrl}/v${version}/win-${osArch}/node.exe`; | ||||
|     libUrl = `${initialUrl}/v${version}/win-${osArch}/node.lib`; | ||||
|  | ||||
|     core.info(`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); | ||||
|     await io.cp(libPath, path.join(tempDir, 'node.lib')); | ||||
|   } catch (err) { | ||||
|     if (err instanceof tc.HTTPError && err.httpStatusCode == 404) { | ||||
|       exeUrl = `${initialUrl}/v${version}/node.exe`; | ||||
|       libUrl = `${initialUrl}/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')); | ||||
|     } else { | ||||
|       throw err; | ||||
|     } | ||||
|   } | ||||
|   let toolPath = await tc.cacheDir(tempDir, 'node', version, arch); | ||||
|   core.addPath(toolPath); | ||||
|   return toolPath; | ||||
| } | ||||
|  | ||||
| // os.arch does not always match the relative download url, e.g. | ||||
| // os.arch == 'arm' != node-v12.13.1-linux-armv7l.tar.gz | ||||
| // All other currently supported architectures match, e.g.: | ||||
| //   os.arch = arm64 => https://nodejs.org/dist/v{VERSION}/node-v{VERSION}-{OS}-arm64.tar.gz | ||||
| //   os.arch = x64 => https://nodejs.org/dist/v{VERSION}/node-v{VERSION}-{OS}-x64.tar.gz | ||||
| function translateArchToDistUrl(arch: string): string { | ||||
|   switch (arch) { | ||||
|     case 'arm': | ||||
|       return 'armv7l'; | ||||
|     default: | ||||
|       return arch; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function parseNodeVersionFile(contents: string): string { | ||||
|   let nodeVersion: string | undefined; | ||||
|  | ||||
|   // Try parsing the file as an NPM `package.json` file. | ||||
|   try { | ||||
|     nodeVersion = JSON.parse(contents).volta?.node; | ||||
|     if (!nodeVersion) nodeVersion = JSON.parse(contents).engines?.node; | ||||
|   } catch { | ||||
|     core.info('Node version file is not JSON file'); | ||||
|   } | ||||
|  | ||||
|   if (!nodeVersion) { | ||||
|     const found = contents.match(/^(?:nodejs\s+)?v?(?<version>[^\s]+)$/m); | ||||
|     nodeVersion = found?.groups?.version; | ||||
|   } | ||||
|  | ||||
|   // In the case of an unknown format, | ||||
|   // return as is and evaluate the version separately. | ||||
|   if (!nodeVersion) nodeVersion = contents.trim(); | ||||
|  | ||||
|   return nodeVersion as string; | ||||
| } | ||||
|  | ||||
| function isLatestSyntax(versionSpec): boolean { | ||||
|   return ['current', 'latest', 'node'].includes(versionSpec); | ||||
| } | ||||
							
								
								
									
										60
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								src/main.ts
									
									
									
									
									
								
							| @@ -1,12 +1,14 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as exec from '@actions/exec'; | ||||
| import * as installer from './installer'; | ||||
|  | ||||
| import fs from 'fs'; | ||||
| import os from 'os'; | ||||
|  | ||||
| import * as auth from './authutil'; | ||||
| import * as path from 'path'; | ||||
| import {restoreCache} from './cache-restore'; | ||||
| import {isGhes, isCacheFeatureAvailable} from './cache-utils'; | ||||
| import os from 'os'; | ||||
| import {isCacheFeatureAvailable} from './cache-utils'; | ||||
| import {getNodejsDistribution} from './distributions/installer-factory'; | ||||
| import {parseNodeVersionFile, printEnvDetailsAndSetOutput} from './util'; | ||||
|  | ||||
| export async function run() { | ||||
|   try { | ||||
| @@ -38,7 +40,15 @@ export async function run() { | ||||
|         (core.getInput('stable') || 'true').toUpperCase() === 'TRUE'; | ||||
|       const checkLatest = | ||||
|         (core.getInput('check-latest') || 'false').toUpperCase() === 'TRUE'; | ||||
|       await installer.getNode(version, stable, checkLatest, auth, arch); | ||||
|       const nodejsInfo = { | ||||
|         versionSpec: version, | ||||
|         checkLatest, | ||||
|         auth, | ||||
|         stable, | ||||
|         arch | ||||
|       }; | ||||
|       const nodeDistribution = getNodejsDistribution(nodejsInfo); | ||||
|       await nodeDistribution.setupNodeJs(); | ||||
|     } | ||||
|  | ||||
|     await printEnvDetailsAndSetOutput(); | ||||
| @@ -93,48 +103,10 @@ function resolveVersionInput(): string { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     version = installer.parseNodeVersionFile( | ||||
|       fs.readFileSync(versionFilePath, 'utf8') | ||||
|     ); | ||||
|     version = parseNodeVersionFile(fs.readFileSync(versionFilePath, 'utf8')); | ||||
|  | ||||
|     core.info(`Resolved ${versionFileInput} as ${version}`); | ||||
|   } | ||||
|  | ||||
|   return version; | ||||
| } | ||||
|  | ||||
| export async function printEnvDetailsAndSetOutput() { | ||||
|   core.startGroup('Environment details'); | ||||
|  | ||||
|   const promises = ['node', 'npm', 'yarn'].map(async tool => { | ||||
|     const output = await getToolVersion(tool, ['--version']); | ||||
|  | ||||
|     if (tool === 'node') { | ||||
|       core.setOutput(`${tool}-version`, output); | ||||
|     } | ||||
|  | ||||
|     core.info(`${tool}: ${output}`); | ||||
|   }); | ||||
|  | ||||
|   await Promise.all(promises); | ||||
|  | ||||
|   core.endGroup(); | ||||
| } | ||||
|  | ||||
| async function getToolVersion(tool: string, options: string[]) { | ||||
|   try { | ||||
|     const {stdout, stderr, exitCode} = await exec.getExecOutput(tool, options, { | ||||
|       ignoreReturnCode: true, | ||||
|       silent: true | ||||
|     }); | ||||
|  | ||||
|     if (exitCode > 0) { | ||||
|       core.warning(`[warning]${stderr}`); | ||||
|       return ''; | ||||
|     } | ||||
|  | ||||
|     return stdout.trim(); | ||||
|   } catch (err) { | ||||
|     return ''; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										63
									
								
								src/util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/util.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| import * as core from '@actions/core'; | ||||
| import * as exec from '@actions/exec'; | ||||
|  | ||||
| export function parseNodeVersionFile(contents: string): string { | ||||
|   let nodeVersion: string | undefined; | ||||
|  | ||||
|   // Try parsing the file as an NPM `package.json` file. | ||||
|   try { | ||||
|     nodeVersion = JSON.parse(contents).volta?.node; | ||||
|     if (!nodeVersion) nodeVersion = JSON.parse(contents).engines?.node; | ||||
|   } catch { | ||||
|     core.info('Node version file is not JSON file'); | ||||
|   } | ||||
|  | ||||
|   if (!nodeVersion) { | ||||
|     const found = contents.match(/^(?:nodejs\s+)?v?(?<version>[^\s]+)$/m); | ||||
|     nodeVersion = found?.groups?.version; | ||||
|   } | ||||
|  | ||||
|   // In the case of an unknown format, | ||||
|   // return as is and evaluate the version separately. | ||||
|   if (!nodeVersion) nodeVersion = contents.trim(); | ||||
|  | ||||
|   return nodeVersion as string; | ||||
| } | ||||
|  | ||||
| export async function printEnvDetailsAndSetOutput() { | ||||
|   core.startGroup('Environment details'); | ||||
|  | ||||
|   const promises = ['node', 'npm', 'yarn'].map(async tool => { | ||||
|     const output = await getToolVersion(tool, ['--version']); | ||||
|  | ||||
|     return {tool, output}; | ||||
|   }); | ||||
|  | ||||
|   const tools = await Promise.all(promises); | ||||
|   tools.forEach(({tool, output}) => { | ||||
|     if (tool === 'node') { | ||||
|       core.setOutput(`${tool}-version`, output); | ||||
|     } | ||||
|     core.info(`${tool}: ${output}`); | ||||
|   }); | ||||
|  | ||||
|   core.endGroup(); | ||||
| } | ||||
|  | ||||
| async function getToolVersion(tool: string, options: string[]) { | ||||
|   try { | ||||
|     const {stdout, stderr, exitCode} = await exec.getExecOutput(tool, options, { | ||||
|       ignoreReturnCode: true, | ||||
|       silent: true | ||||
|     }); | ||||
|  | ||||
|     if (exitCode > 0) { | ||||
|       core.info(`[warning]${stderr}`); | ||||
|       return ''; | ||||
|     } | ||||
|  | ||||
|     return stdout.trim(); | ||||
|   } catch (err) { | ||||
|     return ''; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Dmitry Shibanov
					Dmitry Shibanov