Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions __tests__/cache-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,79 @@ describe('cache-utils', () => {
});
});

describe('npm getCacheFolderPath', () => {
const devEnginesError = [
'npm error code EBADDEVENGINES',
'npm error EBADDEVENGINES Invalid name "pnpm" does not match "npm" for "packageManager"'
].join('\n');

function mockExecResultOnce(result: {
stdout?: string;
stderr?: string;
exitCode: number;
}) {
getExecOutputSpy.mockImplementationOnce(async () => ({
stdout: result.stdout ?? '',
stderr: result.stderr ?? '',
exitCode: result.exitCode
}));
}

it('returns the cache path without --force when npm config succeeds', async () => {
mockExecResultOnce({stdout: '/home/runner/.npm', exitCode: 0});

await expect(
supportedPackageManagers.npm.getCacheFolderPath()
).resolves.toBe('/home/runner/.npm');
expect(getExecOutputSpy).toHaveBeenCalledTimes(1);
expect(getExecOutputSpy).toHaveBeenCalledWith(
'npm config get cache',
undefined,
expect.anything()
);
});

it('retries with --force when npm rejects devEngines', async () => {
mockExecResultOnce({stderr: devEnginesError, exitCode: 1});
mockExecResultOnce({stdout: '/home/runner/.npm', exitCode: 0});

await expect(
supportedPackageManagers.npm.getCacheFolderPath()
).resolves.toBe('/home/runner/.npm');
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
expect(getExecOutputSpy).toHaveBeenNthCalledWith(
2,
'npm config get cache --force',
undefined,
expect.anything()
);
expect(info).toHaveBeenCalledWith(
expect.stringContaining(
'retrying the npm cache path lookup with --force'
)
);
});

it('does not retry on other npm errors', async () => {
mockExecResultOnce({stderr: 'npm error code ENOENT', exitCode: 1});

await expect(
supportedPackageManagers.npm.getCacheFolderPath()
).rejects.toThrow('npm error code ENOENT');
expect(getExecOutputSpy).toHaveBeenCalledTimes(1);
});

it('surfaces the error when the --force retry also fails', async () => {
mockExecResultOnce({stderr: devEnginesError, exitCode: 1});
mockExecResultOnce({stderr: 'npm error code EFORCEFAILED', exitCode: 1});

await expect(
supportedPackageManagers.npm.getCacheFolderPath()
).rejects.toThrow('npm error code EFORCEFAILED');
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
});
});

it('isCacheFeatureAvailable for GHES is false', () => {
isFeatureAvailable.mockImplementation(() => false);
process.env['GITHUB_SERVER_URL'] = 'https://www.test.com';
Expand Down
19 changes: 18 additions & 1 deletion dist/cache-save/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92228,7 +92228,7 @@ const 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')
getCacheFolderPath: () => getNpmCacheFolderPath()
},
pnpm: {
name: 'pnpm',
Expand Down Expand Up @@ -92268,6 +92268,23 @@ const getCommandOutputNotEmpty = async (toolCommand, error, cwd) => {
}
return stdOut;
};
const getNpmCacheFolderPath = async () => {
const error = 'Could not get npm cache folder path';
try {
return await getCommandOutputNotEmpty('npm config get cache', error);
}
catch (err) {
// npm checks devEngines.packageManager from the package.json in the
// workspace even for `npm config get`, so the cache lookup fails before
// the workflow had a chance to install the required package manager.
// `--force` turns that check into a warning and still honours .npmrc.
if (!err.message.includes('EBADDEVENGINES')) {
throw err;
}
info("The installed npm doesn't satisfy devEngines.packageManager in package.json, retrying the npm cache path lookup with --force");
return getCommandOutputNotEmpty('npm config get cache --force', error);
}
};
const getPackageManagerInfo = async (packageManager) => {
if (packageManager === 'npm') {
return supportedPackageManagers.npm;
Expand Down
19 changes: 18 additions & 1 deletion dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97622,7 +97622,7 @@ const 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')
getCacheFolderPath: () => getNpmCacheFolderPath()
},
pnpm: {
name: 'pnpm',
Expand Down Expand Up @@ -97662,6 +97662,23 @@ const getCommandOutputNotEmpty = async (toolCommand, error, cwd) => {
}
return stdOut;
};
const getNpmCacheFolderPath = async () => {
const error = 'Could not get npm cache folder path';
try {
return await getCommandOutputNotEmpty('npm config get cache', error);
}
catch (err) {
// npm checks devEngines.packageManager from the package.json in the
// workspace even for `npm config get`, so the cache lookup fails before
// the workflow had a chance to install the required package manager.
// `--force` turns that check into a warning and still honours .npmrc.
if (!err.message.includes('EBADDEVENGINES')) {
throw err;
}
core_info("The installed npm doesn't satisfy devEngines.packageManager in package.json, retrying the npm cache path lookup with --force");
return getCommandOutputNotEmpty('npm config get cache --force', error);
}
};
const getPackageManagerInfo = async (packageManager) => {
if (packageManager === 'npm') {
return supportedPackageManagers.npm;
Expand Down
25 changes: 20 additions & 5 deletions src/cache-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ 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'
)
getCacheFolderPath: () => getNpmCacheFolderPath()
},
pnpm: {
name: 'pnpm',
Expand Down Expand Up @@ -98,6 +94,25 @@ export const getCommandOutputNotEmpty = async (
return stdOut;
};

const getNpmCacheFolderPath = async (): Promise<string> => {
const error = 'Could not get npm cache folder path';
try {
return await getCommandOutputNotEmpty('npm config get cache', error);
} catch (err) {
// npm checks devEngines.packageManager from the package.json in the
// workspace even for `npm config get`, so the cache lookup fails before
// the workflow had a chance to install the required package manager.
// `--force` turns that check into a warning and still honours .npmrc.
if (!(err as Error).message.includes('EBADDEVENGINES')) {
throw err;
}
core.info(
"The installed npm doesn't satisfy devEngines.packageManager in package.json, retrying the npm cache path lookup with --force"
);
return getCommandOutputNotEmpty('npm config get cache --force', error);
}
};

export const getPackageManagerInfo = async (packageManager: string) => {
if (packageManager === 'npm') {
return supportedPackageManagers.npm;
Expand Down