mirror of https://github.com/actions/setup-go.git
				
				
				
			
		
			
				
	
	
		
			588 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			588 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import * as tc from '@actions/tool-cache';
 | |
| import * as core from '@actions/core';
 | |
| import * as path from 'path';
 | |
| import * as semver from 'semver';
 | |
| import * as httpm from '@actions/http-client';
 | |
| import * as sys from './system';
 | |
| import fs from 'fs';
 | |
| import os from 'os';
 | |
| import {StableReleaseAlias, isSelfHosted} from './utils';
 | |
| import {Architecture} from './types';
 | |
| 
 | |
| export const GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN';
 | |
| export const GOTOOLCHAIN_LOCAL_VAL = 'local';
 | |
| const MANIFEST_REPO_OWNER = 'actions';
 | |
| const MANIFEST_REPO_NAME = 'go-versions';
 | |
| const MANIFEST_REPO_BRANCH = 'main';
 | |
| const MANIFEST_URL = `https://raw.githubusercontent.com/${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}/${MANIFEST_REPO_BRANCH}/versions-manifest.json`;
 | |
| 
 | |
| type InstallationType = 'dist' | 'manifest';
 | |
| 
 | |
| const GOLANG_DOWNLOAD_URL = 'https://go.dev/dl/?mode=json&include=all';
 | |
| 
 | |
| export interface IGoVersionFile {
 | |
|   filename: string;
 | |
|   // darwin, linux, windows
 | |
|   os: string;
 | |
|   arch: string;
 | |
| }
 | |
| 
 | |
| export interface IGoVersion {
 | |
|   version: string;
 | |
|   stable: boolean;
 | |
|   files: IGoVersionFile[];
 | |
| }
 | |
| 
 | |
| export interface IGoVersionInfo {
 | |
|   type: InstallationType;
 | |
|   downloadUrl: string;
 | |
|   resolvedVersion: string;
 | |
|   fileName: string;
 | |
| }
 | |
| 
 | |
| export async function getGo(
 | |
|   versionSpec: string,
 | |
|   checkLatest: boolean,
 | |
|   auth: string | undefined,
 | |
|   arch: Architecture = os.arch() as Architecture
 | |
| ) {
 | |
|   let manifest: tc.IToolRelease[] | undefined;
 | |
|   const osPlat: string = os.platform();
 | |
| 
 | |
|   if (
 | |
|     versionSpec === StableReleaseAlias.Stable ||
 | |
|     versionSpec === StableReleaseAlias.OldStable
 | |
|   ) {
 | |
|     manifest = await getManifest(auth);
 | |
|     let stableVersion = await resolveStableVersionInput(
 | |
|       versionSpec,
 | |
|       arch,
 | |
|       osPlat,
 | |
|       manifest
 | |
|     );
 | |
| 
 | |
|     if (!stableVersion) {
 | |
|       stableVersion = await resolveStableVersionDist(versionSpec, arch);
 | |
|       if (!stableVersion) {
 | |
|         throw new Error(
 | |
|           `Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.`
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     core.info(`${versionSpec} version resolved as ${stableVersion}`);
 | |
| 
 | |
|     versionSpec = stableVersion;
 | |
|   }
 | |
| 
 | |
|   if (checkLatest) {
 | |
|     core.info('Attempting to resolve the latest version from the manifest...');
 | |
|     const resolvedVersion = await resolveVersionFromManifest(
 | |
|       versionSpec,
 | |
|       true,
 | |
|       auth,
 | |
|       arch,
 | |
|       manifest
 | |
|     );
 | |
|     if (resolvedVersion) {
 | |
|       versionSpec = resolvedVersion;
 | |
|       core.info(`Resolved as '${versionSpec}'`);
 | |
|     } else {
 | |
|       core.info(`Failed to resolve version ${versionSpec} from manifest`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // check cache
 | |
|   const toolPath = tc.find('go', versionSpec, arch);
 | |
|   // If not found in cache, download
 | |
|   if (toolPath) {
 | |
|     core.info(`Found in cache @ ${toolPath}`);
 | |
|     return toolPath;
 | |
|   }
 | |
|   core.info(`Attempting to download ${versionSpec}...`);
 | |
|   let downloadPath = '';
 | |
|   let info: IGoVersionInfo | null = null;
 | |
| 
 | |
|   //
 | |
|   // Try download from internal distribution (popular versions only)
 | |
|   //
 | |
|   try {
 | |
|     info = await getInfoFromManifest(versionSpec, true, auth, arch, manifest);
 | |
|     if (info) {
 | |
|       downloadPath = await installGoVersion(info, auth, arch);
 | |
|     } else {
 | |
|       core.info(
 | |
|         'Not found in manifest.  Falling back to download directly from Go'
 | |
|       );
 | |
|     }
 | |
|   } catch (err) {
 | |
|     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 as Error).message);
 | |
|     }
 | |
|     core.debug((err as Error).stack ?? '');
 | |
|     core.info('Falling back to download directly from Go');
 | |
|   }
 | |
| 
 | |
|   //
 | |
|   // Download from storage.googleapis.com
 | |
|   //
 | |
|   if (!downloadPath) {
 | |
|     info = await getInfoFromDist(versionSpec, arch);
 | |
|     if (!info) {
 | |
|       throw new Error(
 | |
|         `Unable to find Go version '${versionSpec}' for platform ${osPlat} and architecture ${arch}.`
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       core.info('Install from dist');
 | |
|       downloadPath = await installGoVersion(info, undefined, arch);
 | |
|     } catch (err) {
 | |
|       throw new Error(`Failed to download version ${versionSpec}: ${err}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return downloadPath;
 | |
| }
 | |
| 
 | |
| async function resolveVersionFromManifest(
 | |
|   versionSpec: string,
 | |
|   stable: boolean,
 | |
|   auth: string | undefined,
 | |
|   arch: Architecture,
 | |
|   manifest: tc.IToolRelease[] | undefined
 | |
| ): Promise<string | undefined> {
 | |
|   try {
 | |
|     const info = await getInfoFromManifest(
 | |
|       versionSpec,
 | |
|       stable,
 | |
|       auth,
 | |
|       arch,
 | |
|       manifest
 | |
|     );
 | |
|     return info?.resolvedVersion;
 | |
|   } catch (err) {
 | |
|     core.info('Unable to resolve a version from the manifest...');
 | |
|     core.debug((err as Error).message);
 | |
|   }
 | |
| }
 | |
| 
 | |
| // for github hosted windows runner handle latency of OS drive
 | |
| // by avoiding write operations to C:
 | |
| async function cacheWindowsDir(
 | |
|   extPath: string,
 | |
|   tool: string,
 | |
|   version: string,
 | |
|   arch: string
 | |
| ): Promise<string | false> {
 | |
|   if (os.platform() !== 'win32') return false;
 | |
| 
 | |
|   // make sure the action runs in the hosted environment
 | |
|   if (isSelfHosted()) return false;
 | |
| 
 | |
|   const defaultToolCacheRoot = process.env['RUNNER_TOOL_CACHE'];
 | |
|   if (!defaultToolCacheRoot) return false;
 | |
| 
 | |
|   if (!fs.existsSync('d:\\') || !fs.existsSync('c:\\')) return false;
 | |
| 
 | |
|   const actualToolCacheRoot = defaultToolCacheRoot
 | |
|     .replace('C:', 'D:')
 | |
|     .replace('c:', 'd:');
 | |
|   // make toolcache root to be on drive d:
 | |
|   process.env['RUNNER_TOOL_CACHE'] = actualToolCacheRoot;
 | |
| 
 | |
|   const actualToolCacheDir = await tc.cacheDir(extPath, tool, version, arch);
 | |
| 
 | |
|   // create a link from c: to d:
 | |
|   const defaultToolCacheDir = actualToolCacheDir.replace(
 | |
|     actualToolCacheRoot,
 | |
|     defaultToolCacheRoot
 | |
|   );
 | |
|   fs.mkdirSync(path.dirname(defaultToolCacheDir), {recursive: true});
 | |
|   fs.symlinkSync(actualToolCacheDir, defaultToolCacheDir, 'junction');
 | |
|   core.info(`Created link ${defaultToolCacheDir} => ${actualToolCacheDir}`);
 | |
| 
 | |
|   const actualToolCacheCompleteFile = `${actualToolCacheDir}.complete`;
 | |
|   const defaultToolCacheCompleteFile = `${defaultToolCacheDir}.complete`;
 | |
|   fs.symlinkSync(
 | |
|     actualToolCacheCompleteFile,
 | |
|     defaultToolCacheCompleteFile,
 | |
|     'file'
 | |
|   );
 | |
|   core.info(
 | |
|     `Created link ${defaultToolCacheCompleteFile} => ${actualToolCacheCompleteFile}`
 | |
|   );
 | |
| 
 | |
|   // make outer code to continue using toolcache as if it were installed on c:
 | |
|   // restore toolcache root to default drive c:
 | |
|   process.env['RUNNER_TOOL_CACHE'] = defaultToolCacheRoot;
 | |
|   return defaultToolCacheDir;
 | |
| }
 | |
| 
 | |
| async function addExecutablesToToolCache(
 | |
|   extPath: string,
 | |
|   info: IGoVersionInfo,
 | |
|   arch: string
 | |
| ): Promise<string> {
 | |
|   const tool = 'go';
 | |
|   const version = makeSemver(info.resolvedVersion);
 | |
|   return (
 | |
|     (await cacheWindowsDir(extPath, tool, version, arch)) ||
 | |
|     (await tc.cacheDir(extPath, tool, version, arch))
 | |
|   );
 | |
| }
 | |
| 
 | |
| async function installGoVersion(
 | |
|   info: IGoVersionInfo,
 | |
|   auth: string | undefined,
 | |
|   arch: string
 | |
| ): Promise<string> {
 | |
|   core.info(`Acquiring ${info.resolvedVersion} from ${info.downloadUrl}`);
 | |
| 
 | |
|   // Windows requires that we keep the extension (.zip) for extraction
 | |
|   const isWindows = os.platform() === 'win32';
 | |
|   const tempDir = process.env.RUNNER_TEMP || '.';
 | |
|   const fileName = isWindows ? path.join(tempDir, info.fileName) : undefined;
 | |
| 
 | |
|   const downloadPath = await tc.downloadTool(info.downloadUrl, fileName, auth);
 | |
| 
 | |
|   core.info('Extracting Go...');
 | |
|   let extPath = await extractGoArchive(downloadPath);
 | |
|   core.info(`Successfully extracted go to ${extPath}`);
 | |
|   if (info.type === 'dist') {
 | |
|     extPath = path.join(extPath, 'go');
 | |
|   }
 | |
| 
 | |
|   core.info('Adding to the cache ...');
 | |
|   const toolCacheDir = await addExecutablesToToolCache(extPath, info, arch);
 | |
|   core.info(`Successfully cached go to ${toolCacheDir}`);
 | |
| 
 | |
|   return toolCacheDir;
 | |
| }
 | |
| 
 | |
| export async function extractGoArchive(archivePath: string): Promise<string> {
 | |
|   const platform = os.platform();
 | |
|   let extPath: string;
 | |
| 
 | |
|   if (platform === 'win32') {
 | |
|     extPath = await tc.extractZip(archivePath);
 | |
|   } else {
 | |
|     extPath = await tc.extractTar(archivePath);
 | |
|   }
 | |
| 
 | |
|   return extPath;
 | |
| }
 | |
| 
 | |
| function isIToolRelease(obj: any): obj is tc.IToolRelease {
 | |
|   return (
 | |
|     typeof obj === 'object' &&
 | |
|     obj !== null &&
 | |
|     typeof obj.version === 'string' &&
 | |
|     typeof obj.stable === 'boolean' &&
 | |
|     Array.isArray(obj.files) &&
 | |
|     obj.files.every(
 | |
|       (file: any) =>
 | |
|         typeof file.filename === 'string' &&
 | |
|         typeof file.platform === 'string' &&
 | |
|         typeof file.arch === 'string' &&
 | |
|         typeof file.download_url === 'string'
 | |
|     )
 | |
|   );
 | |
| }
 | |
| 
 | |
| export async function getManifest(
 | |
|   auth: string | undefined
 | |
| ): Promise<tc.IToolRelease[]> {
 | |
|   try {
 | |
|     const manifest = await getManifestFromRepo(auth);
 | |
|     if (
 | |
|       Array.isArray(manifest) &&
 | |
|       manifest.length &&
 | |
|       manifest.every(isIToolRelease)
 | |
|     ) {
 | |
|       return manifest;
 | |
|     }
 | |
| 
 | |
|     let errorMessage =
 | |
|       'An unexpected error occurred while fetching the manifest.';
 | |
|     if (
 | |
|       typeof manifest === 'object' &&
 | |
|       manifest !== null &&
 | |
|       'message' in manifest
 | |
|     ) {
 | |
|       errorMessage = (manifest as {message: string}).message;
 | |
|     }
 | |
|     throw new Error(errorMessage);
 | |
|   } catch (err) {
 | |
|     core.debug('Fetching the manifest via the API failed.');
 | |
|     if (err instanceof Error) {
 | |
|       core.debug(err.message);
 | |
|     }
 | |
|   }
 | |
|   return await getManifestFromURL();
 | |
| }
 | |
| 
 | |
| function getManifestFromRepo(
 | |
|   auth: string | undefined
 | |
| ): Promise<tc.IToolRelease[]> {
 | |
|   core.debug(
 | |
|     `Getting manifest from ${MANIFEST_REPO_OWNER}/${MANIFEST_REPO_NAME}@${MANIFEST_REPO_BRANCH}`
 | |
|   );
 | |
|   return tc.getManifestFromRepo(
 | |
|     MANIFEST_REPO_OWNER,
 | |
|     MANIFEST_REPO_NAME,
 | |
|     auth,
 | |
|     MANIFEST_REPO_BRANCH
 | |
|   );
 | |
| }
 | |
| 
 | |
| async function getManifestFromURL(): Promise<tc.IToolRelease[]> {
 | |
|   core.debug('Falling back to fetching the manifest using raw URL.');
 | |
| 
 | |
|   const http: httpm.HttpClient = new httpm.HttpClient('tool-cache');
 | |
|   const response = await http.getJson<tc.IToolRelease[]>(MANIFEST_URL);
 | |
|   if (!response.result) {
 | |
|     throw new Error(`Unable to get manifest from ${MANIFEST_URL}`);
 | |
|   }
 | |
|   return response.result;
 | |
| }
 | |
| 
 | |
| export async function getInfoFromManifest(
 | |
|   versionSpec: string,
 | |
|   stable: boolean,
 | |
|   auth: string | undefined,
 | |
|   arch: Architecture = os.arch() as Architecture,
 | |
|   manifest?: tc.IToolRelease[] | undefined
 | |
| ): Promise<IGoVersionInfo | null> {
 | |
|   let info: IGoVersionInfo | null = null;
 | |
|   if (!manifest) {
 | |
|     core.debug('No manifest cached');
 | |
|     manifest = await getManifest(auth);
 | |
|   }
 | |
| 
 | |
|   core.info(`matching ${versionSpec}...`);
 | |
| 
 | |
|   const rel = await tc.findFromManifest(versionSpec, stable, manifest, arch);
 | |
| 
 | |
|   if (rel && rel.files.length > 0) {
 | |
|     info = <IGoVersionInfo>{};
 | |
|     info.type = 'manifest';
 | |
|     info.resolvedVersion = rel.version;
 | |
|     info.downloadUrl = rel.files[0].download_url;
 | |
|     info.fileName = rel.files[0].filename;
 | |
|   }
 | |
| 
 | |
|   return info;
 | |
| }
 | |
| 
 | |
| async function getInfoFromDist(
 | |
|   versionSpec: string,
 | |
|   arch: Architecture
 | |
| ): Promise<IGoVersionInfo | null> {
 | |
|   const version: IGoVersion | undefined = await findMatch(versionSpec, arch);
 | |
|   if (!version) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const downloadUrl = `https://go.dev/dl/${version.files[0].filename}`;
 | |
| 
 | |
|   return <IGoVersionInfo>{
 | |
|     type: 'dist',
 | |
|     downloadUrl: downloadUrl,
 | |
|     resolvedVersion: version.version,
 | |
|     fileName: version.files[0].filename
 | |
|   };
 | |
| }
 | |
| 
 | |
| export async function findMatch(
 | |
|   versionSpec: string,
 | |
|   arch: Architecture = os.arch() as Architecture
 | |
| ): Promise<IGoVersion | undefined> {
 | |
|   const archFilter = sys.getArch(arch);
 | |
|   const platFilter = sys.getPlatform();
 | |
| 
 | |
|   let result: IGoVersion | undefined;
 | |
|   let match: IGoVersion | undefined;
 | |
| 
 | |
|   const candidates: IGoVersion[] | null = await module.exports.getVersionsDist(
 | |
|     GOLANG_DOWNLOAD_URL
 | |
|   );
 | |
|   if (!candidates) {
 | |
|     throw new Error(`golang download url did not return results`);
 | |
|   }
 | |
| 
 | |
|   let goFile: IGoVersionFile | undefined;
 | |
|   for (let i = 0; i < candidates.length; i++) {
 | |
|     const candidate: IGoVersion = candidates[i];
 | |
|     const version = makeSemver(candidate.version);
 | |
| 
 | |
|     core.debug(`check ${version} satisfies ${versionSpec}`);
 | |
|     if (semver.satisfies(version, versionSpec)) {
 | |
|       goFile = candidate.files.find(file => {
 | |
|         core.debug(
 | |
|           `${file.arch}===${archFilter} && ${file.os}===${platFilter}`
 | |
|         );
 | |
|         return file.arch === archFilter && file.os === platFilter;
 | |
|       });
 | |
| 
 | |
|       if (goFile) {
 | |
|         core.debug(`matched ${candidate.version}`);
 | |
|         match = candidate;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (match && goFile) {
 | |
|     // clone since we're mutating the file list to be only the file that matches
 | |
|     result = <IGoVersion>Object.assign({}, match);
 | |
|     result.files = [goFile];
 | |
|   }
 | |
| 
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| export async function getVersionsDist(
 | |
|   dlUrl: string
 | |
| ): Promise<IGoVersion[] | null> {
 | |
|   // this returns versions descending so latest is first
 | |
|   const http: httpm.HttpClient = new httpm.HttpClient('setup-go', [], {
 | |
|     allowRedirects: true,
 | |
|     maxRedirects: 3
 | |
|   });
 | |
|   return (await http.getJson<IGoVersion[]>(dlUrl)).result;
 | |
| }
 | |
| 
 | |
| //
 | |
| // Convert the go version syntax into semver for semver matching
 | |
| // 1.13.1 => 1.13.1
 | |
| // 1.13 => 1.13.0
 | |
| // 1.10beta1 => 1.10.0-beta.1, 1.10rc1 => 1.10.0-rc.1
 | |
| // 1.8.5beta1 => 1.8.5-beta.1, 1.8.5rc1 => 1.8.5-rc.1
 | |
| export function makeSemver(version: string): string {
 | |
|   version = version.replace('go', '');
 | |
|   version = version.replace('beta', '-beta.').replace('rc', '-rc.');
 | |
|   const parts = version.split('-');
 | |
| 
 | |
|   const semVersion = semver.coerce(parts[0])?.version;
 | |
|   if (!semVersion) {
 | |
|     throw new Error(
 | |
|       `The version: ${version} can't be changed to SemVer notation`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   if (!parts[1]) {
 | |
|     return semVersion;
 | |
|   }
 | |
| 
 | |
|   const fullVersion = semver.valid(`${semVersion}-${parts[1]}`);
 | |
| 
 | |
|   if (!fullVersion) {
 | |
|     throw new Error(
 | |
|       `The version: ${version} can't be changed to SemVer notation`
 | |
|     );
 | |
|   }
 | |
|   return fullVersion;
 | |
| }
 | |
| 
 | |
| export function parseGoVersionFile(versionFilePath: string): string {
 | |
|   const contents = fs.readFileSync(versionFilePath).toString();
 | |
| 
 | |
|   if (
 | |
|     path.basename(versionFilePath) === 'go.mod' ||
 | |
|     path.basename(versionFilePath) === 'go.work'
 | |
|   ) {
 | |
|     // for backwards compatibility: use version from go directive if
 | |
|     // 'GOTOOLCHAIN' has been explicitly set
 | |
|     if (process.env[GOTOOLCHAIN_ENV_VAR] !== GOTOOLCHAIN_LOCAL_VAL) {
 | |
|       // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain
 | |
|       const matchToolchain = contents.match(
 | |
|         /^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m
 | |
|       );
 | |
|       if (matchToolchain) {
 | |
|         return matchToolchain[1];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // go directive: https://go.dev/ref/mod#go-mod-file-go
 | |
|     const matchGo = contents.match(/^go (\d+(\.\d+)*)/m);
 | |
|     return matchGo ? matchGo[1] : '';
 | |
|   } else if (path.basename(versionFilePath) === '.tool-versions') {
 | |
|     const match = contents.match(/^golang\s+([^\n#]+)/m);
 | |
|     return match ? match[1].trim() : '';
 | |
|   }
 | |
| 
 | |
|   return contents.trim();
 | |
| }
 | |
| 
 | |
| async function resolveStableVersionDist(
 | |
|   versionSpec: string,
 | |
|   arch: Architecture
 | |
| ) {
 | |
|   const archFilter = sys.getArch(arch);
 | |
|   const platFilter = sys.getPlatform();
 | |
|   const candidates: IGoVersion[] | null = await module.exports.getVersionsDist(
 | |
|     GOLANG_DOWNLOAD_URL
 | |
|   );
 | |
|   if (!candidates) {
 | |
|     throw new Error(`golang download url did not return results`);
 | |
|   }
 | |
| 
 | |
|   const fixedCandidates = candidates.map(item => {
 | |
|     return {
 | |
|       ...item,
 | |
|       version: makeSemver(item.version)
 | |
|     };
 | |
|   });
 | |
| 
 | |
|   const stableVersion = await resolveStableVersionInput(
 | |
|     versionSpec,
 | |
|     archFilter,
 | |
|     platFilter,
 | |
|     fixedCandidates
 | |
|   );
 | |
| 
 | |
|   return stableVersion;
 | |
| }
 | |
| 
 | |
| export async function resolveStableVersionInput(
 | |
|   versionSpec: string,
 | |
|   arch: string,
 | |
|   platform: string,
 | |
|   manifest: tc.IToolRelease[] | IGoVersion[]
 | |
| ) {
 | |
|   const releases = manifest
 | |
|     .map(item => {
 | |
|       const index = item.files.findIndex(
 | |
|         item => item.arch === arch && item.filename.includes(platform)
 | |
|       );
 | |
|       if (index === -1) {
 | |
|         return '';
 | |
|       }
 | |
|       return item.version;
 | |
|     })
 | |
|     .filter(item => !!item && !semver.prerelease(item));
 | |
| 
 | |
|   if (versionSpec === StableReleaseAlias.Stable) {
 | |
|     return releases[0];
 | |
|   } else {
 | |
|     const versions = releases.map(
 | |
|       release => `${semver.major(release)}.${semver.minor(release)}`
 | |
|     );
 | |
|     const uniqueVersions = Array.from(new Set(versions));
 | |
| 
 | |
|     const oldstableVersion = releases.find(item =>
 | |
|       item.startsWith(uniqueVersions[1])
 | |
|     );
 | |
| 
 | |
|     return oldstableVersion;
 | |
|   }
 | |
| }
 |