317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
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';
|
|
import {unique} from './util';
|
|
|
|
export interface PackageManagerInfo {
|
|
name: string;
|
|
lockFilePatterns: Array<string>;
|
|
getCacheFolderPath: (projectDir?: string) => Promise<string>;
|
|
}
|
|
|
|
interface SupportedPackageManagers {
|
|
npm: PackageManagerInfo;
|
|
pnpm: PackageManagerInfo;
|
|
yarn: PackageManagerInfo;
|
|
}
|
|
export const supportedPackageManagers: SupportedPackageManagers = {
|
|
npm: {
|
|
name: 'npm',
|
|
lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'],
|
|
getCacheFolderPath: () =>
|
|
getCommandOutputNotEmpty(
|
|
'npm config get cache',
|
|
'Could not get npm cache folder path'
|
|
)
|
|
},
|
|
pnpm: {
|
|
name: 'pnpm',
|
|
lockFilePatterns: ['pnpm-lock.yaml'],
|
|
getCacheFolderPath: () =>
|
|
getCommandOutputNotEmpty(
|
|
'pnpm store path --silent',
|
|
'Could not get pnpm cache folder path'
|
|
)
|
|
},
|
|
yarn: {
|
|
name: 'yarn',
|
|
lockFilePatterns: ['yarn.lock'],
|
|
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;
|
|
}
|
|
}
|
|
};
|
|
|
|
export const getCommandOutput = async (
|
|
toolCommand: string,
|
|
cwd?: string
|
|
): Promise<string> => {
|
|
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 getCommandOutputNotEmpty = async (
|
|
toolCommand: string,
|
|
error: string,
|
|
cwd?: string
|
|
): Promise<string> => {
|
|
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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
/**
|
|
* 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[]> => {
|
|
if (projectDirectoriesMemoized !== null) {
|
|
return projectDirectoriesMemoized;
|
|
}
|
|
|
|
const globber = await glob.create(cacheDependencyPath);
|
|
const cacheDependenciesPaths = await globber.glob();
|
|
|
|
const existingDirectories: string[] = cacheDependenciesPaths
|
|
.map(path.dirname)
|
|
.filter(unique())
|
|
.map(dirName => fs.realpathSync(dirName))
|
|
.filter(directory => fs.lstatSync(directory).isDirectory());
|
|
|
|
if (!existingDirectories.length)
|
|
core.warning(
|
|
`No existing directories found containing cache-dependency-path="${cacheDependencyPath}"`
|
|
);
|
|
|
|
projectDirectoriesMemoized = existingDirectories;
|
|
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 (
|
|
packageManagerInfo: PackageManagerInfo,
|
|
cacheDependencyPath: string
|
|
): Promise<string[]> => {
|
|
const projectDirectories = await getProjectDirectoriesFromCacheDependencyPath(
|
|
cacheDependencyPath
|
|
);
|
|
const cacheFoldersPaths = await Promise.all(
|
|
projectDirectories.map(async projectDirectory => {
|
|
const cacheFolderPath = await packageManagerInfo.getCacheFolderPath(
|
|
projectDirectory
|
|
);
|
|
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());
|
|
};
|
|
|
|
/**
|
|
* 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];
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
};
|
|
|
|
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;
|
|
}
|