2021-06-16 02:52:44 -04:00
import * as core from '@actions/core' ;
import * as exec from '@actions/exec' ;
2022-03-31 15:10:37 -04:00
import * as cache from '@actions/cache' ;
2023-06-21 11:52:17 -04:00
import * as glob from '@actions/glob' ;
import path from 'path' ;
import fs from 'fs' ;
import { unique } from './util' ;
2021-06-16 02:52:44 -04:00
export interface PackageManagerInfo {
2023-06-21 11:52:17 -04:00
name : string ;
2021-06-16 02:52:44 -04:00
lockFilePatterns : Array < string > ;
2023-06-21 11:52:17 -04:00
getCacheFolderPath : ( projectDir? : string ) = > Promise < string > ;
2021-06-16 02:52:44 -04:00
}
2023-06-21 11:52:17 -04:00
interface SupportedPackageManagers {
npm : PackageManagerInfo ;
pnpm : PackageManagerInfo ;
yarn : PackageManagerInfo ;
}
2021-06-16 02:52:44 -04:00
export const supportedPackageManagers : SupportedPackageManagers = {
npm : {
2023-06-21 11:52:17 -04:00
name : 'npm' ,
2022-07-04 17:29:56 -04:00
lockFilePatterns : [ 'package-lock.json' , 'npm-shrinkwrap.json' , 'yarn.lock' ] ,
2023-06-21 11:52:17 -04:00
getCacheFolderPath : ( ) = >
getCommandOutputNotEmpty (
'npm config get cache' ,
'Could not get npm cache folder path'
)
2021-06-16 02:52:44 -04:00
} ,
2021-06-30 11:44:51 -04:00
pnpm : {
2023-06-21 11:52:17 -04:00
name : 'pnpm' ,
2021-06-30 11:44:51 -04:00
lockFilePatterns : [ 'pnpm-lock.yaml' ] ,
2023-06-21 11:52:17 -04:00
getCacheFolderPath : ( ) = >
getCommandOutputNotEmpty (
'pnpm store path --silent' ,
'Could not get pnpm cache folder path'
)
2021-06-30 11:44:51 -04:00
} ,
2023-06-21 11:52:17 -04:00
yarn : {
name : 'yarn' ,
2021-06-16 02:52:44 -04:00
lockFilePatterns : [ 'yarn.lock' ] ,
2023-06-21 11:52:17 -04:00
getCacheFolderPath : async projectDir = > {
const yarnVersion = await getCommandOutputNotEmpty (
` yarn --version ` ,
'Could not retrieve version of yarn' ,
projectDir
) ;
core . debug (
` Consumed yarn version is ${ yarnVersion } (working dir: " ${
projectDir || ''
} " ) `
) ;
const stdOut = yarnVersion . startsWith ( '1.' )
? await getCommandOutput ( 'yarn cache dir' , projectDir )
: await getCommandOutput ( 'yarn config get cacheFolder' , projectDir ) ;
if ( ! stdOut ) {
throw new Error (
` Could not get yarn cache folder path for ${ projectDir } `
) ;
}
return stdOut ;
}
2021-06-16 02:52:44 -04:00
}
} ;
2023-06-21 11:52:17 -04:00
export const getCommandOutput = async (
toolCommand : string ,
cwd? : string
) : Promise < string > = > {
2021-12-27 04:34:06 -05:00
let { stdout , stderr , exitCode } = await exec . getExecOutput (
toolCommand ,
undefined ,
2023-06-21 11:52:17 -04:00
{ ignoreReturnCode : true , . . . ( cwd && { cwd } ) }
2021-12-27 04:34:06 -05:00
) ;
if ( exitCode ) {
stderr = ! stderr . trim ( )
? ` The ' ${ toolCommand } ' command failed with exit code: ${ exitCode } `
: stderr ;
2021-06-16 02:52:44 -04:00
throw new Error ( stderr ) ;
}
2021-06-30 11:44:51 -04:00
return stdout . trim ( ) ;
2021-06-16 02:52:44 -04:00
} ;
2023-06-21 11:52:17 -04:00
export const getCommandOutputNotEmpty = async (
toolCommand : string ,
error : string ,
cwd? : string
) : Promise < string > = > {
const stdOut = getCommandOutput ( toolCommand , cwd ) ;
2021-06-16 02:52:44 -04:00
if ( ! stdOut ) {
2023-06-21 11:52:17 -04:00
throw new Error ( error ) ;
2021-06-16 02:52:44 -04:00
}
return stdOut ;
} ;
export const getPackageManagerInfo = async ( packageManager : string ) = > {
if ( packageManager === 'npm' ) {
return supportedPackageManagers . npm ;
2021-06-30 11:44:51 -04:00
} else if ( packageManager === 'pnpm' ) {
return supportedPackageManagers . pnpm ;
2021-06-16 02:52:44 -04:00
} else if ( packageManager === 'yarn' ) {
2023-06-21 11:52:17 -04:00
return supportedPackageManagers . yarn ;
2021-06-16 02:52:44 -04:00
} else {
return null ;
}
} ;
2023-06-27 07:07:43 -04:00
/ * *
* getProjectDirectoriesFromCacheDependencyPath is called twice during ` restoreCache `
* - first through ` getCacheDirectories `
* - second from ` repoHasYarn3ManagedCache `
*
* it contains expensive IO operation and thus should be memoized
* /
let projectDirectoriesMemoized : string [ ] | null = null ;
/ * *
* unit test must reset memoized variables
* /
export const resetProjectDirectoriesMemoized = ( ) = >
( projectDirectoriesMemoized = null ) ;
2023-06-21 11:52:17 -04:00
/ * *
* Expands ( converts ) the string input ` cache-dependency-path ` to list of directories that
* may be project roots
* @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
* expected to be the result of ` core.getInput('cache-dependency-path') `
* @return list of directories and possible
* /
const getProjectDirectoriesFromCacheDependencyPath = async (
cacheDependencyPath : string
) : Promise < string [ ] > = > {
2023-06-27 07:07:43 -04:00
if ( projectDirectoriesMemoized !== null ) {
return projectDirectoriesMemoized ;
}
2023-06-21 11:52:17 -04:00
const globber = await glob . create ( cacheDependencyPath ) ;
const cacheDependenciesPaths = await globber . glob ( ) ;
const existingDirectories : string [ ] = cacheDependenciesPaths
. map ( path . dirname )
. filter ( unique ( ) )
. filter ( directory = > fs . lstatSync ( directory ) . isDirectory ( ) ) ;
if ( ! existingDirectories . length )
core . warning (
` No existing directories found containing cache-dependency-path=" ${ cacheDependencyPath } " `
) ;
2023-06-27 07:07:43 -04:00
projectDirectoriesMemoized = existingDirectories ;
2023-06-21 11:52:17 -04:00
return existingDirectories ;
} ;
/ * *
* Finds the cache directories configured for the repo if cache - dependency - path is not empty
* @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM
* @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
* expected to be the result of ` core.getInput('cache-dependency-path') `
* @return list of files on which the cache depends
* /
const getCacheDirectoriesFromCacheDependencyPath = async (
2021-06-16 02:52:44 -04:00
packageManagerInfo : PackageManagerInfo ,
2023-06-21 11:52:17 -04:00
cacheDependencyPath : string
) : Promise < string [ ] > = > {
const projectDirectories = await getProjectDirectoriesFromCacheDependencyPath (
cacheDependencyPath
2021-07-15 07:43:19 -04:00
) ;
2023-06-21 11:52:17 -04:00
const cacheFoldersPaths = await Promise . all (
projectDirectories . map ( async projectDirectory = > {
2023-06-27 07:07:43 -04:00
const cacheFolderPath = await packageManagerInfo . getCacheFolderPath (
projectDirectory
) ;
2023-06-21 11:52:17 -04:00
core . debug (
` ${ packageManagerInfo . name } 's cache folder " ${ cacheFolderPath } " configured for the directory " ${ projectDirectory } " `
) ;
return cacheFolderPath ;
} )
) ;
// uniq in order to do not cache the same directories twice
return cacheFoldersPaths . filter ( unique ( ) ) ;
} ;
2021-06-16 02:52:44 -04:00
2023-06-21 11:52:17 -04:00
/ * *
* Finds the cache directories configured for the repo ignoring cache - dependency - path
* @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM
* @return list of files on which the cache depends
* /
const getCacheDirectoriesForRootProject = async (
packageManagerInfo : PackageManagerInfo
) : Promise < string [ ] > = > {
const cacheFolderPath = await packageManagerInfo . getCacheFolderPath ( ) ;
core . debug (
` ${ packageManagerInfo . name } 's cache folder " ${ cacheFolderPath } " configured for the root directory `
) ;
return [ cacheFolderPath ] ;
} ;
2021-06-16 02:52:44 -04:00
2023-06-21 11:52:17 -04:00
/ * *
* A function to find the cache directories configured for the repo
* currently it handles only the case of PM = yarn && cacheDependencyPath is not empty
* @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM
* @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
* expected to be the result of ` core.getInput('cache-dependency-path') `
* @return list of files on which the cache depends
* /
export const getCacheDirectories = async (
packageManagerInfo : PackageManagerInfo ,
cacheDependencyPath : string
) : Promise < string [ ] > = > {
// For yarn, if cacheDependencyPath is set, ask information about cache folders in each project
// folder satisfied by cacheDependencyPath https://github.com/actions/setup-node/issues/488
if ( packageManagerInfo . name === 'yarn' && cacheDependencyPath ) {
return getCacheDirectoriesFromCacheDependencyPath (
packageManagerInfo ,
cacheDependencyPath
) ;
}
return getCacheDirectoriesForRootProject ( packageManagerInfo ) ;
2021-06-16 02:52:44 -04:00
} ;
2022-03-31 15:10:37 -04:00
2023-06-27 07:07:43 -04:00
/ * *
* A function to check if the directory is a yarn project configured to manage
* obsolete dependencies in the local cache
* @param directory - a path to the folder
* @return - true if the directory ' s project is yarn managed
* - if there ' s . yarn / cache folder do not mess with the dependencies kept in the repo , return false
* - global cache is not managed by yarn @see https : //yarnpkg.com/features/offline-cache, return false
* - if local cache is not explicitly enabled ( not yarn3 ) , return false
* - return true otherwise
* /
const projectHasYarnBerryManagedDependencies = async (
directory : string
) : Promise < boolean > = > {
const workDir = directory || process . env . GITHUB_WORKSPACE || '.' ;
core . debug ( ` check if " ${ workDir } " has locally managed yarn3 dependencies ` ) ;
// if .yarn/cache directory exists the cache is managed by version control system
const yarnCacheFile = path . join ( workDir , '.yarn' , 'cache' ) ;
if (
fs . existsSync ( yarnCacheFile ) &&
fs . lstatSync ( yarnCacheFile ) . isDirectory ( )
) {
core . debug (
` " ${ workDir } " has .yarn/cache - dependencies are kept in the repository `
) ;
return Promise . resolve ( false ) ;
}
// NOTE: yarn1 returns 'undefined' with return code = 0
const enableGlobalCache = await getCommandOutput (
'yarn config get enableGlobalCache' ,
workDir
) ;
// only local cache is not managed by yarn
const managed = enableGlobalCache . includes ( 'false' ) ;
if ( managed ) {
core . debug ( ` " ${ workDir } " dependencies are managed by yarn 3 locally ` ) ;
return true ;
} else {
core . debug ( ` " ${ workDir } " dependencies are not managed by yarn 3 locally ` ) ;
return false ;
}
} ;
/ * *
* A function to report the repo contains Yarn managed projects
* @param packageManagerInfo - used to make sure current package manager is yarn
* @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
* expected to be the result of ` core.getInput('cache-dependency-path') `
* @return - true if all project directories configured to be Yarn managed
* /
export const repoHasYarnBerryManagedDependencies = async (
packageManagerInfo : PackageManagerInfo ,
cacheDependencyPath : string
) : Promise < boolean > = > {
if ( packageManagerInfo . name !== 'yarn' ) return false ;
const yarnDirs = cacheDependencyPath
? await getProjectDirectoriesFromCacheDependencyPath ( cacheDependencyPath )
: [ '' ] ;
const isManagedList = await Promise . all (
yarnDirs . map ( projectHasYarnBerryManagedDependencies )
) ;
return isManagedList . every ( Boolean ) ;
} ;
2022-03-31 15:10:37 -04: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 05:41:54 -05:00
if ( cache . isFeatureAvailable ( ) ) return true ;
2022-03-31 15:10:37 -04:00
2022-12-09 06:05:59 -05:00
if ( isGhes ( ) ) {
core . warning (
2022-12-09 05:41:54 -05: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 06:05:59 -05:00
return false ;
}
2022-12-09 05:41:54 -05:00
core . warning (
'The runner was not able to contact the cache service. Caching will be skipped'
) ;
2022-03-31 15:10:37 -04:00
2022-12-09 05:41:54 -05:00
return false ;
2022-03-31 15:10:37 -04:00
}