Merge branch 'actions:main' into main

This commit is contained in:
Devlynnx 2023-06-23 00:52:43 -07:00 committed by GitHub
commit 977d4381ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 3455 additions and 1499 deletions

View File

@ -134,3 +134,30 @@ jobs:
- name: Verify node and yarn - name: Verify node and yarn
run: __tests__/verify-node.sh "${{ matrix.node-version }}" run: __tests__/verify-node.sh "${{ matrix.node-version }}"
shell: bash shell: bash
yarn-subprojects:
name: Test yarn subprojects
strategy:
matrix:
node-version: [12, 14, 16]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: prepare sub-projects
run: __tests__/prepare-subprojects.sh
# expect
# - no errors
# - log
# ##[debug]Cache Paths:
# ##[debug]["sub2/.yarn/cache","sub3/.yarn/cache","../../../.cache/yarn/v6"]
- name: Setup Node
uses: ./
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
cache-dependency-path: |
**/*.lock
yarn.lock

View File

@ -1,9 +1,10 @@
import os from 'os'; import os from 'os';
import * as fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as io from '@actions/io'; import * as io from '@actions/io';
import * as auth from '../src/authutil'; import * as auth from '../src/authutil';
import * as cacheUtils from '../src/cache-utils';
let rcFile: string; let rcFile: string;

View File

@ -32,13 +32,13 @@ describe('cache-restore', () => {
function findCacheFolder(command: string) { function findCacheFolder(command: string) {
switch (command) { switch (command) {
case utils.supportedPackageManagers.npm.getCacheFolderCommand: case 'npm config get cache':
return npmCachePath; return npmCachePath;
case utils.supportedPackageManagers.pnpm.getCacheFolderCommand: case 'pnpm store path --silent':
return pnpmCachePath; return pnpmCachePath;
case utils.supportedPackageManagers.yarn1.getCacheFolderCommand: case 'yarn cache dir':
return yarn1CachePath; return yarn1CachePath;
case utils.supportedPackageManagers.yarn2.getCacheFolderCommand: case 'yarn config get cacheFolder':
return yarn2CachePath; return yarn2CachePath;
default: default:
return 'packge/not/found'; return 'packge/not/found';
@ -108,7 +108,7 @@ describe('cache-restore', () => {
it.each([['npm7'], ['npm6'], ['pnpm6'], ['yarn1'], ['yarn2'], ['random']])( it.each([['npm7'], ['npm6'], ['pnpm6'], ['yarn1'], ['yarn2'], ['random']])(
'Throw an error because %s is not supported', 'Throw an error because %s is not supported',
async packageManager => { async packageManager => {
await expect(restoreCache(packageManager)).rejects.toThrow( await expect(restoreCache(packageManager, '')).rejects.toThrow(
`Caching for '${packageManager}' is not supported` `Caching for '${packageManager}' is not supported`
); );
} }
@ -132,7 +132,7 @@ describe('cache-restore', () => {
} }
}); });
await restoreCache(packageManager); await restoreCache(packageManager, '');
expect(hashFilesSpy).toHaveBeenCalled(); expect(hashFilesSpy).toHaveBeenCalled();
expect(infoSpy).toHaveBeenCalledWith( expect(infoSpy).toHaveBeenCalledWith(
`Cache restored from key: node-cache-${platform}-${packageManager}-${fileHash}` `Cache restored from key: node-cache-${platform}-${packageManager}-${fileHash}`
@ -163,7 +163,7 @@ describe('cache-restore', () => {
}); });
restoreCacheSpy.mockImplementationOnce(() => undefined); restoreCacheSpy.mockImplementationOnce(() => undefined);
await restoreCache(packageManager); await restoreCache(packageManager, '');
expect(hashFilesSpy).toHaveBeenCalled(); expect(hashFilesSpy).toHaveBeenCalled();
expect(infoSpy).toHaveBeenCalledWith( expect(infoSpy).toHaveBeenCalledWith(
`${packageManager} cache is not found` `${packageManager} cache is not found`

View File

@ -107,18 +107,20 @@ describe('run', () => {
describe('Validate unchanged cache is not saved', () => { describe('Validate unchanged cache is not saved', () => {
it('should not save cache for yarn1', async () => { it('should not save cache for yarn1', async () => {
inputs['cache'] = 'yarn'; inputs['cache'] = 'yarn';
getStateSpy.mockImplementation(() => yarnFileHash); getStateSpy.mockImplementation(key =>
getCommandOutputSpy key === State.CachePrimaryKey || key === State.CacheMatchedKey
.mockImplementationOnce(() => '1.2.3') ? yarnFileHash
.mockImplementationOnce(() => `${commonPath}/yarn1`); : key === State.CachePaths
? '["/foo/bar"]'
: 'not expected'
);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn1`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 1.2.3');
expect(infoSpy).toHaveBeenCalledWith( expect(infoSpy).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.` `Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
); );
@ -127,18 +129,20 @@ describe('run', () => {
it('should not save cache for yarn2', async () => { it('should not save cache for yarn2', async () => {
inputs['cache'] = 'yarn'; inputs['cache'] = 'yarn';
getStateSpy.mockImplementation(() => yarnFileHash); getStateSpy.mockImplementation(key =>
getCommandOutputSpy key === State.CachePrimaryKey || key === State.CacheMatchedKey
.mockImplementationOnce(() => '2.2.3') ? yarnFileHash
.mockImplementationOnce(() => `${commonPath}/yarn2`); : key === State.CachePaths
? '["/foo/bar"]'
: 'not expected'
);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn2`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 2.2.3');
expect(infoSpy).toHaveBeenCalledWith( expect(infoSpy).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.` `Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
); );
@ -147,35 +151,40 @@ describe('run', () => {
it('should not save cache for npm', async () => { it('should not save cache for npm', async () => {
inputs['cache'] = 'npm'; inputs['cache'] = 'npm';
getStateSpy.mockImplementation(() => npmFileHash); getStateSpy.mockImplementation(key =>
key === State.CachePrimaryKey || key === State.CacheMatchedKey
? yarnFileHash
: key === State.CachePaths
? '["/foo/bar"]'
: 'not expected'
);
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(infoSpy).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
);
expect(setFailedSpy).not.toHaveBeenCalled(); expect(setFailedSpy).not.toHaveBeenCalled();
}); });
it('should not save cache for pnpm', async () => { it('should not save cache for pnpm', async () => {
inputs['cache'] = 'pnpm'; inputs['cache'] = 'pnpm';
getStateSpy.mockImplementation(() => pnpmFileHash); getStateSpy.mockImplementation(key =>
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`); key === State.CachePrimaryKey || key === State.CacheMatchedKey
? yarnFileHash
: key === State.CachePaths
? '["/foo/bar"]'
: 'not expected'
);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(infoSpy).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
);
expect(setFailedSpy).not.toHaveBeenCalled(); expect(setFailedSpy).not.toHaveBeenCalled();
}); });
}); });
@ -183,24 +192,22 @@ describe('run', () => {
describe('action saves the cache', () => { describe('action saves the cache', () => {
it('saves cache from yarn 1', async () => { it('saves cache from yarn 1', async () => {
inputs['cache'] = 'yarn'; inputs['cache'] = 'yarn';
getStateSpy.mockImplementation((name: string) => { getStateSpy.mockImplementation((key: string) =>
if (name === State.CacheMatchedKey) { key === State.CacheMatchedKey
return yarnFileHash; ? yarnFileHash
} else { : key === State.CachePrimaryKey
return npmFileHash; ? npmFileHash
} : key === State.CachePaths
}); ? '["/foo/bar"]'
getCommandOutputSpy : 'not expected'
.mockImplementationOnce(() => '1.2.3') );
.mockImplementationOnce(() => `${commonPath}/yarn1`);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn1`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 1.2.3');
expect(infoSpy).not.toHaveBeenCalledWith( expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.` `Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
); );
@ -213,24 +220,22 @@ describe('run', () => {
it('saves cache from yarn 2', async () => { it('saves cache from yarn 2', async () => {
inputs['cache'] = 'yarn'; inputs['cache'] = 'yarn';
getStateSpy.mockImplementation((name: string) => { getStateSpy.mockImplementation((key: string) =>
if (name === State.CacheMatchedKey) { key === State.CacheMatchedKey
return yarnFileHash; ? yarnFileHash
} else { : key === State.CachePrimaryKey
return npmFileHash; ? npmFileHash
} : key === State.CachePaths
}); ? '["/foo/bar"]'
getCommandOutputSpy : 'not expected'
.mockImplementationOnce(() => '2.2.3') );
.mockImplementationOnce(() => `${commonPath}/yarn2`);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn2`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 2.2.3');
expect(infoSpy).not.toHaveBeenCalledWith( expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.` `Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
); );
@ -243,21 +248,22 @@ describe('run', () => {
it('saves cache from npm', async () => { it('saves cache from npm', async () => {
inputs['cache'] = 'npm'; inputs['cache'] = 'npm';
getStateSpy.mockImplementation((name: string) => { getStateSpy.mockImplementation((key: string) =>
if (name === State.CacheMatchedKey) { key === State.CacheMatchedKey
return npmFileHash; ? npmFileHash
} else { : key === State.CachePrimaryKey
return yarnFileHash; ? yarnFileHash
} : key === State.CachePaths
}); ? '["/foo/bar"]'
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); : 'not expected'
);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(infoSpy).not.toHaveBeenCalledWith( expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.` `Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
); );
@ -270,21 +276,22 @@ describe('run', () => {
it('saves cache from pnpm', async () => { it('saves cache from pnpm', async () => {
inputs['cache'] = 'pnpm'; inputs['cache'] = 'pnpm';
getStateSpy.mockImplementation((name: string) => { getStateSpy.mockImplementation((key: string) =>
if (name === State.CacheMatchedKey) { key === State.CacheMatchedKey
return pnpmFileHash; ? pnpmFileHash
} else { : key === State.CachePrimaryKey
return npmFileHash; ? npmFileHash
} : key === State.CachePaths
}); ? '["/foo/bar"]'
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`); : 'not expected'
);
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(infoSpy).not.toHaveBeenCalledWith( expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.` `Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
); );
@ -297,14 +304,15 @@ describe('run', () => {
it('save with -1 cacheId , should not fail workflow', async () => { it('save with -1 cacheId , should not fail workflow', async () => {
inputs['cache'] = 'npm'; inputs['cache'] = 'npm';
getStateSpy.mockImplementation((name: string) => { getStateSpy.mockImplementation((key: string) =>
if (name === State.CacheMatchedKey) { key === State.CacheMatchedKey
return npmFileHash; ? npmFileHash
} else { : key === State.CachePrimaryKey
return yarnFileHash; ? yarnFileHash
} : key === State.CachePaths
}); ? '["/foo/bar"]'
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); : 'not expected'
);
saveCacheSpy.mockImplementation(() => { saveCacheSpy.mockImplementation(() => {
return -1; return -1;
}); });
@ -312,9 +320,9 @@ describe('run', () => {
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(infoSpy).not.toHaveBeenCalledWith( expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.` `Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
); );
@ -327,14 +335,15 @@ describe('run', () => {
it('saves with error from toolkit, should fail workflow', async () => { it('saves with error from toolkit, should fail workflow', async () => {
inputs['cache'] = 'npm'; inputs['cache'] = 'npm';
getStateSpy.mockImplementation((name: string) => { getStateSpy.mockImplementation((key: string) =>
if (name === State.CacheMatchedKey) { key === State.CacheMatchedKey
return npmFileHash; ? npmFileHash
} else { : key === State.CachePrimaryKey
return yarnFileHash; ? yarnFileHash
} : key === State.CachePaths
}); ? '["/foo/bar"]'
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`); : 'not expected'
);
saveCacheSpy.mockImplementation(() => { saveCacheSpy.mockImplementation(() => {
throw new cache.ValidationError('Validation failed'); throw new cache.ValidationError('Validation failed');
}); });
@ -342,9 +351,9 @@ describe('run', () => {
await run(); await run();
expect(getInputSpy).toHaveBeenCalled(); expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(2); expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1); expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`); expect(debugSpy).toHaveBeenCalledTimes(0);
expect(infoSpy).not.toHaveBeenCalledWith( expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.` `Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
); );

View File

@ -2,7 +2,17 @@ import * as core from '@actions/core';
import * as cache from '@actions/cache'; import * as cache from '@actions/cache';
import path from 'path'; import path from 'path';
import * as utils from '../src/cache-utils'; import * as utils from '../src/cache-utils';
import {PackageManagerInfo, isCacheFeatureAvailable} from '../src/cache-utils'; import {
PackageManagerInfo,
isCacheFeatureAvailable,
supportedPackageManagers,
getCommandOutput
} from '../src/cache-utils';
import fs from 'fs';
import * as cacheUtils from '../src/cache-utils';
import * as glob from '@actions/glob';
import {Globber} from '@actions/glob';
import {MockGlobber} from './mock/glob-mock';
describe('cache-utils', () => { describe('cache-utils', () => {
const versionYarn1 = '1.2.3'; const versionYarn1 = '1.2.3';
@ -30,7 +40,7 @@ describe('cache-utils', () => {
it.each<[string, PackageManagerInfo | null]>([ it.each<[string, PackageManagerInfo | null]>([
['npm', utils.supportedPackageManagers.npm], ['npm', utils.supportedPackageManagers.npm],
['pnpm', utils.supportedPackageManagers.pnpm], ['pnpm', utils.supportedPackageManagers.pnpm],
['yarn', utils.supportedPackageManagers.yarn1], ['yarn', utils.supportedPackageManagers.yarn],
['yarn1', null], ['yarn1', null],
['yarn2', null], ['yarn2', null],
['npm7', null] ['npm7', null]
@ -72,4 +82,261 @@ describe('cache-utils', () => {
jest.resetAllMocks(); jest.resetAllMocks();
jest.clearAllMocks(); jest.clearAllMocks();
}); });
describe('getCacheDirectoriesPaths', () => {
let existsSpy: jest.SpyInstance;
let lstatSpy: jest.SpyInstance;
let globCreateSpy: jest.SpyInstance;
beforeEach(() => {
existsSpy = jest.spyOn(fs, 'existsSync');
existsSpy.mockImplementation(() => true);
lstatSpy = jest.spyOn(fs, 'lstatSync');
lstatSpy.mockImplementation(arg => ({
isDirectory: () => true
}));
globCreateSpy = jest.spyOn(glob, 'create');
globCreateSpy.mockImplementation(
(pattern: string): Promise<Globber> =>
MockGlobber.create(['/foo', '/bar'])
);
});
afterEach(() => {
existsSpy.mockRestore();
lstatSpy.mockRestore();
globCreateSpy.mockRestore();
});
it.each([
[supportedPackageManagers.npm, ''],
[supportedPackageManagers.npm, '/dir/file.lock'],
[supportedPackageManagers.npm, '/**/file.lock'],
[supportedPackageManagers.pnpm, ''],
[supportedPackageManagers.pnpm, '/dir/file.lock'],
[supportedPackageManagers.pnpm, '/**/file.lock']
])(
'getCacheDirectoriesPaths should return one dir for non yarn',
async (packageManagerInfo, cacheDependency) => {
getCommandOutputSpy.mockImplementation(() => 'foo');
const dirs = await cacheUtils.getCacheDirectories(
packageManagerInfo,
cacheDependency
);
expect(dirs).toEqual(['foo']);
// to do not call for a version
// call once for get cache folder
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
}
);
it('getCacheDirectoriesPaths should return one dir for yarn without cacheDependency', async () => {
getCommandOutputSpy.mockImplementation(() => 'foo');
const dirs = await cacheUtils.getCacheDirectories(
supportedPackageManagers.yarn,
''
);
expect(dirs).toEqual(['foo']);
});
it.each([
[supportedPackageManagers.npm, ''],
[supportedPackageManagers.npm, '/dir/file.lock'],
[supportedPackageManagers.npm, '/**/file.lock'],
[supportedPackageManagers.pnpm, ''],
[supportedPackageManagers.pnpm, '/dir/file.lock'],
[supportedPackageManagers.pnpm, '/**/file.lock'],
[supportedPackageManagers.yarn, ''],
[supportedPackageManagers.yarn, '/dir/file.lock'],
[supportedPackageManagers.yarn, '/**/file.lock']
])(
'getCacheDirectoriesPaths should throw for getCommandOutput returning empty',
async (packageManagerInfo, cacheDependency) => {
getCommandOutputSpy.mockImplementation((command: string) =>
// return empty string to indicate getCacheFolderPath failed
// --version still works
command.includes('version') ? '1.' : ''
);
await expect(
cacheUtils.getCacheDirectories(packageManagerInfo, cacheDependency)
).rejects.toThrow(); //'Could not get cache folder path for /dir');
}
);
it.each([
[supportedPackageManagers.yarn, '/dir/file.lock'],
[supportedPackageManagers.yarn, '/**/file.lock']
])(
'getCacheDirectoriesPaths should nothrow in case of having not directories',
async (packageManagerInfo, cacheDependency) => {
lstatSpy.mockImplementation(arg => ({
isDirectory: () => false
}));
await cacheUtils.getCacheDirectories(
packageManagerInfo,
cacheDependency
);
expect(warningSpy).toHaveBeenCalledTimes(1);
expect(warningSpy).toHaveBeenCalledWith(
`No existing directories found containing cache-dependency-path="${cacheDependency}"`
);
}
);
it.each(['1.1.1', '2.2.2'])(
'getCacheDirectoriesPaths yarn v%s should return one dir without cacheDependency',
async version => {
getCommandOutputSpy.mockImplementationOnce(() => version);
getCommandOutputSpy.mockImplementationOnce(() => `foo${version}`);
const dirs = await cacheUtils.getCacheDirectories(
supportedPackageManagers.yarn,
''
);
expect(dirs).toEqual([`foo${version}`]);
}
);
it.each(['1.1.1', '2.2.2'])(
'getCacheDirectoriesPaths yarn v%s should return 2 dirs with globbed cacheDependency',
async version => {
let dirNo = 1;
getCommandOutputSpy.mockImplementation((command: string) =>
command.includes('version') ? version : `file_${version}_${dirNo++}`
);
globCreateSpy.mockImplementation(
(pattern: string): Promise<Globber> =>
MockGlobber.create(['/tmp/dir1/file', '/tmp/dir2/file'])
);
const dirs = await cacheUtils.getCacheDirectories(
supportedPackageManagers.yarn,
'/tmp/**/file'
);
expect(dirs).toEqual([`file_${version}_1`, `file_${version}_2`]);
}
);
it.each(['1.1.1', '2.2.2'])(
'getCacheDirectoriesPaths yarn v%s should return 2 dirs with globbed cacheDependency expanding to duplicates',
async version => {
let dirNo = 1;
getCommandOutputSpy.mockImplementation((command: string) =>
command.includes('version') ? version : `file_${version}_${dirNo++}`
);
globCreateSpy.mockImplementation(
(pattern: string): Promise<Globber> =>
MockGlobber.create([
'/tmp/dir1/file',
'/tmp/dir2/file',
'/tmp/dir1/file'
])
);
const dirs = await cacheUtils.getCacheDirectories(
supportedPackageManagers.yarn,
'/tmp/**/file'
);
expect(dirs).toEqual([`file_${version}_1`, `file_${version}_2`]);
}
);
it.each(['1.1.1', '2.2.2'])(
'getCacheDirectoriesPaths yarn v%s should return 2 uniq dirs despite duplicate cache directories',
async version => {
let dirNo = 1;
getCommandOutputSpy.mockImplementation((command: string) =>
command.includes('version')
? version
: `file_${version}_${dirNo++ % 2}`
);
globCreateSpy.mockImplementation(
(pattern: string): Promise<Globber> =>
MockGlobber.create([
'/tmp/dir1/file',
'/tmp/dir2/file',
'/tmp/dir3/file'
])
);
const dirs = await cacheUtils.getCacheDirectories(
supportedPackageManagers.yarn,
'/tmp/**/file'
);
expect(dirs).toEqual([`file_${version}_1`, `file_${version}_0`]);
expect(getCommandOutputSpy).toHaveBeenCalledTimes(6);
expect(getCommandOutputSpy).toHaveBeenCalledWith(
'yarn --version',
'/tmp/dir1'
);
expect(getCommandOutputSpy).toHaveBeenCalledWith(
'yarn --version',
'/tmp/dir2'
);
expect(getCommandOutputSpy).toHaveBeenCalledWith(
'yarn --version',
'/tmp/dir3'
);
expect(getCommandOutputSpy).toHaveBeenCalledWith(
version.startsWith('1.')
? 'yarn cache dir'
: 'yarn config get cacheFolder',
'/tmp/dir1'
);
expect(getCommandOutputSpy).toHaveBeenCalledWith(
version.startsWith('1.')
? 'yarn cache dir'
: 'yarn config get cacheFolder',
'/tmp/dir2'
);
expect(getCommandOutputSpy).toHaveBeenCalledWith(
version.startsWith('1.')
? 'yarn cache dir'
: 'yarn config get cacheFolder',
'/tmp/dir3'
);
}
);
it.each(['1.1.1', '2.2.2'])(
'getCacheDirectoriesPaths yarn v%s should return 4 dirs with multiple globs',
async version => {
// simulate wrong indents
const cacheDependencyPath = `/tmp/dir1/file
/tmp/dir2/file
/tmp/**/file
`;
globCreateSpy.mockImplementation(
(pattern: string): Promise<Globber> =>
MockGlobber.create([
'/tmp/dir1/file',
'/tmp/dir2/file',
'/tmp/dir3/file',
'/tmp/dir4/file'
])
);
let dirNo = 1;
getCommandOutputSpy.mockImplementation((command: string) =>
command.includes('version') ? version : `file_${version}_${dirNo++}`
);
const dirs = await cacheUtils.getCacheDirectories(
supportedPackageManagers.yarn,
cacheDependencyPath
);
expect(dirs).toEqual([
`file_${version}_1`,
`file_${version}_2`,
`file_${version}_3`,
`file_${version}_4`
]);
}
);
});
}); });

View File

@ -0,0 +1,18 @@
import {MockGlobber} from './glob-mock';
describe('mocked globber tests', () => {
it('globber should return generator', async () => {
const globber = new MockGlobber(['aaa', 'bbb', 'ccc']);
const generator = globber.globGenerator();
const result: string[] = [];
for await (const itemPath of generator) {
result.push(itemPath);
}
expect(result).toEqual(['aaa', 'bbb', 'ccc']);
});
it('globber should return glob', async () => {
const globber = new MockGlobber(['aaa', 'bbb', 'ccc']);
const result: string[] = await globber.glob();
expect(result).toEqual(['aaa', 'bbb', 'ccc']);
});
});

View File

@ -0,0 +1,29 @@
import {Globber} from '@actions/glob';
export class MockGlobber implements Globber {
private readonly expected: string[];
constructor(expected: string[]) {
this.expected = expected;
}
getSearchPaths(): string[] {
return this.expected.slice();
}
async glob(): Promise<string[]> {
const result: string[] = [];
for await (const itemPath of this.globGenerator()) {
result.push(itemPath);
}
return result;
}
async *globGenerator(): AsyncGenerator<string, void> {
for (const e of this.expected) {
yield e;
}
}
static async create(expected: string[]): Promise<MockGlobber> {
return new MockGlobber(expected);
}
}

View File

@ -0,0 +1,48 @@
#!/bin/sh -e
export YARN_ENABLE_IMMUTABLE_INSTALLS=false
rm package.json
rm package-lock.json
echo "create yarn2 project in the sub2"
mkdir sub2
cd sub2
cat <<EOT >package.json
{
"name": "subproject",
"dependencies": {
"random": "^3.0.6",
"uuid": "^9.0.0"
}
}
EOT
yarn set version 2.4.3
yarn install
echo "create yarn3 project in the sub3"
cd ..
mkdir sub3
cd sub3
cat <<EOT >package.json
{
"name": "subproject",
"dependencies": {
"random": "^3.0.6",
"uuid": "^9.0.0"
}
}
EOT
yarn set version 3.5.1
yarn install
echo "create yarn1 project in the root"
cd ..
cat <<EOT >package.json
{
"name": "subproject",
"dependencies": {
"random": "^3.0.6",
"uuid": "^9.0.0"
}
}
EOT
yarn set version 1.22.19
yarn install

1436
dist/cache-save/index.js vendored

File diff suppressed because it is too large Load Diff

141
dist/setup/index.js vendored
View File

@ -71144,7 +71144,8 @@ const restoreCache = (packageManager, cacheDependencyPath) => __awaiter(void 0,
throw new Error(`Caching for '${packageManager}' is not supported`); throw new Error(`Caching for '${packageManager}' is not supported`);
} }
const platform = process.env.RUNNER_OS; const platform = process.env.RUNNER_OS;
const cachePath = yield cache_utils_1.getCacheDirectoryPath(packageManagerInfo, packageManager); const cachePaths = yield cache_utils_1.getCacheDirectories(packageManagerInfo, cacheDependencyPath);
core.saveState(constants_1.State.CachePaths, cachePaths);
const lockFilePath = cacheDependencyPath const lockFilePath = cacheDependencyPath
? cacheDependencyPath ? cacheDependencyPath
: findLockFile(packageManagerInfo); : findLockFile(packageManagerInfo);
@ -71155,7 +71156,7 @@ const restoreCache = (packageManager, cacheDependencyPath) => __awaiter(void 0,
const primaryKey = `node-cache-${platform}-${packageManager}-${fileHash}`; const primaryKey = `node-cache-${platform}-${packageManager}-${fileHash}`;
core.debug(`primary key is ${primaryKey}`); core.debug(`primary key is ${primaryKey}`);
core.saveState(constants_1.State.CachePrimaryKey, primaryKey); core.saveState(constants_1.State.CachePrimaryKey, primaryKey);
const cacheKey = yield cache.restoreCache([cachePath], primaryKey); const cacheKey = yield cache.restoreCache(cachePaths, primaryKey);
core.setOutput('cache-hit', Boolean(cacheKey)); core.setOutput('cache-hit', Boolean(cacheKey));
if (!cacheKey) { if (!cacheKey) {
core.info(`${packageManager} cache is not found`); core.info(`${packageManager} cache is not found`);
@ -71212,31 +71213,47 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectoryPath = exports.getPackageManagerInfo = exports.getCommandOutput = exports.supportedPackageManagers = void 0; exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectories = exports.getPackageManagerInfo = exports.getCommandOutputNotEmpty = exports.getCommandOutput = exports.supportedPackageManagers = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const exec = __importStar(__nccwpck_require__(1514)); const exec = __importStar(__nccwpck_require__(1514));
const cache = __importStar(__nccwpck_require__(7799)); const cache = __importStar(__nccwpck_require__(7799));
const glob = __importStar(__nccwpck_require__(8090));
const path_1 = __importDefault(__nccwpck_require__(1017));
const fs_1 = __importDefault(__nccwpck_require__(7147));
const util_1 = __nccwpck_require__(2629);
exports.supportedPackageManagers = { exports.supportedPackageManagers = {
npm: { npm: {
name: 'npm',
lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'], lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'],
getCacheFolderCommand: 'npm config get cache' getCacheFolderPath: () => exports.getCommandOutputNotEmpty('npm config get cache', 'Could not get npm cache folder path')
}, },
pnpm: { pnpm: {
name: 'pnpm',
lockFilePatterns: ['pnpm-lock.yaml'], lockFilePatterns: ['pnpm-lock.yaml'],
getCacheFolderCommand: 'pnpm store path --silent' getCacheFolderPath: () => exports.getCommandOutputNotEmpty('pnpm store path --silent', 'Could not get pnpm cache folder path')
}, },
yarn1: { yarn: {
name: 'yarn',
lockFilePatterns: ['yarn.lock'], lockFilePatterns: ['yarn.lock'],
getCacheFolderCommand: 'yarn cache dir' getCacheFolderPath: (projectDir) => __awaiter(void 0, void 0, void 0, function* () {
}, const yarnVersion = yield exports.getCommandOutputNotEmpty(`yarn --version`, 'Could not retrieve version of yarn', projectDir);
yarn2: { core.debug(`Consumed yarn version is ${yarnVersion} (working dir: "${projectDir || ''}")`);
lockFilePatterns: ['yarn.lock'], const stdOut = yarnVersion.startsWith('1.')
getCacheFolderCommand: 'yarn config get cacheFolder' ? yield exports.getCommandOutput('yarn cache dir', projectDir)
: yield exports.getCommandOutput('yarn config get cacheFolder', projectDir);
if (!stdOut) {
throw new Error(`Could not get yarn cache folder path for ${projectDir}`);
}
return stdOut;
})
} }
}; };
const getCommandOutput = (toolCommand) => __awaiter(void 0, void 0, void 0, function* () { const getCommandOutput = (toolCommand, cwd) => __awaiter(void 0, void 0, void 0, function* () {
let { stdout, stderr, exitCode } = yield exec.getExecOutput(toolCommand, undefined, { ignoreReturnCode: true }); let { stdout, stderr, exitCode } = yield exec.getExecOutput(toolCommand, undefined, Object.assign({ ignoreReturnCode: true }, (cwd && { cwd })));
if (exitCode) { if (exitCode) {
stderr = !stderr.trim() stderr = !stderr.trim()
? `The '${toolCommand}' command failed with exit code: ${exitCode}` ? `The '${toolCommand}' command failed with exit code: ${exitCode}`
@ -71246,13 +71263,14 @@ const getCommandOutput = (toolCommand) => __awaiter(void 0, void 0, void 0, func
return stdout.trim(); return stdout.trim();
}); });
exports.getCommandOutput = getCommandOutput; exports.getCommandOutput = getCommandOutput;
const getPackageManagerVersion = (packageManager, command) => __awaiter(void 0, void 0, void 0, function* () { const getCommandOutputNotEmpty = (toolCommand, error, cwd) => __awaiter(void 0, void 0, void 0, function* () {
const stdOut = yield exports.getCommandOutput(`${packageManager} ${command}`); const stdOut = exports.getCommandOutput(toolCommand, cwd);
if (!stdOut) { if (!stdOut) {
throw new Error(`Could not retrieve version of ${packageManager}`); throw new Error(error);
} }
return stdOut; return stdOut;
}); });
exports.getCommandOutputNotEmpty = getCommandOutputNotEmpty;
const getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, void 0, function* () { const getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, void 0, function* () {
if (packageManager === 'npm') { if (packageManager === 'npm') {
return exports.supportedPackageManagers.npm; return exports.supportedPackageManagers.npm;
@ -71261,29 +71279,75 @@ const getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, void
return exports.supportedPackageManagers.pnpm; return exports.supportedPackageManagers.pnpm;
} }
else if (packageManager === 'yarn') { else if (packageManager === 'yarn') {
const yarnVersion = yield getPackageManagerVersion('yarn', '--version'); return exports.supportedPackageManagers.yarn;
core.debug(`Consumed yarn version is ${yarnVersion}`);
if (yarnVersion.startsWith('1.')) {
return exports.supportedPackageManagers.yarn1;
}
else {
return exports.supportedPackageManagers.yarn2;
}
} }
else { else {
return null; return null;
} }
}); });
exports.getPackageManagerInfo = getPackageManagerInfo; exports.getPackageManagerInfo = getPackageManagerInfo;
const getCacheDirectoryPath = (packageManagerInfo, packageManager) => __awaiter(void 0, void 0, void 0, function* () { /**
const stdOut = yield exports.getCommandOutput(packageManagerInfo.getCacheFolderCommand); * Expands (converts) the string input `cache-dependency-path` to list of directories that
if (!stdOut) { * may be project roots
throw new Error(`Could not get cache folder path for ${packageManager}`); * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
} * expected to be the result of `core.getInput('cache-dependency-path')`
core.debug(`${packageManager} path is ${stdOut}`); * @return list of directories and possible
return stdOut.trim(); */
const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
const globber = yield glob.create(cacheDependencyPath);
const cacheDependenciesPaths = yield globber.glob();
const existingDirectories = cacheDependenciesPaths
.map(path_1.default.dirname)
.filter(util_1.unique())
.filter(directory => fs_1.default.lstatSync(directory).isDirectory());
if (!existingDirectories.length)
core.warning(`No existing directories found containing cache-dependency-path="${cacheDependencyPath}"`);
return existingDirectories;
}); });
exports.getCacheDirectoryPath = getCacheDirectoryPath; /**
* 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 = (packageManagerInfo, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
const projectDirectories = yield getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath);
const cacheFoldersPaths = yield Promise.all(projectDirectories.map((projectDirectory) => __awaiter(void 0, void 0, void 0, function* () {
const cacheFolderPath = 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(util_1.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 = (packageManagerInfo) => __awaiter(void 0, void 0, void 0, function* () {
const cacheFolderPath = yield 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
*/
const getCacheDirectories = (packageManagerInfo, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
// 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);
});
exports.getCacheDirectories = getCacheDirectories;
function isGhes() { function isGhes() {
const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com');
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'; return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM';
@ -71321,6 +71385,7 @@ var State;
(function (State) { (function (State) {
State["CachePrimaryKey"] = "CACHE_KEY"; State["CachePrimaryKey"] = "CACHE_KEY";
State["CacheMatchedKey"] = "CACHE_RESULT"; State["CacheMatchedKey"] = "CACHE_RESULT";
State["CachePaths"] = "CACHE_PATHS";
})(State = exports.State || (exports.State = {})); })(State = exports.State || (exports.State = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {
@ -72159,7 +72224,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}); });
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.printEnvDetailsAndSetOutput = exports.parseNodeVersionFile = void 0; exports.unique = exports.printEnvDetailsAndSetOutput = exports.parseNodeVersionFile = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const exec = __importStar(__nccwpck_require__(1514)); const exec = __importStar(__nccwpck_require__(1514));
function parseNodeVersionFile(contents) { function parseNodeVersionFile(contents) {
@ -72221,6 +72286,16 @@ function getToolVersion(tool, options) {
} }
}); });
} }
const unique = () => {
const encountered = new Set();
return (value) => {
if (encountered.has(value))
return false;
encountered.add(value);
return true;
};
};
exports.unique = unique;
/***/ }), /***/ }),

View File

@ -261,10 +261,14 @@ steps:
with: with:
node-version: '14' node-version: '14'
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install
- run: pnpm test - run: pnpm test
``` ```
> **Note**: By default `--frozen-lockfile` option is passed starting from pnpm `6.10.x`. It will be automatically added if you run it on [CI](https://pnpm.io/cli/install#--frozen-lockfile).
> If the `pnpm-lock.yaml` file changes then pass `--frozen-lockfile` option.
**Using wildcard patterns to cache dependencies** **Using wildcard patterns to cache dependencies**
```yaml ```yaml
steps: steps:
@ -401,11 +405,14 @@ steps:
yarn config set npmScopes.my-org.npmAlwaysAuth true yarn config set npmScopes.my-org.npmAlwaysAuth true
yarn config set npmScopes.my-org.npmAuthToken $NPM_AUTH_TOKEN yarn config set npmScopes.my-org.npmAuthToken $NPM_AUTH_TOKEN
env: env:
NPM_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }} NPM_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
``` ```
NOTE: As per https://github.com/actions/setup-node/issues/49 you cannot use `secrets.GITHUB_TOKEN` to access private GitHub Packages within the same organisation but in a different repository.
To access private GitHub Packages within the same organization, go to "Manage Actions access" in Package settings and set the repositories you want to access.
Please refer to the [Ensuring workflow access to your package - Configuring a package's access control and visibility](https://docs.github.com/en/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility#ensuring-workflow-access-to-your-package) for more details.
### always-auth input ### always-auth input
The always-auth input sets `always-auth=true` in .npmrc file. With this option set [npm](https://docs.npmjs.com/cli/v6/using-npm/config#always-auth)/yarn sends the authentication credentials when making a request to the registries. The always-auth input sets `always-auth=true` in .npmrc file. With this option set [npm](https://docs.npmjs.com/cli/v6/using-npm/config#always-auth)/yarn sends the authentication credentials when making a request to the registries.

View File

@ -6,14 +6,14 @@ import fs from 'fs';
import {State} from './constants'; import {State} from './constants';
import { import {
getCacheDirectoryPath, getCacheDirectories,
getPackageManagerInfo, getPackageManagerInfo,
PackageManagerInfo PackageManagerInfo
} from './cache-utils'; } from './cache-utils';
export const restoreCache = async ( export const restoreCache = async (
packageManager: string, packageManager: string,
cacheDependencyPath?: string cacheDependencyPath: string
) => { ) => {
const packageManagerInfo = await getPackageManagerInfo(packageManager); const packageManagerInfo = await getPackageManagerInfo(packageManager);
if (!packageManagerInfo) { if (!packageManagerInfo) {
@ -21,10 +21,11 @@ export const restoreCache = async (
} }
const platform = process.env.RUNNER_OS; const platform = process.env.RUNNER_OS;
const cachePath = await getCacheDirectoryPath( const cachePaths = await getCacheDirectories(
packageManagerInfo, packageManagerInfo,
packageManager cacheDependencyPath
); );
core.saveState(State.CachePaths, cachePaths);
const lockFilePath = cacheDependencyPath const lockFilePath = cacheDependencyPath
? cacheDependencyPath ? cacheDependencyPath
: findLockFile(packageManagerInfo); : findLockFile(packageManagerInfo);
@ -41,7 +42,7 @@ export const restoreCache = async (
core.saveState(State.CachePrimaryKey, primaryKey); core.saveState(State.CachePrimaryKey, primaryKey);
const cacheKey = await cache.restoreCache([cachePath], primaryKey); const cacheKey = await cache.restoreCache(cachePaths, primaryKey);
core.setOutput('cache-hit', Boolean(cacheKey)); core.setOutput('cache-hit', Boolean(cacheKey));
if (!cacheKey) { if (!cacheKey) {
@ -56,6 +57,7 @@ export const restoreCache = async (
const findLockFile = (packageManager: PackageManagerInfo) => { const findLockFile = (packageManager: PackageManagerInfo) => {
const lockFiles = packageManager.lockFilePatterns; const lockFiles = packageManager.lockFilePatterns;
const workspace = process.env.GITHUB_WORKSPACE!; const workspace = process.env.GITHUB_WORKSPACE!;
const rootContent = fs.readdirSync(workspace); const rootContent = fs.readdirSync(workspace);
const lockFile = lockFiles.find(item => rootContent.includes(item)); const lockFile = lockFiles.find(item => rootContent.includes(item));

View File

@ -1,8 +1,7 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as cache from '@actions/cache'; import * as cache from '@actions/cache';
import fs from 'fs';
import {State} from './constants'; import {State} from './constants';
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils'; import {getPackageManagerInfo} from './cache-utils';
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@ -24,6 +23,7 @@ export async function run() {
const cachePackages = async (packageManager: string) => { const cachePackages = async (packageManager: string) => {
const state = core.getState(State.CacheMatchedKey); const state = core.getState(State.CacheMatchedKey);
const primaryKey = core.getState(State.CachePrimaryKey); const primaryKey = core.getState(State.CachePrimaryKey);
const cachePaths = JSON.parse(core.getState(State.CachePaths) || '[]');
const packageManagerInfo = await getPackageManagerInfo(packageManager); const packageManagerInfo = await getPackageManagerInfo(packageManager);
if (!packageManagerInfo) { if (!packageManagerInfo) {
@ -31,14 +31,12 @@ const cachePackages = async (packageManager: string) => {
return; return;
} }
const cachePath = await getCacheDirectoryPath( if (cachePaths.length === 0) {
packageManagerInfo, // TODO: core.getInput has a bug - it can return undefined despite its definition (tests only?)
packageManager // export declare function getInput(name: string, options?: InputOptions): string;
); const cacheDependencyPath = core.getInput('cache-dependency-path') || '';
if (!fs.existsSync(cachePath)) {
throw new Error( throw new Error(
`Cache folder path is retrieved for ${packageManager} but doesn't exist on disk: ${cachePath}` `Cache folder paths are not retrieved for ${packageManager} with cache-dependency-path = ${cacheDependencyPath}`
); );
} }
@ -49,7 +47,7 @@ const cachePackages = async (packageManager: string) => {
return; return;
} }
const cacheId = await cache.saveCache([cachePath], primaryKey); const cacheId = await cache.saveCache(cachePaths, primaryKey);
if (cacheId == -1) { if (cacheId == -1) {
return; return;
} }

View File

@ -1,40 +1,79 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import * as cache from '@actions/cache'; import * as cache from '@actions/cache';
import * as glob from '@actions/glob';
type SupportedPackageManagers = { import path from 'path';
[prop: string]: PackageManagerInfo; import fs from 'fs';
}; import {unique} from './util';
export interface PackageManagerInfo { export interface PackageManagerInfo {
name: string;
lockFilePatterns: Array<string>; lockFilePatterns: Array<string>;
getCacheFolderCommand: string; getCacheFolderPath: (projectDir?: string) => Promise<string>;
} }
interface SupportedPackageManagers {
npm: PackageManagerInfo;
pnpm: PackageManagerInfo;
yarn: PackageManagerInfo;
}
export const supportedPackageManagers: SupportedPackageManagers = { export const supportedPackageManagers: SupportedPackageManagers = {
npm: { npm: {
name: 'npm',
lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'], lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'],
getCacheFolderCommand: 'npm config get cache' getCacheFolderPath: () =>
getCommandOutputNotEmpty(
'npm config get cache',
'Could not get npm cache folder path'
)
}, },
pnpm: { pnpm: {
name: 'pnpm',
lockFilePatterns: ['pnpm-lock.yaml'], lockFilePatterns: ['pnpm-lock.yaml'],
getCacheFolderCommand: 'pnpm store path --silent' getCacheFolderPath: () =>
getCommandOutputNotEmpty(
'pnpm store path --silent',
'Could not get pnpm cache folder path'
)
}, },
yarn1: { yarn: {
name: 'yarn',
lockFilePatterns: ['yarn.lock'], lockFilePatterns: ['yarn.lock'],
getCacheFolderCommand: 'yarn cache dir' getCacheFolderPath: async projectDir => {
}, const yarnVersion = await getCommandOutputNotEmpty(
yarn2: { `yarn --version`,
lockFilePatterns: ['yarn.lock'], 'Could not retrieve version of yarn',
getCacheFolderCommand: 'yarn config get cacheFolder' 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) => { export const getCommandOutput = async (
toolCommand: string,
cwd?: string
): Promise<string> => {
let {stdout, stderr, exitCode} = await exec.getExecOutput( let {stdout, stderr, exitCode} = await exec.getExecOutput(
toolCommand, toolCommand,
undefined, undefined,
{ignoreReturnCode: true} {ignoreReturnCode: true, ...(cwd && {cwd})}
); );
if (exitCode) { if (exitCode) {
@ -47,16 +86,15 @@ export const getCommandOutput = async (toolCommand: string) => {
return stdout.trim(); return stdout.trim();
}; };
const getPackageManagerVersion = async ( export const getCommandOutputNotEmpty = async (
packageManager: string, toolCommand: string,
command: string error: string,
) => { cwd?: string
const stdOut = await getCommandOutput(`${packageManager} ${command}`); ): Promise<string> => {
const stdOut = getCommandOutput(toolCommand, cwd);
if (!stdOut) { if (!stdOut) {
throw new Error(`Could not retrieve version of ${packageManager}`); throw new Error(error);
} }
return stdOut; return stdOut;
}; };
@ -66,35 +104,102 @@ export const getPackageManagerInfo = async (packageManager: string) => {
} else if (packageManager === 'pnpm') { } else if (packageManager === 'pnpm') {
return supportedPackageManagers.pnpm; return supportedPackageManagers.pnpm;
} else if (packageManager === 'yarn') { } else if (packageManager === 'yarn') {
const yarnVersion = await getPackageManagerVersion('yarn', '--version'); return supportedPackageManagers.yarn;
core.debug(`Consumed yarn version is ${yarnVersion}`);
if (yarnVersion.startsWith('1.')) {
return supportedPackageManagers.yarn1;
} else {
return supportedPackageManagers.yarn2;
}
} else { } else {
return null; return null;
} }
}; };
export const getCacheDirectoryPath = async ( /**
* 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[]> => {
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}"`
);
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, packageManagerInfo: PackageManagerInfo,
packageManager: string cacheDependencyPath: string
) => { ): Promise<string[]> => {
const stdOut = await getCommandOutput( const projectDirectories = await getProjectDirectoriesFromCacheDependencyPath(
packageManagerInfo.getCacheFolderCommand cacheDependencyPath
); );
const cacheFoldersPaths = await Promise.all(
projectDirectories.map(async projectDirectory => {
const cacheFolderPath =
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());
};
if (!stdOut) { /**
throw new Error(`Could not get cache folder path for ${packageManager}`); * 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);
core.debug(`${packageManager} path is ${stdOut}`);
return stdOut.trim();
}; };
export function isGhes(): boolean { export function isGhes(): boolean {

View File

@ -6,7 +6,8 @@ export enum LockType {
export enum State { export enum State {
CachePrimaryKey = 'CACHE_KEY', CachePrimaryKey = 'CACHE_KEY',
CacheMatchedKey = 'CACHE_RESULT' CacheMatchedKey = 'CACHE_RESULT',
CachePaths = 'CACHE_PATHS'
} }
export enum Outputs { export enum Outputs {

View File

@ -61,3 +61,12 @@ async function getToolVersion(tool: string, options: string[]) {
return ''; return '';
} }
} }
export const unique = () => {
const encountered = new Set();
return (value: unknown): boolean => {
if (encountered.has(value)) return false;
encountered.add(value);
return true;
};
};