import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as cache from '@actions/cache'; import * as glob from '@actions/glob'; import path from 'path'; import fs from 'fs'; export interface PackageManagerInfo { name: string; lockFilePatterns: Array; getCacheFolderPath: (projectDir?: string) => Promise; } interface SupportedPackageManagers { npm: PackageManagerInfo; pnpm: PackageManagerInfo; yarn: PackageManagerInfo; } // 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'; export const supportedPackageManagers: SupportedPackageManagers = { npm: { name: 'npm', lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'], getCacheFolderPath: () => getCommandOutputGuarded( npmGetCacheFolderCommand, 'Could not get npm cache folder path' ) }, pnpm: { name: 'pnpm', lockFilePatterns: ['pnpm-lock.yaml'], getCacheFolderPath: () => getCommandOutputGuarded( pnpmGetCacheFolderCommand, 'Could not get pnpm cache folder path' ) }, yarn: { name: 'yarn', lockFilePatterns: ['yarn.lock'], getCacheFolderPath: async projectDir => { const yarnVersion = await getCommandOutputGuarded( `yarn --version`, 'Could not retrieve version of yarn', projectDir ); core.debug(`Consumed yarn version is ${yarnVersion}`); 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; } } }; export const getCommandOutput = async ( toolCommand: string, cwd?: string ): Promise => { let {stdout, stderr, exitCode} = await exec.getExecOutput( toolCommand, undefined, {ignoreReturnCode: true, ...(cwd && {cwd})} ); if (exitCode) { stderr = !stderr.trim() ? `The '${toolCommand}' command failed with exit code: ${exitCode}` : stderr; throw new Error(stderr); } return stdout.trim(); }; export const getCommandOutputGuarded = async ( toolCommand: string, error: string, cwd?: string ): Promise => { const stdOut = getCommandOutput(toolCommand, cwd); if (!stdOut) { throw new Error(error); } return stdOut; }; export const getPackageManagerInfo = async (packageManager: string) => { if (packageManager === 'npm') { return supportedPackageManagers.npm; } else if (packageManager === 'pnpm') { return supportedPackageManagers.pnpm; } else if (packageManager === 'yarn') { return supportedPackageManagers.yarn; } else { return null; } }; const globPatternToArray = async (pattern: string): Promise => { const globber = await glob.create(pattern); return globber.glob(); }; export const expandCacheDependencyPath = async ( cacheDependencyPath: string ): Promise => { const multilinePaths = cacheDependencyPath .split(/\r?\n/) .map(path => path.trim()) .filter(path => Boolean(path)); const expandedPathsPromises: Promise[] = multilinePaths.map(path => path.includes('*') ? globPatternToArray(path) : Promise.resolve([path]) ); const expandedPaths: string[][] = await Promise.all(expandedPathsPromises); return expandedPaths.length === 0 ? [''] : expandedPaths.flat(); }; const cacheDependencyPathToCacheFolderPath = async ( packageManagerInfo: PackageManagerInfo, cacheDependencyPath: string ): Promise => { const cacheDependencyPathDirectory = path.dirname(cacheDependencyPath); const cacheFolderPath = fs.existsSync(cacheDependencyPathDirectory) && fs.lstatSync(cacheDependencyPathDirectory).isDirectory() ? await packageManagerInfo.getCacheFolderPath( cacheDependencyPathDirectory ) : await packageManagerInfo.getCacheFolderPath(); core.debug( `${packageManagerInfo.name} path is ${cacheFolderPath} (derived from cache-dependency-path: "${cacheDependencyPath}")` ); return cacheFolderPath; }; const cacheDependenciesPathsToCacheFoldersPaths = async ( packageManagerInfo: PackageManagerInfo, cacheDependenciesPaths: string[] ): Promise => { const cacheFoldersPaths = await Promise.all( cacheDependenciesPaths.map(cacheDependencyPath => cacheDependencyPathToCacheFolderPath( packageManagerInfo, cacheDependencyPath ) ) ); return cacheFoldersPaths.filter( (cachePath, i, result) => result.indexOf(cachePath) === i ); }; const cacheDependencyPathToCacheFoldersPaths = async ( packageManagerInfo: PackageManagerInfo, cacheDependencyPath: string ): Promise => { const cacheDependenciesPaths = await expandCacheDependencyPath( cacheDependencyPath ); return cacheDependenciesPathsToCacheFoldersPaths( packageManagerInfo, cacheDependenciesPaths ); }; const cacheFoldersPathsForRoot = async ( packageManagerInfo: PackageManagerInfo ): Promise => { const cacheFolderPath = await packageManagerInfo.getCacheFolderPath(); core.debug(`${packageManagerInfo.name} path is ${cacheFolderPath}`); return [cacheFolderPath]; }; export const getCacheDirectoriesPaths = async ( packageManagerInfo: PackageManagerInfo, cacheDependencyPath: string ): Promise => // TODO: multiple directories limited to yarn so far packageManagerInfo === supportedPackageManagers.yarn ? cacheDependencyPathToCacheFoldersPaths( packageManagerInfo, cacheDependencyPath ) : cacheFoldersPathsForRoot(packageManagerInfo); 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 { if (cache.isFeatureAvailable()) return true; if (isGhes()) { core.warning( '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.' ); return false; } core.warning( 'The runner was not able to contact the cache service. Caching will be skipped' ); return false; }