2021-06-16 09:52:44 +03:00
import * as core from '@actions/core' ;
import * as exec from '@actions/exec' ;
2022-03-31 21:10:37 +02:00
import * as cache from '@actions/cache' ;
2023-05-11 09:40:44 +02:00
import * as glob from '@actions/glob' ;
2023-04-10 16:52:10 +02:00
import path from 'path' ;
2023-05-04 16:05:46 +02:00
import fs from 'fs' ;
2021-06-16 09:52:44 +03:00
export interface PackageManagerInfo {
2023-05-11 09:40:44 +02:00
name : string ;
2021-06-16 09:52:44 +03:00
lockFilePatterns : Array < string > ;
2023-05-11 09:40:44 +02:00
getCacheFolderPath : ( projectDir? : string ) = > Promise < string > ;
}
interface SupportedPackageManagers {
npm : PackageManagerInfo ;
pnpm : PackageManagerInfo ;
yarn : PackageManagerInfo ;
2021-06-16 09:52:44 +03:00
}
2023-05-11 09:40:44 +02:00
// for testing purposes
export const npmGetCacheFolderCommand = 'npm config get cache' ;
export const pnpmGetCacheFolderCommand = 'pnpm store path --silent' ;
export const yarn1GetCacheFolderCommand = 'yarn cache dir' ;
export const yarn2GetCacheFolderCommand = 'yarn config get cacheFolder' ;
2021-06-16 09:52:44 +03:00
export const supportedPackageManagers : SupportedPackageManagers = {
npm : {
2023-05-11 09:40:44 +02:00
name : 'npm' ,
2022-07-04 17:29:56 -04:00
lockFilePatterns : [ 'package-lock.json' , 'npm-shrinkwrap.json' , 'yarn.lock' ] ,
2023-05-11 09:40:44 +02:00
getCacheFolderPath : ( ) = >
getCommandOutputGuarded (
npmGetCacheFolderCommand ,
'Could not get npm cache folder path'
)
2021-06-16 09:52:44 +03:00
} ,
2021-06-30 16:44:51 +01:00
pnpm : {
2023-05-11 09:40:44 +02:00
name : 'pnpm' ,
2021-06-30 16:44:51 +01:00
lockFilePatterns : [ 'pnpm-lock.yaml' ] ,
2023-05-11 09:40:44 +02:00
getCacheFolderPath : ( ) = >
getCommandOutputGuarded (
pnpmGetCacheFolderCommand ,
'Could not get pnpm cache folder path'
)
2021-06-16 09:52:44 +03:00
} ,
2023-05-11 09:40:44 +02:00
yarn : {
name : 'yarn' ,
2021-06-16 09:52:44 +03:00
lockFilePatterns : [ 'yarn.lock' ] ,
2023-05-11 09:40:44 +02:00
getCacheFolderPath : async projectDir = > {
const yarnVersion = await getCommandOutputGuarded (
` yarn --version ` ,
'Could not retrieve version of yarn' ,
projectDir
) ;
2023-05-19 19:57:43 +02:00
core . debug (
` Consumed yarn version is ${ yarnVersion } (working dir: " ${ projectDir } ") `
) ;
2023-05-11 09:40:44 +02:00
const stdOut = yarnVersion . startsWith ( '1.' )
? await getCommandOutput ( yarn1GetCacheFolderCommand , projectDir )
: await getCommandOutput ( yarn2GetCacheFolderCommand , projectDir ) ;
if ( ! stdOut ) {
throw new Error (
` Could not get yarn cache folder path for ${ projectDir } `
) ;
}
return stdOut ;
}
2021-06-16 09:52:44 +03:00
}
} ;
2023-04-10 16:52:10 +02:00
export const getCommandOutput = async (
toolCommand : string ,
2023-05-11 09:40:44 +02:00
cwd? : string
) : Promise < string > = > {
2021-12-27 12:34:06 +03:00
let { stdout , stderr , exitCode } = await exec . getExecOutput (
toolCommand ,
undefined ,
2023-05-11 09:40:44 +02:00
{ ignoreReturnCode : true , . . . ( cwd && { cwd } ) }
2021-12-27 12:34:06 +03:00
) ;
if ( exitCode ) {
stderr = ! stderr . trim ( )
? ` The ' ${ toolCommand } ' command failed with exit code: ${ exitCode } `
: stderr ;
2021-06-16 09:52:44 +03:00
throw new Error ( stderr ) ;
}
2021-06-30 16:44:51 +01:00
return stdout . trim ( ) ;
2021-06-16 09:52:44 +03:00
} ;
2023-05-11 09:40:44 +02:00
export const getCommandOutputGuarded = async (
toolCommand : string ,
error : string ,
cwd? : string
) : Promise < string > = > {
const stdOut = getCommandOutput ( toolCommand , cwd ) ;
2021-06-16 09:52:44 +03:00
if ( ! stdOut ) {
2023-05-11 09:40:44 +02:00
throw new Error ( error ) ;
2021-06-16 09:52:44 +03:00
}
return stdOut ;
} ;
export const getPackageManagerInfo = async ( packageManager : string ) = > {
if ( packageManager === 'npm' ) {
return supportedPackageManagers . npm ;
2021-06-30 16:44:51 +01:00
} else if ( packageManager === 'pnpm' ) {
return supportedPackageManagers . pnpm ;
2021-06-16 09:52:44 +03:00
} else if ( packageManager === 'yarn' ) {
2023-05-11 09:40:44 +02:00
return supportedPackageManagers . yarn ;
2021-06-16 09:52:44 +03:00
} else {
return null ;
}
} ;
2023-05-19 19:57:43 +02:00
export const expandedPatternsMemoized : Record < string , string [ ] > = { } ;
/ * *
* Wrapper around ` glob.create(pattern).glob() ` with the memoization
* @param pattern is expected to be a globed path
* @return list of files or directories expanded from glob
* /
2023-05-11 09:40:44 +02:00
const globPatternToArray = async ( pattern : string ) : Promise < string [ ] > = > {
2023-05-19 19:57:43 +02:00
const memoized = expandedPatternsMemoized [ pattern ] ;
if ( memoized ) return Promise . resolve ( memoized ) ;
2023-05-11 09:40:44 +02:00
const globber = await glob . create ( pattern ) ;
2023-05-19 19:57:43 +02:00
const expanded = await globber . glob ( ) ;
expandedPatternsMemoized [ pattern ] = expanded ;
return expanded ;
2023-05-11 09:40:44 +02:00
} ;
2023-05-19 19:57:43 +02:00
/ * *
* Expands ( converts ) the string input ` cache-dependency-path ` to list of files ' paths
* First it breaks the input by new lines and then expand glob patterns if any
* @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
* @return list of files on which the cache depends
* /
2023-05-11 09:40:44 +02:00
export const expandCacheDependencyPath = async (
cacheDependencyPath : string
) : Promise < string [ ] > = > {
const multilinePaths = cacheDependencyPath
. split ( /\r?\n/ )
. map ( path = > path . trim ( ) )
. filter ( path = > Boolean ( path ) ) ;
const expandedPathsPromises : Promise < string [ ] > [ ] = multilinePaths . map ( path = >
path . includes ( '*' ) ? globPatternToArray ( path ) : Promise . resolve ( [ path ] )
) ;
const expandedPaths : string [ ] [ ] = await Promise . all ( expandedPathsPromises ) ;
return expandedPaths . length === 0 ? [ '' ] : expandedPaths . flat ( ) ;
} ;
2023-05-19 19:57:43 +02:00
/ * *
* Converts dependency file to the directory it resides in and ensures the directory exists
* @param cacheDependencyPath - file name
* @return either directory containing file or null
* /
const cacheDependencyPathToProjectDirectory = (
2023-05-11 09:40:44 +02:00
cacheDependencyPath : string
2023-05-19 19:57:43 +02:00
) : string | null = > {
const projectDirectory = path . dirname ( cacheDependencyPath ) ;
if (
fs . existsSync ( projectDirectory ) &&
fs . lstatSync ( projectDirectory ) . isDirectory ( )
) {
core . debug (
` Project directory " ${ projectDirectory } " derived from cache-dependency-path: " ${ cacheDependencyPath } " `
) ;
return projectDirectory ;
} else {
core . debug (
` No project directory found for cache-dependency-path: " ${ cacheDependencyPath } ", will be skipped `
) ;
return null ;
}
} ;
2023-05-11 09:40:44 +02:00
2023-05-19 19:57:43 +02:00
/ * *
* Expands ( converts ) the string input ` cache-dependency-path ` to list of directories that
* may be project roots
* @param cacheDependencyPath
* @return list of directories and possible
* /
const cacheDependencyPathToProjectsDirectories = async (
cacheDependencyPath : string
) : Promise < string [ ] > = > {
const cacheDependenciesPaths = await expandCacheDependencyPath (
cacheDependencyPath
) ;
const existingDirectories : string [ ] = cacheDependenciesPaths
. map ( cacheDependencyPath = >
cacheDependencyPathToProjectDirectory ( cacheDependencyPath )
)
. filter ( path = > path !== null ) as string [ ] ;
if ( existingDirectories . length === 0 )
throw Error (
'No existing directories found containing `cache-dependency-path`="${cacheDependencyPath}"'
) ;
// uniq
return existingDirectories . filter (
( cachePath , i , result ) = >
cachePath != null && result . indexOf ( cachePath ) === i
2021-07-15 12:43:19 +01:00
) ;
2023-05-19 19:57:43 +02:00
} ;
2021-06-16 09:52:44 +03:00
2023-05-19 19:57:43 +02:00
const projectDirectoryToCacheFolderPath = async (
packageManagerInfo : PackageManagerInfo ,
projectDirectory : string
) : Promise < string > = > {
const cacheFolderPath = await packageManagerInfo . getCacheFolderPath (
projectDirectory
) ;
core . debug (
` ${ packageManagerInfo . name } 's cache folder " ${ cacheFolderPath } " configured for the directory " ${ projectDirectory } " `
) ;
2023-05-11 09:40:44 +02:00
return cacheFolderPath ;
} ;
2023-05-19 19:57:43 +02:00
const projectDirectoriesToCacheFoldersPaths = async (
2023-05-11 09:40:44 +02:00
packageManagerInfo : PackageManagerInfo ,
2023-05-19 19:57:43 +02:00
projectDirectories : string [ ]
2023-05-11 09:40:44 +02:00
) : Promise < string [ ] > = > {
const cacheFoldersPaths = await Promise . all (
2023-05-19 19:57:43 +02:00
projectDirectories . map ( projectDirectory = >
projectDirectoryToCacheFolderPath ( packageManagerInfo , projectDirectory )
2023-05-11 09:40:44 +02:00
)
) ;
return cacheFoldersPaths . filter (
( cachePath , i , result ) = > result . indexOf ( cachePath ) === i
) ;
} ;
const cacheDependencyPathToCacheFoldersPaths = async (
packageManagerInfo : PackageManagerInfo ,
cacheDependencyPath : string
) : Promise < string [ ] > = > {
2023-05-19 19:57:43 +02:00
const projectDirectories = await cacheDependencyPathToProjectsDirectories (
2023-05-11 09:40:44 +02:00
cacheDependencyPath
) ;
2023-05-19 19:57:43 +02:00
return projectDirectoriesToCacheFoldersPaths (
2023-05-11 09:40:44 +02:00
packageManagerInfo ,
2023-05-19 19:57:43 +02:00
projectDirectories
2023-05-11 09:40:44 +02:00
) ;
} ;
2021-06-16 09:52:44 +03:00
2023-05-11 09:40:44 +02:00
const cacheFoldersPathsForRoot = async (
packageManagerInfo : PackageManagerInfo
) : Promise < string [ ] > = > {
const cacheFolderPath = await packageManagerInfo . getCacheFolderPath ( ) ;
2023-05-19 19:57:43 +02:00
core . debug (
` ${ packageManagerInfo . name } 's cache folder " ${ cacheFolderPath } " configured for the root directory `
) ;
2023-05-11 09:40:44 +02:00
return [ cacheFolderPath ] ;
2021-06-16 09:52:44 +03:00
} ;
2022-03-31 21:10:37 +02:00
2023-05-11 09:40:44 +02:00
export const getCacheDirectoriesPaths = async (
packageManagerInfo : PackageManagerInfo ,
cacheDependencyPath : string
) : Promise < string [ ] > = >
// TODO: multiple directories limited to yarn so far
packageManagerInfo === supportedPackageManagers . yarn
? cacheDependencyPathToCacheFoldersPaths (
packageManagerInfo ,
cacheDependencyPath
)
: cacheFoldersPathsForRoot ( packageManagerInfo ) ;
2022-03-31 21:10:37 +02:00
export function isGhes ( ) : boolean {
const ghUrl = new URL (
process . env [ 'GITHUB_SERVER_URL' ] || 'https://github.com'
) ;
return ghUrl . hostname . toUpperCase ( ) !== 'GITHUB.COM' ;
}
export function isCacheFeatureAvailable ( ) : boolean {
2022-12-09 11:41:54 +01:00
if ( cache . isFeatureAvailable ( ) ) return true ;
2022-03-31 21:10:37 +02:00
2022-12-09 12:05:59 +01:00
if ( isGhes ( ) ) {
core . warning (
2022-12-09 11:41:54 +01:00
'Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.'
) ;
2022-12-09 12:05:59 +01:00
return false ;
}
2022-12-09 11:41:54 +01:00
core . warning (
'The runner was not able to contact the cache service. Caching will be skipped'
) ;
2022-03-31 21:10:37 +02:00
2022-12-09 11:41:54 +01:00
return false ;
2022-03-31 21:10:37 +02:00
}